<?xml version="1.0" encoding="utf-8"?>
<AlvaoApplication xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" ModelVersion="1">
  <Applications>
    <Application id="1006">
      <Name>Lansweeper Connector</Name>
      <UniqueId>2bc667ec-3833-49a8-bf92-e28805928030</UniqueId>
      <Version>4</Version>
      <AdvancedSettings>
        <Setting>
          <Name>Lansweeper.Token</Name>
          <Value />
        </Setting>
        <Setting>
          <Name>Lansweeper.Url</Name>
          <Value />
        </Setting>
      </AdvancedSettings>
      <Scripts>
        <Script id="2046">
          <Name>LansweeperConnectorPeriodicAction</Name>
          <Code>using System;
using System.Data;
using Microsoft.Data.SqlClient;
using Alvao.Global;
using Alvao.API.Common;
using Alvao.Apps.API;
using Alvao.API.Common.Model.Database;
using System.Threading;
using System.Collections.Generic;
using Alvao.Context;
using Dapper;
using System.Threading.Tasks;
using System.Net.Http;
using System.Text;
using System.Net.Http.Headers;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using System.Linq;
using static Alvao.API.AM.Model.Kind;
using Alvao.API.Utils;
using static Alvao.Global.ModuleInfo;
using Alvao.API.AM.Model;
using Alvao.API.Common.Model.CustomApps.Requests;

public class PeriodicAction : IPeriodicAction
{
    public const string ConnectorName = "Lansweeper";

    public string Name 
    { 
        get =&gt; "PeriodicAction";
        set { }
    }

    private readonly LansweeperConnectorRepository LansweeperConnectorRepository = new LansweeperConnectorRepository();
    private readonly LansweeperClientService LansweeperClientService = new LansweeperClientService();
    private int ImportedObjectsNodeFolderNodeId = 0;
    private Dictionary&lt;int, bool&gt; CheckedTemplatePropMap = new();

    public void OnPeriod(SqlConnection con)
    {
        if(!Activation.IsModuleActivated(ModuleId.MonitoringConnectors))
        {
            Logger.Log.Warn($"{ConnectorName} Connector cannot be used. Module MonitoringConnectors is not activated.");
            return;
        }

        if(string.IsNullOrEmpty(DbProperty.LansweeperToken) || string.IsNullOrEmpty(DbProperty.LansweeperUrl))
        {
            Logger.Log.Warn($"{ConnectorName} Connector is not properly set. Check Lansweeper.Token and Lansweeper.Url in advanced settings.");
            return;
        }

        try
        {
            Task task = Task.Run(async () =&gt; await CreateNewOrUpdateChangedDevices(CancellationToken.None));
            task.Wait(); 
        }
        catch (Exception ex)
        {
            Logger.Log.Error(ex, $"Error importing devices from {ConnectorName}.");
        }
    }

    private async Task CreateNewOrUpdateChangedDevices(CancellationToken cancellationToken)
    {
        var systemPersonId = Alvao.API.Common.Person.GetSystem().iPersonId;
        DateTime? lastAmLsCheckInDateTime = LansweeperConnectorRepository.GetLansweeperLastCheckInDevicesDateTime();

        IEnumerable&lt;LansweeperDevice&gt; newOrUpdatedIntuneDevices = await GetNewOrUpdatedDevices(lastAmLsCheckInDateTime, cancellationToken);
        cancellationToken.ThrowIfCancellationRequested();

        if (newOrUpdatedIntuneDevices is null || !newOrUpdatedIntuneDevices.Any())
            return;

        CheckOrAddMissingPropertiesToTemplateClasses();
        LansweeperConnectorRepository.CreateConnectorScannerIfNotExists();
        LansweeperConnectorRepository.ClearScannerPropertyLockoutTable();

        foreach (var device in newOrUpdatedIntuneDevices.OrderBy(d =&gt; d.LansweeperLastCheckIn))
        {
            Logger.Log.Info($"Processing device {device}");

            cancellationToken.ThrowIfCancellationRequested();

            using var scope = AlvaoContext.GetConnectionScope();
            try
            {
                scope.BeginTransaction();
                tblNode node;
                if (device.NodeId is null || device.NodeId == 0)
                {
                    node = CreateNewDevice(device);
                    if (node.intNodeId == 0)
                        continue;

                    Logger.Log.Info("Created new device '{0}': {1}", device.DeviceType.ToString(), device.Hostname);
                }
                else 
                {
                    node = new tblNode() { intNodeId = device.NodeId.Value };
                    Logger.Log.Info($"Device found in AM. Updating properties.");
                }
                    

                if (node is null)
                    throw new NullReferenceException($"Error: Device ID: '{device.LansweeperDeviceId}' not found in DB and creation failed.");

                LansweeperConnectorRepository.RecalculateScannerPropertyLockoutTable(device, node.lintClassId ?? 0);

                List&lt;(tblKind.KindCode kindCode, object value)&gt; updatedProperties = UpdateDeviceProperties(node.intNodeId, device);
                //Logger.Log.Info("Updated {0} properties.", updatedCnt);

                scope.CommitTransaction();

                foreach(var property in updatedProperties){
                    OnObjectPropertyModifiedRequest req = new OnObjectPropertyModifiedRequest(node.intNodeId, Alvao.API.AM.ObjectProperty.GetDefinition(property.kindCode).intKindId, Alvao.API.Common.Person.GetSystem().iPersonId, property.value);
                    Alvao.API.AM.CustomApps.OnObjectPropertyModified(req);
                }
            }
            catch (NullReferenceException ex)
            {
                Logger.Log.Error(ex, "Error processing device.");
            }
        }
        //Logger.Log.Info("Processed {0} devices", newOrUpdatedIntuneDevices?.Count());
    }

    public async Task&lt;IEnumerable&lt;LansweeperDevice&gt;&gt; GetNewOrUpdatedDevices(DateTime? lastSync, CancellationToken cancellationToken) {
        var devices = await LansweeperClientService.GetDevices(cancellationToken);
        var collisionChecker = new ImportCollisionChecker();
        foreach(var device in devices) 
        {
            tblNode node = GetExistingDeviceNode(device);
            if (node is null) 
            {
                collisionChecker.AddImportedInfo(0, device);
            }
            else 
            {
                device.NodeId = node.intNodeId;
                collisionChecker.AddImportedInfo(node.intNodeId, device, node.ImportDuplicateAlert);
            }                     
        }
        SetAndLogCollisions(collisionChecker);
        var toRemove = collisionChecker.GetCollidingRecords().SelectMany(c =&gt; c.Value.CollidingObjects);
        devices.ToList().RemoveAll(d =&gt; toRemove.Any(r =&gt; d.LansweeperDeviceId == r.LansweeperDeviceId));
        return lastSync is null ? devices : devices.Where(d =&gt; DateTime.Parse(d.LansweeperLastCheckIn) &gt; lastSync);
    }

    private void SetAndLogCollisions(ImportCollisionChecker collisionChecker) 
    {
        var collisions = collisionChecker.GetCollidingRecords();
        var systemPersonId = Alvao.API.Common.Person.GetSystem().iPersonId;
        foreach (var collision in collisions)
        {
            if (collisionChecker.ShouldLogCollision(collision.Key))
            {
                var log = collisionChecker.GetCollisionLog(collision.Key, systemPersonId);
                using var scope = AlvaoContext.GetConnectionScope();
                scope.BeginTransaction();
                LansweeperConnectorRepository.InsertIntoObjectLog(log);
                LansweeperConnectorRepository.SetImportConflict(collision.Key, true);
                scope.CommitTransaction();
            } 
        }

        var noMoreColliding = collisionChecker.GetNodesToUnsetCollisionFlag();
        foreach (var nodeId in noMoreColliding)
        {
            LansweeperConnectorRepository.SetImportConflict(nodeId, false);
        }
    }

    public tblNode GetExistingDeviceNode(LansweeperDevice device)
    {
        if (device.LansweeperDeviceId is null)
            throw new ArgumentNullException("Missing Lansweeper ID.");

        tblNode node = LansweeperConnectorRepository.GetDeviceNodeByAnotherProperty((int)KindCode.LansweeperDeviceId, device.LansweeperDeviceId.ToString(), false);

        if (node == null)
        {
            if (device.DeviceType == LansweeperDevice.Type.MobileDevice)
                node = LansweeperConnectorRepository.GetDeviceNodeByAnotherProperty((int)KindCode.IMEI, device.Imei, false);
            else
            {
                var isSnBlacklisted = LansweeperConnectorRepository.GetBlacklistedValuesForKindcode((int)KindCode.BIOS_SN).Any(blacklisted =&gt; device.SerialNo == blacklisted);
                if (!isSnBlacklisted)
                {
                    node = LansweeperConnectorRepository.GetDeviceNodeByAnotherProperty((int)KindCode.BIOS_SN, device.SerialNo, true);
                }

                if (node is null)
                {
                    node = LansweeperConnectorRepository.GetDeviceNodeByAnotherProperty((int)KindCode.HostName, device.Hostname, true, string.IsNullOrEmpty(device.SerialNo));
                }                                       
            }
        }
        return node;
    }

    private void CheckOrAddMissingPropertiesToTemplateClasses()
    {
        Dictionary&lt;tblClass.ClassCode, LansweeperDevice.Type&gt; map = new();
        foreach (var classId in LansweeperDevice.ClassMap.Keys)
            map[classId] = LansweeperDevice.ClassMap[classId];

        tblClass.ClassCode defaultComputerClassId = (tblClass.ClassCode)Alvao.API.Common.DbProperty.AMDefaultComputerClass;
        if (!map.ContainsKey(defaultComputerClassId))
            map.Add(defaultComputerClassId, LansweeperDevice.Type.Computer);

        foreach (var classId in map.Keys)
        {
            var deviceType = map[classId];
            if (!Alvao.API.AM.ObjectProperty.TemplateContains((int)classId, (tblKind.KindCode)KindCode.LansweeperDeviceId))
            {
                string kindCodes = string.Join(",", LansweeperDevice.GetPropertyMappingForType(deviceType).Select(x =&gt; ((int)x.Key).ToString()));
                LansweeperConnectorRepository.AddMissingPropsToAmTemplateAndUnify((int)classId, kindCodes);
            }
        }
    }

    private tblNode CreateNewDevice(LansweeperDevice managedDevice)
    {
        if (ImportedObjectsNodeFolderNodeId == 0)
            ImportedObjectsNodeFolderNodeId = LansweeperConnectorRepository.GetOrCreateImportedObjectsFolder();

        int classId = Alvao.API.AM.ObjectType.GetDeviceTypeId(managedDevice.Hostname, managedDevice.Model, 0);
        if (classId == 0)
            classId = managedDevice.DeviceType == LansweeperDevice.Type.MobileDevice ? (int)tblClass.ClassCode.MobilePhone : Alvao.API.Common.DbProperty.AMDefaultComputerClass;

        if (CheckedTemplatePropMap.TryAdd(classId, true))
        {
            if (!Alvao.API.AM.ObjectProperty.TemplateContains((int)classId, (tblKind.KindCode)KindCode.LansweeperDeviceId))
            {                
                LansweeperDevice.Type deviceType = IsComputer(classId) ? LansweeperDevice.Type.Computer : LansweeperDevice.Type.MobileDevice;
                string kindCodes = string.Join(",", LansweeperDevice.GetPropertyMappingForType(deviceType).Select(x =&gt; ((int)x.Key).ToString()));
                LansweeperConnectorRepository.AddMissingPropsToAmTemplateAndUnify((int)classId, kindCodes);
            }
        }

        tblNode node = new();
        if(IsComputer(classId))
            node = LansweeperConnectorRepository.CreateSimpleComputer(classId, ImportedObjectsNodeFolderNodeId, managedDevice.Hostname, Alvao.API.Common.Person.GetSystem().iPersonId);
        else
            node = LansweeperConnectorRepository.CreateObjectByClass(classId, ImportedObjectsNodeFolderNodeId, Alvao.API.Common.Person.GetSystem().iPersonId);
        return node;
    }

    private bool IsComputer(int classId)
    {
        using(var scope = AlvaoContext.GetConnectionScope())
        return scope.Connection.ExecuteScalar&lt;bool?&gt;("select bComputer from tblClass where intClassId=@classId", new { classId }, scope.Transaction) ?? false;
    }

    private List&lt;(tblKind.KindCode kindCode, object value)&gt; UpdateDeviceProperties(int nodeId, LansweeperDevice device)
    {
        List&lt;(tblKind.KindCode kindCode, object value)&gt; codeValue = new List&lt;(tblKind.KindCode kindCode, object value)&gt;();
        using (var scope = AlvaoContext.GetConnectionScope())
        {
            scope.BeginTransaction();
            foreach (var mapping in device.GetPropertyMapping())
            {        
                object value = typeof(LansweeperDevice).GetProperty(mapping.Value)?.GetValue(device);      

                if(value is null)
                    continue;

                Alvao.API.AM.ObjectProperty.Update(nodeId, (tblKind.KindCode)mapping.Key, value, false, (tblKind.KindCode)mapping.Key != tblKind.KindCode.LansweeperLastCheckIn);
                codeValue.Add(((tblKind.KindCode)mapping.Key, value));
            }
            scope.CommitTransaction();
        }

        return codeValue;
    }
}

public class LansweeperConnectorRepository 
{
    private List&lt;int&gt; ScannerPropMapCache = new();

    public DateTime? GetLansweeperLastCheckInDevicesDateTime()
    {
        using var scope = AlvaoContext.GetConnectionScope();
        return scope.Connection.ExecuteScalar&lt;DateTime?&gt;(@$"select top 1
            LansweeperLastCheckIn
        from NodeCust nc
        join tblNode n on n.intNodeId = nc.NodeId
        where n.IsHidden = 0
            and isdate(substring(LansweeperLastCheckIn,1,10))=1    --Lansweeper values: 2024-07-09T10:44:13.9769700Z
        order by LansweeperLastCheckIn desc", new { kind = KindCode.LansweeperLastCheckIn }, transaction: scope.Transaction);
    }

    public tblNode GetDeviceNodeByAnotherProperty(int keyPropKindCode, string keyPropValue, bool onlyInComputers, bool isBiosSNInSourceEmpty = false)
    {
        using var scope = AlvaoContext.GetConnectionScope();
        return scope.Connection.Query&lt;tblNode&gt;(@$"
            declare @columnName nvarchar(255)
            set @columnName = (select k.ColumnName from tblKind k where k.intKindCode = @kindCode)

            declare @valueType nvarchar(255)
            set @valueType = (select DATA_TYPE from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = 'NodeCust' and COLUMN_NAME = @columnName)
            if (@valueType = 'varchar' or @valueType = 'nvarchar')
                set @valueType = @valueType + '(' + cast((select CHARACTER_MAXIMUM_LENGTH from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = 'NodeCust' and COLUMN_NAME = @columnName) as nvarchar(255)) + ')'


            declare @sql nvarchar(max)
            set @sql = '
            select top 1
                n.intNodeId, 
                n.lintClassId,
                n.ImportDuplicateAlert
               -- case when isdate(substring(nc.IntuneLastCheckIn,1,10))=1 then nc.IntuneLastCheckIn else null end as IntuneLastCheckIn
            from tblNode n
                {(onlyInComputers ? "join tblClass c on c.intClassId=n.lintClassId and c.bComputer=1" : "")}
                join ClassKind ck on ck.ClassId=n.lintClassId
                join tblKind k on k.intKindId=ck.KindId and k.intKindCode=@kindCode
                join NodeCust nc on nc.NodeId = n.intNodeId
            where n.IsHidden=0
                and ltrim(rtrim(nc.' + @columnName + '))=@value 
                {(keyPropKindCode == (int)KindCode.HostName &amp;&amp; !isBiosSNInSourceEmpty ? $@"
                    and (nc.BiosSerialNumber is null or nc.BiosSerialNumber in {CreateBlacklistedForDynamicSql(GetCachedBlacklistedValuesForKindcode((int)KindCode.BIOS_SN))}) " : "")}
            order by n.intNodeId asc'

            declare @prmsDecl nvarchar(max)
            set @prmsDecl = N'@kindCode int, @value ' + @valueType

            exec sp_executesql @sql, @prmsDecl, @kindCode, @value",
            new { kindCode = keyPropKindCode, value = (keyPropValue != null) ? keyPropValue.Trim() : null },
            transaction: scope.Transaction).FirstOrDefault();
    }

    private string CreateBlacklistedForDynamicSql(IEnumerable&lt;string&gt; blacklisted)
    {
        StringBuilder sb = new();
        sb.Append('(').Append(string.Join(',', blacklisted.Select(it =&gt; "''" + it + "''"))).Append(')');
        return sb.ToString();
    }

    public IEnumerable&lt;string&gt; GetCachedBlacklistedValuesForKindcode(int kindCode)
    {
        return CacheUtil.GetOrCreateCachedItem("blacklistedValuesForKindCode_" + kindCode, () =&gt; GetBlacklistedValuesForKindcode(kindCode), minutesToCache: 30);
    }

    public IEnumerable&lt;string&gt; GetBlacklistedValuesForKindcode(int kindCode)
    {
        var wbemProp = ObjectWbemProcess.GetWbemEquivalentNameAndClass(kindCode);
        if (wbemProp is null)
        {
            return Enumerable.Empty&lt;string&gt;();
        }
        using (var scope = AlvaoContext.GetConnectionScope())
        {
            return scope.Connection.Query&lt;string&gt;(
                @$"select 
                    txtPropValue
                from tblWbemObjectProcess
                where txtPropName = '{wbemProp.Name}'
                    and txtCLASS = '{wbemProp.Class}'
                ", new { }, scope.Transaction);
        }
    }

    public int GetOrCreateImportedObjectsFolder()
    {
        //Logger.Log.Debug(nameof(GetOrCreateImportedObjectsFolder));
        using var scope = AlvaoContext.GetConnectionScope();
        return scope.Connection.ExecuteScalar&lt;int&gt;($@"declare @nodeId int
        select top 1
            @nodeId=n.intNodeId
        from tblNode n
        where n.lintClassId=@classId
            and n.IsActive = 1

        if @nodeId is null
        begin
            insert tblNode (lintIconId,intState,txtName,lintClassId)
            select i.intIconId,128,d.txtText,d.lintClassId
            from tblDict d
                join tblIcon i on i.uid=@iconUid
            where d.lintClassId=@classId

            select @nodeId=scope_identity()

            insert tblNodeParent values (@nodeId,@nodeId)
        end

        select @nodeId", new { classId = tblClass.ClassCode.ImportedObjects, iconUid = tblIcon.IconUid.Subnet }, transaction: scope.Transaction);
    }

    public int GetActualComputerCount()
    {
        //Logger.Log.Debug(nameof(GetActualComputerCount));
        using var scope = AlvaoContext.GetConnectionScope();
        return scope.Connection.ExecuteScalar&lt;int&gt;(@"select count(1) cnt 
        from tblNode n
        join tblClass c on c.intClassId=n.lintClassId
        where c.bComputer=1
        and n.IsActive = 1", transaction: scope.Transaction);
    }

    public tblNode CreateSimpleComputer(int classId, int parentNodeId, string hostname, int personId)
    {
        //Logger.Log.Debug("{0}, {1}, {2}", classId, parentNodeId, hostname);
        using var scope = AlvaoContext.GetConnectionScope();
        return scope.Connection.QueryFirst&lt;tblNode&gt;(@"declare @pcNodeId int, @setNodeId int
exec Internal.spCreateSimpleComputer @hostname, @parentNodeId, @personId, @classId, 1, @pcNodeId output, @setNodeId output
select @pcNodeId intNodeId, @classId lintClassId",
            new { hostname, parentNodeId, personId, classId }, transaction: scope.Transaction);
    }

    public tblNode CreateObjectByClass(int classId, int importedObjectsNodeFolderNodeId, int personId)
    {
        //Logger.Log.Debug("{0}, {1}, {2}", classId, importedObjectsNodeFolderNodeId, personId);
        using var scope = AlvaoContext.GetConnectionScope();
        return scope.Connection.QueryFirst&lt;tblNode&gt;(@"declare @id int
exec @id=spCreateNodeFromTemplate @classId, '', @parentId, @personId
select @id intNodeId, @classId lintClassId", new { classId, parentId = importedObjectsNodeFolderNodeId, personId }, transaction: scope.Transaction);
    }

    public void AddMissingPropsToAmTemplateAndUnify(int classId, string kindCodes)
    {
        //Logger.Log.Debug("Unifying properties classId: {0}, kindCodes: {1}", classId, kindCodes);
        using var scope = AlvaoContext.GetConnectionScope();
            scope.Connection.Execute($@"
if object_id('tempdb..#kinds') is not null
	drop table #kinds

create table #kinds (id int primary key)
insert #kinds (id)
select id from dbo.ftCommaListToTableIds(@kindCodes)

--insert new to template
insert into ClassKind (KindId, ClassId)
select
	k.intKindId,
	@classId classId
from #kinds ids
	join tblKind k on k.intKindCode = ids.id
	left join ClassKind ck on ck.ClassId = @classId and k.intKindId = ck.KindId
where ck.KindId is null
", new { classId, kindCodes }, transaction: scope.Transaction);
        }

    public void ClearScannerPropertyLockoutTable()
    {
        //Logger.Log.Debug("");
        using var scope = AlvaoContext.GetConnectionScope();
        scope.Connection.Execute($@"
delete spl
from ScannerPropertyLockout spl
left join ClassKind ck on ck.KindId=spl.LockingKindId and ck.ClassId=spl.ClassId
where spl.ScannerId={(int)ScannerId.Lansweeper}
and ck.KindId is null");
    }

    public void CreateConnectorScannerIfNotExists()
    {
        // pre Alvao 25.2
        using var scope = AlvaoContext.GetConnectionScope();
        scope.Connection.Execute($@"
            IF NOT EXISTS (
                SELECT 1 FROM Scanner WHERE id = {(int)ScannerId.Lansweeper}
            )
            BEGIN
                INSERT INTO Scanner (id, Name)
                VALUES ({(int)ScannerId.Lansweeper}, N'Lansweeper');
            END
            ");
    }

    public void RecalculateScannerPropertyLockoutTable(LansweeperDevice device, int classId)
    {
        if (classId == 0)
            return;

        if (ScannerPropMapCache.Contains(classId))
            return;

        KindCode lockingKindCode = KindCode.LansweeperDeviceId;
        string lockedKindCodes = string.Join(",", LansweeperDevice.GetPropertyMappingForType(LansweeperDevice.Type.Computer, true).Select(kv =&gt; kv.Key).Where(k =&gt; k != lockingKindCode).Select(k =&gt; ((int)k).ToString()));
        RecalculateScannerPropertyLockoutByClass(classId, (int)lockingKindCode, lockedKindCodes);

        ScannerPropMapCache.Add(classId);
    }

    public void RecalculateScannerPropertyLockoutByClass(int classId, int lockingKindCode, string lockedKindCodes)
    {
        //TODO: Call AssetLib.RecalculateScannerPropertyLockoutByClass from API

        //Logger.Log.Debug("{ClassId}, {LockindKindCode}, {LockedKindCodes}", classId, lockingKindCode, lockedKindCodes);
        using var scope = AlvaoContext.GetConnectionScope();
        scope.Connection.Execute($@"
if object_id('tempdb..#locked') is not null
	drop table #locked

create table #locked (id int primary key, notInTemplate bit default 0)
insert #locked (id)
select id from dbo.ftCommaListToTableIds(@lockedKindCodes)

merge into ScannerPropertyLockout tar using (
	select
		{(int)ScannerId.Lansweeper},
		@classId,
		locking.intKindId,
		locked.intKindId
	from tblKind locked
		join #locked l on l.id=locked.intKindCode
		join tblKind locking on locking.intKindCode=@lockingKindCode
) src (ScannerId,ClassId,LockingKindId,LockedKindId) on 
	tar.ScannerId=src.ScannerId 
	and tar.ClassId=src.ClassId 
	and tar.LockingKindId=src.LockingKindId
	and tar.LockedKindId=src.LockedKindId
when not matched by target then insert (ScannerId,ClassId,LockingKindId,LockedKindId) values (ScannerId,ClassId,LockingKindId,LockedKindId)
when not matched by source and tar.ClassId=@classId and tar.ScannerId={(int)ScannerId.Lansweeper} then delete;", new { classId, lockingKindCode, lockedKindCodes }, transaction: scope.Transaction);
    }

    public void InsertIntoObjectLog(tblLog log)
    {
        Logger.Log.Debug(nameof(InsertIntoObjectLog));
        using var scope = AlvaoContext.GetConnectionScope();
        scope.Connection.Execute(@"
            insert into tblLog (lintNodeId, liLogPersonId, dteLog, txtLog)
            values (@lintNodeId, @liLogPersonId, @dteLog, @txtLog)   
        ", new {log.lintNodeId, log.liLogPersonId, log.dteLog, log.txtLog}, transaction: scope.Transaction);  
    }

    public void SetImportConflict(int nodeId, bool hasConflict)
    {
        using var scope = AlvaoContext.GetConnectionScope();
        scope.Connection.Execute(@"
            update tblNode
            set ImportDuplicateAlert = @hasConflict
            where intNodeId = @nodeId
        ", new { nodeId, hasConflict }, transaction: scope.Transaction);
    }
}

public class LansweeperClientService 
{
    private string Token { get; set; }
    private static readonly HttpClient client = new HttpClient(); 

    public async Task&lt;IEnumerable&lt;LSSite&gt;&gt; GetAvailableSites(CancellationToken cancellationToken)
    {
        //Logger.Log.Debug($"Loading available sites");
        var requestBody = new {
            query = "{ authorizedSites { sites { id name } } }"
        };

        var request = await HttpClientHelpers.PrepareRequest(HttpMethod.Post, "", requestBody);
        var response = await client.SendAsync(request);
        var responseContent = await response.Content.ReadAsStringAsync();
        if (response.IsSuccessStatusCode)
        {
            var responseObject = JsonConvert.DeserializeObject&lt;LSResponse&lt;AuthorizedSitesData&gt;&gt;(responseContent);
            var sites = responseObject.Data.AuthorizedSites.Sites;
            //Logger.Log.Debug($"Sites loaded: {string.Join(", ", sites.Select(s =&gt; s.Name))}");
            return sites;
        }
        else
        {
            throw new Exception($"Unsuccessful response from {PeriodicAction.ConnectorName}: {response.StatusCode} {responseContent}");
        }
    }

    public async Task&lt;IEnumerable&lt;LansweeperDevice&gt;&gt; GetDevices(CancellationToken cancellationToken)
    {
        var sites = await GetAvailableSites(cancellationToken);
        
        IEnumerable&lt;LansweeperDevice&gt; devices = new List&lt;LansweeperDevice&gt;();
        foreach(var site in sites)
        {
            var siteDevices = await LoadDevicesFromSite(site.Id, null, cancellationToken);
            devices = devices.Union(siteDevices);
        }
        return devices;
    }

    public async Task&lt;List&lt;LansweeperDevice&gt;&gt; LoadDevicesFromSite(string siteId, DateTime? lastCheckInDateTime, CancellationToken cancellationToken)
    {

        int limit = 50;
        string cursor = "";
        string page = "FIRST";
        
        List&lt;LansweeperDevice&gt; devices = new ();

        lastCheckInDateTime ??= new DateTime(2000, 1, 1);

        var lastCheckInStr = lastCheckInDateTime.Value.ToUniversalTime().ToString("o");

        while(cursor != null)
        {
            var requestBody = 
                new {
                    query = @"query assetResources($siteId: ID!, $pagination: AssetsPaginationInputValidated){ 
                        site(id: $siteId)
                        {
                            assetResources(
                                assetPagination: $pagination,
                                fields: [
                                    ""assetBasicInfo.name"",
                                    ""assetBasicInfo.fqdn"",
                                    ""assetBasicInfo.type"",
                                    ""bioses.serialNumber"",
                                    ""assetCustom.model"",
                                    ""assetCustom.manufacturer"",
                                    ""assetCustom.serialNumber"",
                                    ""assetBasicInfo.lastUpdated"",
                                    ""assetBasicInfo.lastSeen"",
                                    ""memoryModules.size"",
                                    ""networkAdapters.macAddress"",
                                    ""assetBasicInfo.userName"",
                                    ""hardDisks.size"",
                                    ""url"",
                                    ""operatingSystem.caption"",
                                    ""mobileDeviceManagement.imei""
                                ],
                                filters: {
                                    conjunction: OR
                                    conditions: [
                                        { operator: GREATER_THAN, path: ""assetBasicInfo.lastUpdated"", value: """ + lastCheckInStr + @""" },
                                        { operator: GREATER_THAN, path: ""assetBasicInfo.lastSeen"", value: """ + lastCheckInStr + @""" },
                                    ]
                                },
                            ){
                                total
                                pagination {
                                    limit
                                    current
                                    next
                                    page 
                                }
                                items
                            }
                        }
                    }",
                    variables = new {
                        siteId = "1610fb57-ea49-4a54-9ef9-e61e2c6253a5",
                        pagination = new {
                            limit,
                            page,
                            cursor
                        }
                    }
                };
            var request = await HttpClientHelpers.PrepareRequest(HttpMethod.Post, "", requestBody);
            var response = await client.SendAsync(request);
            var responseContent = await response.Content.ReadAsStringAsync();
            if (response.IsSuccessStatusCode)
            {
                var responseObject = JsonConvert.DeserializeObject&lt;LSResponse&lt;SiteResponse&gt;&gt;(responseContent);
                page = "NEXT";
                cursor = responseObject.Data.Site.AssetResources.Pagination.Next;

                var loadedDevices = responseObject.Data.Site.AssetResources.Items;
                devices.AddRange(loadedDevices.Select(d =&gt; {var ld = d.ToLansweeperDevice(); ld.LansweeperSiteId = siteId; return ld;}).ToList());
                
            }
            else
            {
                throw new Exception($"Unsuccessful response from {PeriodicAction.ConnectorName}: {response.StatusCode} {responseContent}");
            }              
        }

        Logger.Log.Info($"Loaded {devices.Count()} devices from Lansweeper changed from {lastCheckInStr}.");
        return devices;
    }

}

// MODELS
public record LSSite (string Id, string Name);

public record AuthorizedSites(IEnumerable&lt;LSSite&gt; Sites);

public record AuthorizedSitesData(AuthorizedSites AuthorizedSites);

public record LSResponse&lt;T&gt;(T Data);

public record SiteResponse(Site Site);
public record Site(AssetResources AssetResources);
public record AssetResources(int? Total, Pagination Pagination, IEnumerable&lt;LSDevice&gt; Items);
public class LSDevice : LansweeperApiModel
{
    public string SiteId { get; set; }
    public string Key { get; set; }
    public AssetBasicInfo AssetBasicInfo { get; set; }
    public AssetCustom AssetCustom { get; set; }
    public string Url { get; set; }
    public IEnumerable&lt;Bios&gt; Bioses { get; set; }
    public IEnumerable&lt;HardDisk&gt; HardDisks { get; set; }
    public IEnumerable&lt;MemoryModule&gt; MemoryModules { get; set; }
    public OperatingSystem OperatingSystem { get; set; }
    public IEnumerable&lt;MobileDeviceManagement&gt; MobileDeviceManagement { get; set; }
    public IEnumerable&lt;NetworkAdapter&gt; NetworkAdapters { get; set; }
    public override LansweeperDevice ToLansweeperDevice()
    {
        return new LansweeperDevice()
        {
            LansweeperDeviceId = Key,
            LansweeperLastCheckIn = AssetBasicInfo?.LastSeen ?? AssetBasicInfo?.LastUpdated, // when last seen is not available, use last updated instead (manually created objects without discovery)
            Hostname = AssetBasicInfo?.Name,
            OperatingSystem = OperatingSystem?.Caption,
            UserUpn = AssetBasicInfo?.UserName,
            Model = AssetCustom?.Model,
            Manufacturer = AssetCustom?.Manufacturer,
            Imei = MobileDeviceManagement?.FirstOrDefault()?.Imei,
            SerialNo = Bioses?.FirstOrDefault()?.SerialNumber ?? AssetCustom?.SerialNumber,
            TotalStorageSpaceInGB = (double?)HardDisks?.Sum(d =&gt; d.Size/1024/1024/1024),
            RamCapacityInGB = (double?)MemoryModules?.Sum(m =&gt; m.Size/1024/1024/1024),
            MacAddresses = NetworkAdapters?.Any() ?? false ? GetMacAddresses(NetworkAdapters.Select(a =&gt; a.MacAddress)) : null,
            DeviceType = GetDeviceType(),
            LansweeperSiteId = SiteId
        };
    }

    private LansweeperDevice.Type GetDeviceType()
    {
        string[] MobilePhoneTypes = ["Android", "iOS", "Tablet", "Cell phone", "iPhone", "iPad", "Mobile"];

        if(MobilePhoneTypes.Contains(AssetBasicInfo.Type, StringComparer.OrdinalIgnoreCase))
            return LansweeperDevice.Type.MobileDevice;

        return LansweeperDevice.Type.Computer;
    }
    
}
public record AssetBasicInfo(string LastUpdated, string LastSeen, string Name, string Type, string UserName);
public record AssetCustom(string Manufacturer, string Model, string SerialNumber);
public record Bios(string SerialNumber);
public record MemoryModule(long Size);
public record HardDisk(long Size);
public record OperatingSystem(string Caption, string Name);
public record MobileDeviceManagement(string Imei);
public record NetworkAdapter(string MacAddress);

public record Pagination(int Limit, string Current, string Next, string Page);

public class LansweeperDevice
{
    public enum Type
    {
        Computer,
        MobileDevice
    }
    public string LansweeperDeviceId { get; set; }
    public string LansweeperLastCheckIn { get; set; }
    public string LansweeperSiteId { get; set; }
    public string Hostname { get; set; }
    public string OperatingSystem { get; set; } 
    public string UserUpn { get; set; }
    public string Model { get; set; }
    public string Manufacturer { get; set; } 
    public string Imei { get; set; }
    public string SerialNo { get; set; }
    public double? TotalStorageSpaceInGB { get; set; }
    public double? RamCapacityInGB { get; set; }
    public string MacAddresses { get; set; }
    public Type DeviceType { get; set; }
    public int? NodeId { get; set; }
    
    public static Dictionary&lt;tblClass.ClassCode, Type&gt; ClassMap = new()
    {
        { tblClass.ClassCode.MobilePhone, Type.MobileDevice },
        { tblClass.ClassCode.Tablet, Type.MobileDevice },
        { tblClass.ClassCode.Computer, Type.Computer },
        { tblClass.ClassCode.ComputerNotebook, Type.Computer }
    };

    public static int? GetFirstClassIdForDeviceType(Type type)
    {
        foreach(var classId in ClassMap.Keys)
        {
            if (ClassMap[classId] == type)
                return (int)classId;
        }
        return null;
    }

    public Dictionary&lt;KindCode, string&gt; GetPropertyMapping()
    {
        return GetPropertyMappingForType(DeviceType);
    }

    public static Dictionary&lt;KindCode, string&gt; GetPropertyMappingForType(Type type, bool returnAll = false)
    {
        Dictionary&lt;KindCode, string&gt; mapping = new()
        {
            {KindCode.UserUpn, nameof(UserUpn)},
            {KindCode.LansweeperDeviceId, nameof(LansweeperDeviceId)},
            {KindCode.LansweeperLastCheckIn, nameof(LansweeperLastCheckIn)},
            {KindCode.LansweeperSiteId, nameof(LansweeperSiteId)},
            {KindCode.HostName, nameof(Hostname)},
            {KindCode.OperatingSystem, nameof(OperatingSystem)},
            {KindCode.Model, nameof(Model)},
            {KindCode.Manufacturer, nameof(Manufacturer)},
            {KindCode.SerialNo, nameof(SerialNo)},
            {KindCode.MACAddresses, nameof(MacAddresses)},
            {KindCode.TotalStorageSpaceInGB, nameof(TotalStorageSpaceInGB)},
        };

        if(returnAll || type == Type.Computer)
        {
            mapping.Add(KindCode.BIOS_SN, nameof(SerialNo));
            mapping.Add(KindCode.RAMCapacityInGB, nameof(RamCapacityInGB)); // not available on mobile devices in Lansweeper
        }

        if(returnAll || type == Type.MobileDevice)
            mapping.Add(KindCode.IMEI, nameof(Imei));

        return mapping;
    }

    public override string ToString()
    {
            return $"LansweeperDevice [\n" +
           $"  LansweeperDeviceId={LansweeperDeviceId},\n" +
           $"  LansweeperLastCheckIn={LansweeperLastCheckIn},\n" +
           $"  Hostname={Hostname},\n" +
           $"  OperatingSystem={OperatingSystem},\n" +
           $"  UserPrincipalName={UserUpn},\n" +
           $"  Model={Model},\n" +
           $"  Manufacturer={Manufacturer},\n" +
           $"  Imei={Imei},\n" +
           $"  SerialNo={SerialNo},\n" +
           $"  TotalStorageSpaceInGB={TotalStorageSpaceInGB},\n" +
           $"  RamCapacityInGB={RamCapacityInGB},\n" +
           $"  DeviceType={DeviceType},\n" +
           $"  MacAddresses={MacAddresses}\n" +
           $"  SiteId={LansweeperSiteId}\n" +
           $"]";
    }
}

public abstract class LansweeperApiModel 
{
    public abstract LansweeperDevice ToLansweeperDevice();

    protected string GetMacAddresses (IEnumerable&lt;string&gt; MacAddresses)
    { 
        return string.Join("; ", MacAddresses.Where(x =&gt; !string.IsNullOrEmpty(x)).Select(x =&gt; FormatMAC(x)));
    }
    
    private static string FormatMAC(string mac)
    {
        if( mac == null)
            return string.Empty;

        if (mac.Length != 12)
            return mac;

        
        return string.Format("{0}:{1}:{2}:{3}:{4}:{5}",
            mac.Substring(0, 2),
            mac.Substring(2, 2),
            mac.Substring(4, 2),
            mac.Substring(6, 2),
            mac.Substring(8, 2),
            mac.Substring(10, 2)).ToUpper();
    }

}

public class LansweeperTokenResponse 
{
    public string Token { get; set; }
}

public class LansweeperClientCredentialsResponse {
    public string access_token { get; set; }
}

public class LansweeperPaginatedResponse&lt;T&gt;
{
    public int TotalCount { get; set; }
    public IEnumerable&lt;T&gt; Results { get; set; }
}

internal static class ObjectWbemProcess
{
    internal class WbemProp
    {
        public string Class { get; set; }
        public string Name { get; set; }

        public WbemProp(string txtclass, string txtname)
        {
            Class = txtclass;
            Name = txtname;
        }
    }
    private static Dictionary&lt;int, WbemProp&gt; KindToWbemMap = new()
        {
            {87, new("BIOS", "SerialNumber") }
        };
    internal static WbemProp GetWbemEquivalentNameAndClass(int kindCode)
    {
        WbemProp wbemProp = null;
        KindToWbemMap.TryGetValue(kindCode, out wbemProp);
        return wbemProp;
    }
}

/*Groups incoming devices based on Alvao object they matched to. Devices that do not exist in Alvao yet are matched to object ID 0.
     * Collisions are detected when two or more incoming devices match to the same Alvao object (ID &gt; 0) or when two or more incoming devices do not match to any Alvao object (ID = 0) but match each other based on BIOS SN or Hostname.
     */
    public class ImportCollisionChecker
    {
        private Dictionary&lt;int, CollisionInfo&gt; MatchMap { get; set; } = new Dictionary&lt;int, CollisionInfo&gt;();

        public void AddImportedInfo(int nodeId, LansweeperDevice lansweeperDevice, bool? errorSetBeforeImport = null) 
        {
            var isFirstOccurence = !MatchMap.ContainsKey(nodeId);
            if (isFirstOccurence)
            {
                MatchMap.Add(nodeId, new CollisionInfo());
            }
            var collisionInfo = MatchMap[nodeId];
            if (errorSetBeforeImport is not null &amp;&amp; isFirstOccurence) collisionInfo.PresentBeforeImport = errorSetBeforeImport.Value;
            collisionInfo.CollidingObjects.Add(lansweeperDevice);            
        }

        public bool ShouldLogCollision(int nodeId)
        {
            return nodeId &gt; 0 &amp;&amp; HasCollision(nodeId) &amp;&amp; !HadCollisionBeforeImportStarted(nodeId);
        }

        public bool HasCollision(int nodeId)
        {
            MatchMap.TryGetValue(nodeId, out var collisionInfo);
            if (collisionInfo is null) 
            { 
                return false; 
            }
            return collisionInfo.CollidingObjects.Count &gt;= 2;
        }

        public bool HadCollisionBeforeImportStarted(int nodeId)
        {
            MatchMap.TryGetValue(nodeId, out var collisionInfo);
            return collisionInfo?.PresentBeforeImport ?? false;
        }

        public tblLog GetCollisionLog(int nodeId, int personId)
        {
            return new tblLog()
            {
                lintNodeId = nodeId,
                liLogPersonId = personId,
                dteLog = DateTime.UtcNow,
                txtLog = GetCollisionLogMessage(nodeId)
            };
        }

        public Dictionary&lt;int, CollisionInfo&gt; GetCollidingRecords()
        {
            var collisionsOnExistingObjects = MatchMap.Where(it =&gt; it.Key &gt; 0 &amp;&amp; it.Value.CollidingObjects.Count &gt;= 2).ToDictionary(); // two incoming devices matched on one existing AM object (does not matter by which parameter)
            
            if (!MatchMap.ContainsKey(0))
            {
                return collisionsOnExistingObjects;
            }
            
            var collisionsOnObjectsToCreateByBiosSn = MatchMap[0].CollidingObjects.GroupBy(o =&gt; o.SerialNo).Where(gr =&gt; gr.Count() &gt;= 2).SelectMany(gr =&gt; gr); // two incoming devices do not have matching AM object yet, but they match each other based on BIOS SN or Hostname
            var collisionsOnObjectsToCreateByHostname = MatchMap[0].CollidingObjects.GroupBy(o =&gt; o.Hostname).Where(gr =&gt; gr.Count() &gt;= 2).SelectMany(gr =&gt; gr);

            var collisionsOnObjectsToCreate = collisionsOnObjectsToCreateByBiosSn.Union(collisionsOnObjectsToCreateByHostname).DistinctBy(d =&gt; d.LansweeperDeviceId).ToList();
            return collisionsOnExistingObjects.Concat(new Dictionary&lt;int, CollisionInfo&gt;()
            {
                { 0, new CollisionInfo() { PresentBeforeImport = false, CollidingObjects = collisionsOnObjectsToCreate } }
            }).ToDictionary();
        }

        public IEnumerable&lt;int&gt; GetNodesToUnsetCollisionFlag()
        {
            return MatchMap.Where(it =&gt; it.Key &gt; 0 &amp;&amp; it.Value.CollidingObjects.Count == 1 &amp;&amp; it.Value.PresentBeforeImport).Select(it =&gt; it.Key);
        }

        private string GetCollisionLogMessage(int nodeId)
        {
            var sb = new StringBuilder();
            sb.Append("Duplicate devices were found during import. Lansweeper device IDs: ");

            var ids = MatchMap[nodeId].CollidingObjects.Select(o =&gt; o.LansweeperDeviceId).ToList();
            var threeDots = "";
            if (ids.Count &gt; 3) threeDots = "...";

            var idsPart = string.Join(", ", ids.Take(3));
            sb.Append($"{idsPart}{threeDots}");
            return sb.ToString();
        }

        public class CollisionInfo
        {
            public bool PresentBeforeImport { get; set; } = false;
            public List&lt;LansweeperDevice&gt; CollidingObjects { get; set; } = [];
        }
    }</Code>
          <IsLibCode>false</IsLibCode>
          <Codesign>kqBOdODQ0NjSZxK3kfPrpTKEj96uGU7E5rV95nBF5az5QgQGNP2LUYQozJLZQf43bDmAK7eZhab8TluTnKaUp4E68w6P3ZglaMS878txwlH6H9Mfmarf0w6zx+bgGWEa32pmY3dFRvCN+8y38CwPIrudatXVnyljQQmktnbQMmYmly0jsZYTS0eXsdKr/De3db6uZQRsFfqQMl6DZ5kNtBNmnwwNEc9LRH6b2b9GZ499jX53rp/NxajFJUxRW/iqFofzTrTYV0WDZnuIX+wWkjB9bD3IOe1bXx/un8UuU8EGNwZki7w1CbPcKE19ps1mT6UfThOBPwGCZngjlzmkcg==</Codesign>
        </Script>
        <Script id="2047">
          <Name>Logger</Name>
          <Code>using System.IO;
using System;
using NLog;
using Alvao.API.Internal;

public static class Logger 
{
    public static ILogger Log = TenantDiagnosticsLog.Get();
}</Code>
          <IsLibCode>true</IsLibCode>
          <Codesign>sqUrA9TtUJOL3D7RxdMmLlTFaDMtcvgGwDoR4LhBpiCEKrb2zGhBa6eXV8mIlynGBPHZNJ+jmuQzrhKSs33lOTUF7zCW218NsblfdsE6NrwktVYltwV80YwWVCWISBLQ8ROcBnnUOHT4kxRdcqrLgKaQatiWZbFWAg66D6jLLlwI+UuDWr8EEFJse5XbmYsDJNudlHxChiFYWGFT7e0Yyt+PxI4lGOiZfOG9fXfMHFfGCJyrTWtxyFoj/L/UQvZ0S8LfIfow3kSfgzLJvjB/uVgs2z5zhCT3bmQZaf+FFcDkoLhcohEwuXf+zp2ixw5G5/rl9CNUdeA+mDYqS+779w==</Codesign>
        </Script>
        <Script id="2048">
          <Name>OpenInLansweeperObjectCommand</Name>
          <Code>using System;
using System.Data;
using Alvao.Global;
using Alvao.API.Common;
using Alvao.API.Common.Model.CustomApps;
using Alvao.Apps.API;
using Alvao.Context;
using Dapper;
using Alvao.API.AM.Model;
using System.Net.Http;
using Newtonsoft.Json.Linq;


public class OpenInLansweeperObjectCommand : IEntityCommand 
{
    public string Id {get; set;}
    public Entity Entity {get; set;}
    private static readonly HttpClient client = new HttpClient(); 

    public OpenInLansweeperObjectCommand()
    {
        Id = "OpenInLansweeperObjectCommand";
        Entity = Entity.Object;
    }

    public EntityCommandShowResult Show(int entityId, int personId)
    {   
        int position = 2; 
        string icon = "open_20_regular"; 
        string name = "Open in Lansweeper";
        bool show = GetLansweeperDevice(entityId) != null; 
        return new EntityCommandShowResult (show, name, icon, position);
    }

    public CommandResult Run(int entityId, int personId)
    {
        MessageType messageType = MessageType.None;
        string messageText = null; 
        string navigateToUrl = $"https://app.lansweeper.com/{GetLansweeperDeviceUrl(entityId)}";
        return new CommandResult(messageType, messageText, navigateToUrl);
    }

    private class LansweeperDevice 
    {
        public string Id { get; set; }
        public string SiteId { get; set; }
    }
    
    private string GetLansweeperDeviceUrl(int objectId)
    {
        using(var scope = AlvaoContext.GetConnectionScope())
        {
            var device = GetLansweeperDevice(objectId);

            if(device is null)
                return null;
            
            return $"{GetSiteName(device.SiteId)}/asset/{device.Id}/summary";
        }
    }

    private LansweeperDevice GetLansweeperDevice(int objectId)
    {
        using var scope = AlvaoContext.GetConnectionScope();
        return scope.Connection.QueryFirstOrDefault&lt;LansweeperDevice&gt;($@"
                select nc.LansweeperDeviceId Id, nc.LansweeperSiteId SiteId
                from tblNode n                
                join NodeCust nc on n.intNodeId=nc.NodeId
                where n.intNodeId=@objectId and nc.LansweeperDeviceId is not null", new { objectId }, scope.Transaction);
    }

    private string GetSiteName(string siteId){
        var requestBody = new {
            query = 
            @"query GetSiteInfo($siteId: ID!) {
                site(id: $siteId) {
                    name
                }
            }",
            variables = new {
                siteId
            }
        };

        var request = HttpClientHelpers.PrepareRequest(HttpMethod.Post, "", requestBody).Result;
        var response = client.Send(request);
        var responseContent = response.Content.ReadAsStringAsync().Result;
        if (response.IsSuccessStatusCode)
        { 
            JObject obj = JObject.Parse(responseContent);
            var result = (string)obj?["data"]?["site"]?["name"] ?? throw new Exception($"Could not load site name for {siteId}");
            return result;
        }
        else
        {
            throw new Exception($"Unsuccessful response from Lansweeper: {response.StatusCode} {responseContent}");
        }
    }
}</Code>
          <IsLibCode>false</IsLibCode>
          <Codesign>eazgTWsOPlnc601YSVH0/IIjuBsInc2scd0E0if7GroA2blpVuZR3TxXRd1T+Sr1UtzBfIApMzX1sz4hOx6k667KER+aJ0F2duIOWB4zupzP5Xrueur3VPJmDwPAVj2T7L0SzKwWeGtiWi1QPsYbm+Mvj7tIhuvWNtl1ajlQnHu+A7Yc7VeRYqolXUB+uyrLTKzTCHAZpFDdvuhPT+QTr1FW/CT5vr723ce/P8GPcA9KUYkW/H3VD3V/6XiyTBEDO4ba4qC68N7SBPMc1GElHOLdRxmy7uYYXFcg/Sw09xORkOQnjKllVSOo+NulzssCfybjdiEX6DKxkn9F8ozr9w==</Codesign>
        </Script>
        <Script id="2049">
          <Name>HttpClientHelpers</Name>
          <Code>using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using System.Net.Http;
using System.Text;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using System;
using Alvao.API.Common;

public class HttpClientHelpers
{
    public static async Task&lt;HttpRequestMessage&gt; PrepareRequest(HttpMethod method, string url, object content = null)
    {
        var completeUrl = $"{DbProperty.LansweeperUrl}/{url}";
        //Logger.Log.Debug($"Preparing request {method} {completeUrl}");
        var httpRequest = new HttpRequestMessage(method, completeUrl);
        
        var token = await Task.FromResult(DbProperty.LansweeperToken);
        httpRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Token", token);
        httpRequest.Headers.Add("x-ls-integration-id", "7758ef3b-0edc-428c-bf8b-89129688c1e3");
        httpRequest.Headers.Add("x-ls-integration-version", "1.0");

        if(content != null){
            DefaultContractResolver contractResolver = new DefaultContractResolver
            {
                NamingStrategy = new CamelCaseNamingStrategy()
            };
            var settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ContractResolver = contractResolver, };

            var strContent = JsonConvert.SerializeObject(content, settings);

            httpRequest.Content = new StringContent(strContent, Encoding.UTF8, "application/json");
        }
        return httpRequest;
    }
}</Code>
          <IsLibCode>true</IsLibCode>
          <Codesign>uQaW58Nhe5Ot7nq9eGlWDrDi6WFbA99+OcKrKfxxAJE0tdmhy0/SX7o8ruDNdnHUQcpaEZTPuuB02rcX/do5ooq5ztbChBUS1fdmx4TaKnOP4viVVyb/ja/XfQZAmwHBwzwsN3J3kwa+XGwTu3NkLi7HuqjMeLZ4QohBi/OfUQ+6FpPAb3Qd7x0vsyI0lqigvxkacWo3GbvHXPUlbnh5XteqwdSeGg0ZUN9oADeg3Kk98NOLzLQx8Glwlmk4b8TITpFJPG3FNot+oOBWppY0DYrHeqJX4QD2gHL6Fp0/50/nk3VSkamGZUjZtaIJRF9FAaYdrtVmIveuZ+J/Sbi5IA==</Codesign>
        </Script>
      </Scripts>
    </Application>
  </Applications>
</AlvaoApplication>