<?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="2155">
      <Name>Azure DevOps Connector</Name>
      <Description>This application connects ALVAO Service Desk and Azure DevOps. After the request state is changed to specific state, a work item is automatically created in Azure DevOps. For opening this work item, there is a custom command available on the ticket.</Description>
      <UniqueId>7e52e7a0-9c83-4bd0-8360-0ac382d9d93e</UniqueId>
      <Version>4</Version>
      <AdvancedSettings />
      <Scripts>
        <Script id="4112">
          <Name>Settings</Name>
          <Code>using System.Collections.Generic;

public class Settings
{
    // API URL and credential of THIS vendor integration.
    public static string ConnectorType = "AzureDevOps";
 
    // Text configuration
    // Subject of error (in ticket log) when fails operation "Create ticket"
    public static string CreateTicketErrorSubject = "Failed to create ticket in external system";
    // Subject of error (in ticket log) when fails operation "Update ticket"
    public static string UpdateTicketErrorSubject = "Failed to update ticket in external system";
    // Subject of error (in ticket log) when fails operation "Create act"
    public static string CreateActErrorSubject = "Failed to create comment in external system";

    public const string IssueCreatedMessage = "Work item created in Azure DevOps.";
 
    // Custom Columns configuration
    public const string AppSettingsPersonColumn = "ExtAppSettings";
    public const string ExternalTicketIdTicketColumn = "ExtAppTicketId";
    public const string StatusMapName = "StatusMap";
    public const string AttributeMapName = "AttributeMap";
    public const string SyncCommunicationName = "SyncCommunication";
    public const string ShowCommandName = "ShowOpenExternalTicketCommand";
    public const string TicketReporterName = "TicketReporter";
    public const string UrlSettingsName = "URL";

    public const string OriginalMessageSeparator = @"&lt;hr id=""origmsg""&gt;";
}</Code>
          <IsLibCode>true</IsLibCode>
          <Codesign>FJtv1iVpocOMUfsNAWaZmlVHV5+Sa1dIhnrkb3RjzS1E5G6KEv5TnffEcU7J0fLSeKR+JVK4PgwArMvGM1kzhrjBHDunYB+5b+Aq8BuLAWTCv6wSk/Nav7lUBJaRzIu3K8KSYCPbHkTY14CWYYcfVA8mOxyIz7XFuVT87LHHvA7BPEefcaydQvbFi5Y21ABt+Oa65LABsJuUNDloGBKBkFs2GUmNZikg8NmhT46QjYpvw0LqJlPKiRmqA9idq0IgwEJhEp1Tb6fsA0HuyrukzwIqEsUQkyPFtRCTUdOJnXLObvebrQO2cMquhyC0/u66ONHBk6LqWU7xCp/1EFONDg==</Codesign>
        </Script>
        <Script id="4113">
          <Name>Helpers</Name>
          <Code>using Alvao.API.Common.Model;
using Alvao.API.Common.Model.Database;
using Alvao.API.SD;
using Alvao.Context;
using System;
using System.Collections.Generic;
using System.Data;
using Microsoft.Data.SqlClient;
using Dapper;
using System.Linq;
using Newtonsoft.Json.Linq;
 
public class Helpers
{
    public static string GetExternalTicketId(int ticketId, string externalTicketIdTicketColumn)
    {
        return Alvao.API.Common.CustomColumn.GetValue("tHdTicketCust", externalTicketIdTicketColumn, ticketId, new System.Globalization.CultureInfo(1033), string.Empty);
    }
 
    public static void SaveExternalTicketId(int ticketId, object externalTicketId, string externalTicketIdTicketColumn)
    {
        Alvao.API.Common.CustomColumn.UpdateValues(
            new Alvao.API.Common.Model.CustomColumnsModel() 
            {
                CustomTableName = "tHdTicketCust", 
                EntityId = ticketId, 
                ColumnValues = new List&lt;ColumnValue&gt;() 
                { 
                    new ColumnValue() 
                    { 
                        ColumnName = externalTicketIdTicketColumn, 
                        Value = externalTicketId 
                    } 
                } 
            }
        );
    }
    
    public static Service GetService(int serviceId, string extAppSettingsPersonColumn)
    {
        var defaultService = new Service() 
        { 
            ServiceId = serviceId, 
            ExtAppSettings = string.Empty 

        };
 
        using(var scope = AlvaoContext.GetConnectionScope())
        {
            string query = string.Format(@"
                select {0} as ExtAppSettings, iHdSectionId as ServiceId
                from tHdSection s
                inner join tHdSectionCust sc on sc.liHdSectionId = s.iHdSectionId
                where s.iHdSectionId = @serviceId", extAppSettingsPersonColumn);
 
            var service = scope.Connection.QueryFirstOrDefault&lt;Service&gt;(query, new { serviceId }, scope.Transaction);
            return service ?? defaultService;
        }
    }
 
    public static Service GetTicketService(int ticketId, string extAppSettingsPersonColumn)
    {
        var ticket = Alvao.API.SD.Ticket.GetById(ticketId);
        return GetService(ticket.liHdTicketHdSectionId, extAppSettingsPersonColumn);
    }
 
    public static tAct GetActById(int actId)
    {
        using(var scope = AlvaoContext.GetConnectionScope())
        {
            return scope.Connection.QueryFirstOrDefault&lt;tAct&gt;("select * from tAct where iActId = @actId", new {actId}, scope.Transaction);
        }
    }
 
    public static IEnumerable&lt;tDocument&gt; GetActDocuments(int actId)
    {
        using(var scope = AlvaoContext.GetConnectionScope())
        {
            return scope.Connection.Query&lt;tDocument&gt;("select * from tDocument where liDocumentActId = @actId", new {actId}, scope.Transaction);
        }
    }
 
    public static IVendorIntegration GetVendorIntegration()
    {
        var vendorInterface = typeof(IVendorIntegration);
        var assemblies = AppDomain.CurrentDomain.GetAssemblies();
        foreach(var assembly in assemblies)
        {
            foreach(var at in assembly.GetTypes())
            {
                if (vendorInterface.IsAssignableFrom(at))
                {
                    if (!at.IsInterface)
                        return (IVendorIntegration)Activator.CreateInstance(at);
                }
            }
        }
 
        throw new InvalidOperationException();
    }
 
    public static void LogError(int ticketId, string errorSubject, Exception ex)
    {
        Alvao.API.SD.Act.Create(
            ticketId, 
            errorSubject, 
            new HtmlTextModel(Alvao.Global.StringLib.HtmlEncode2(ex.ToString())),
            Alvao.API.Common.Person.GetSystem().iPersonId,
            null,
            Alvao.API.Common.Model.Database.tActKind.ActKind.Other,
            new Alvao.API.SD.Model.ActCreateSettings { IgnoreRights = true }
        );

        var logger = Alvao.API.Internal.TenantDiagnosticsLog.Get();
        ex.Data.Add("Ticket ID", ticketId);
        logger.Error(ex, $"Azure DevOps Connector encountered an error: {errorSubject}");
    }
}
 
public class Service
{
    public int ServiceId { get; set; }
    public string ExtAppSettings { get; set; }

    public bool IsExternalSystemConnector(string connectorType)
    {
        string serviceConnector = GetExtAppSettingsItem&lt;string&gt;("ConnectorType");
        return connectorType.Equals(serviceConnector, StringComparison.OrdinalIgnoreCase);
    }

    public string GetExtAppUrl()
    {
        return GetExtAppSettingsItem&lt;string&gt;("URL");
    }

    public bool GetExtAppSynchronizeAllCommunication()
    {
        return GetExtAppSettingsItem&lt;bool&gt;("SynchronizeAllCommunication");
    }

    public T GetExtAppSettingsItem&lt;T&gt;(string itemName)
    {
        if (string.IsNullOrEmpty(ExtAppSettings))
            return default(T);

        var settings = JObject.Parse(ExtAppSettings);
        var item = settings[itemName];

        if (item != null)
        {
            return item.ToObject&lt;T&gt;();
        }

        return default(T);
    }
}
 
public interface IVendorIntegration
{
    string ExternalSystemApiUrl { get; set; }
    Service IntegratedService { get; set; }
    string CreateTicket(int ticketId, SqlConnection con, SqlTransaction trans);
    void UpdateTicket(int ticketId, string changedProperties, string externalTicketId, SqlConnection con, SqlTransaction trans);
    void CreateAct(int actId, string externalTicketId, SqlConnection con, SqlTransaction trans);
}</Code>
          <IsLibCode>true</IsLibCode>
          <Codesign>Q0j4DxVD349lTXVCh7CM8uRZr9lkyBYxsePTG2mPDbTbNGxwUxDRP5p0b+Tx+mLtIXDh015rVk3DkgYXBqdn1vKkNZFeBBCENfLjV3MYa7dIkZVgxOe4HBRlippKPDAH3ZDTbmX2+39P77hj950PMl0eurQDA2jW54Al/tLyWwJ01iXUrKEBG8GYPOoBCt60/ybPgzWhmUV5JkvCjbSV2yelxc+U08QXn2YESmsT9KhtIdfEb5fJcxnvCC0K2wWah3nwxBRK4IasmIcXKKWFe0ISkw35Yf8uwVkQ6+HV/gMCdIkQLVyqGnj1idTrXXnL38KYlJAOX5THjCkaqtQYOQ==</Codesign>
        </Script>
        <Script id="4114">
          <Name>AlvaoVendorIntegrationAutoActions</Name>
          <Code>using System;
using System.Data;
using Microsoft.Data.SqlClient;
using Alvao.Global;
using Alvao.API.Common;
using Alvao.API.SD.Model;
using Alvao.Apps.API;
using System.Net.Http;
using System.Net.Http.Headers;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using System.Text;
using Alvao.API.Common.Model.Database;
using Alvao.API.Utils;
using System.Linq;
using System.IO;
using System.Collections.Generic;
using Alvao.Context;
using Dapper;
using Alvao.API.SD;
using System.Globalization;
using System.Text.RegularExpressions;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.VisualStudio.Services.WebApi;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.WebApi.Patch.Json;

public class AlvaoVendorIntegrationAutoActions : ITicketAutoAction, IActAutoAction
{
    public string Name { get; set; }
    private IVendorIntegration vendor;
 
    public AlvaoVendorIntegrationAutoActions()
    {
        Name = "Vendor Integration: " + Settings.ConnectorType;
        vendor = Helpers.GetVendorIntegration();
    }

    private bool LicenceCheck() =&gt; Alvao.API.Common.Activation.IsModuleActivated(Alvao.Global.ModuleInfo.ModuleId.DevOpsConnectors);
 
    public void OnTicketChanged(SqlConnection con, SqlTransaction trans, int ticketId, int personId, string properties)
    {
        if(!LicenceCheck())
            return;

        Service service = Helpers.GetTicketService(ticketId, Settings.AppSettingsPersonColumn);
        if (service.IsExternalSystemConnector(Settings.ConnectorType) &amp;&amp; personId != Alvao.API.Common.Person.GetSystem().iPersonId)
        {
            string externalTicketId = Helpers.GetExternalTicketId(ticketId, Settings.ExternalTicketIdTicketColumn);
            var ticket = Ticket.GetById(ticketId);
 
            vendor.ExternalSystemApiUrl = service.GetExtAppUrl();
            vendor.IntegratedService = service;
            var statusMap = service.GetExtAppSettingsItem&lt;Dictionary&lt;string, string&gt;&gt;(Settings.StatusMapName);

            Alvao.API.Common.Model.Database.TicketState ticketState;
            if (string.IsNullOrEmpty(externalTicketId) &amp;&amp; properties.Contains("tHdTicket.TicketStateId") &amp;&amp; statusMap.ContainsValue((ticketState = Alvao.API.SD.TicketState.GetById(ticket.TicketStateId))?._TicketState))
            {
                if(ticketState.TicketStateBehaviorId == (int)TicketStateBehavior.BehaviorId.Reopen)
                    return; // dont create ticket when ticket has been reopened

                try
                {
                    externalTicketId = vendor.CreateTicket(ticketId, con, trans);
                    Helpers.SaveExternalTicketId(ticketId, externalTicketId, Settings.ExternalTicketIdTicketColumn);
                }
                catch (Exception e)
                {
                    Helpers.LogError(ticketId, Settings.CreateTicketErrorSubject, e);
                }
            }
            else
            {
                try
                {
                    vendor.UpdateTicket(ticketId, properties, externalTicketId, con, trans);
                }
                catch (Exception e)
                {
                    Helpers.LogError(ticketId, Settings.UpdateTicketErrorSubject, e);
                }
            }
        }
    }
 
    public void OnActCreated(SqlConnection con, SqlTransaction trans, int actId, int personId)
    {
        if(!LicenceCheck())
            return;

        var act = Helpers.GetActById(actId);

        if(!act.liActHdTicketId.HasValue)
            return;
                    
        if(act.liActFromPersonId == Alvao.API.Common.Person.GetSystem().iPersonId)
            return;

        if (act.liActKindId == (int)tActKind.ActKind.Process || act.liActKindId == (int)tActKind.ActKind.Other || act.liActKindId == (int)tActKind.ActKind.Notification)
            return;

        int ticketId = act.liActHdTicketId.Value;
        Service service = Helpers.GetTicketService(ticketId, Settings.AppSettingsPersonColumn);

        if (!service.IsExternalSystemConnector(Settings.ConnectorType))
            return;

        if(!service.GetExtAppSettingsItem&lt;bool&gt;(Settings.SyncCommunicationName))
            return;

        string externalTicketId = Helpers.GetExternalTicketId(ticketId, Settings.ExternalTicketIdTicketColumn);
 
        if (string.IsNullOrEmpty(externalTicketId))
            return;

        var ticket = Ticket.GetById(ticketId);
        if(ticket != null &amp;&amp; ticket.liHdTicketStartingActId == actId)
            return;

        vendor.ExternalSystemApiUrl = service.GetExtAppUrl();
        vendor.IntegratedService = service;
        
        try
        {
            vendor.CreateAct(actId, externalTicketId, con, trans);
        }
        catch (Exception e)
        {
            Helpers.LogError(ticketId, Settings.CreateActErrorSubject, e);
        }
    }
 
    public void OnTicketCreated(SqlConnection con, SqlTransaction trans, int ticketId, int personId)
    {
        // only solver -&gt; automatically assigned -&gt; changed to status which is in statusMapping
        OnTicketChanged(con, trans, ticketId, personId, "tHdTicket.TicketStateId");
    }
 
    public void OnActChanged(SqlConnection con, SqlTransaction trans, int actId, int personId, string properties)
    {
        throw new NotImplementedException();
    }
 
    public void OnActRemoved(SqlConnection con, SqlTransaction trans, int actId, int personId)
    {
        throw new NotImplementedException();
    }
}

public class AzureDevOpsIntegration : IVendorIntegration
{
    public string ExternalSystemApiUrl { get; set; }
    public Service IntegratedService { get; set; }
    private static readonly HttpClient client = new HttpClient();
    private static WorkItemTrackingHttpClient devOpsClient;

    private WorkItemTrackingHttpClient GetClient()
    {
        if(devOpsClient != null)
            return devOpsClient;

        var connection = GetDevOpsConnection();
        var client = connection.GetClient&lt;WorkItemTrackingHttpClient&gt;();
        return devOpsClient = client;
    }



    public void CreateAct(int actId, string externalTicketId, SqlConnection con, SqlTransaction trans)
    {
        var act = Helpers.GetActById(actId);
        if (act is null)
            return;
        var docs = Helpers.GetActDocuments(actId);

        var message = PrepareMessage(act.ActHtml, ref docs);
        UploadAttachments(docs, externalTicketId);

        var createComment = new {
            text = $@"{act.sActFrom} wrote: 
            
            {act.sAct}
            
            {message}"
        };

        var request = PrepareRequest(HttpMethod.Post, $"workItems/{externalTicketId}/comments", createComment );
        var response = client.SendAsync(request).Result;
        var responseContent = response.Content.ReadAsStringAsync().Result;
        if (!response.IsSuccessStatusCode)
            throw new Exception($"Unsuccessful response from Azure DevOps: {response.StatusCode} {responseContent}");
    }

    private string PrepareMessage(string message, ref IEnumerable&lt;tDocument&gt; docs)
    {
        if(message.Contains(Settings.OriginalMessageSeparator))
        {
            var messageParts = message.Split(new string[] {Settings.OriginalMessageSeparator}, 2, StringSplitOptions.None);
            if(messageParts.Count() == 2)
            {
                message = messageParts[0]; // remove original message
            }
        }

        List&lt;int&gt; docIdsToRemove = new();
        foreach(var intextImg in docs.Where(d =&gt; d.EmbededImage))
        {
            var intextImgTag = $"[$cid:{intextImg.iDocumentId}$]";
            
            if(message.Contains(intextImgTag))
                message = message.Replace(intextImgTag, UploadAttachment(new MemoryStream(intextImg.oDocument), intextImg.sDocument)); // extract CID, images will be removed in StripHtml
            else // remove attachment, it is from original message 
                docIdsToRemove.Add(intextImg.iDocumentId);
        }

        docs = docs.Where(d =&gt; !docIdsToRemove.Contains(d.iDocumentId));

        var atts = docs.Where(d =&gt; !d.EmbededImage);
        if(atts.Any())
        {
            message += @"

            Attachments: 
            ";

            foreach(var att in atts)
            {
                message += $"&lt;a href=\"{UploadAttachment(new MemoryStream(att.oDocument), att.sDocument)}\"&gt;{att.sDocument}&lt;/a&gt;" + Environment.NewLine;
            }
        }
        return message;
    }

    private HttpRequestMessage PrepareRequest(HttpMethod method, string url, object content = null){
        var httpRequest = new HttpRequestMessage(method, $"{ExternalSystemApiUrl}/_apis/wit/{url}?api-version=7.0-preview.3");
        
        httpRequest = AddAuth(httpRequest);

        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;
    }

    private HttpRequestMessage AddAuth(HttpRequestMessage httpRequest)
    {
        string token = GetToken();

        httpRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($":{token}")));
        
        return httpRequest;
    }

    private string GetToken()
    {
        var tokenNumber = IntegratedService.GetExtAppSettingsItem&lt;string&gt;("AccessToken");

        using (var scope = AlvaoContext.GetConnectionScope())
        {
            var token = scope.Connection.ExecuteScalar&lt;string&gt;("SELECT TOP 1 sPropertyValue FROM tProperty WHERE sProperty = @propertyName",
                new { propertyName = $"DevOpsIntegrationToken{tokenNumber}" }, scope.Transaction);
            return token ?? string.Empty;
        }
    }

    public string CreateTicket(int ticketId, SqlConnection con, SqlTransaction trans)
    {
        tHdTicket ticket = Alvao.API.SD.Ticket.GetById(ticketId);

        string reporter = IntegratedService.GetExtAppSettingsItem&lt;string&gt;(Settings.TicketReporterName);

        Dictionary&lt;string, string&gt; attrMap = IntegratedService.GetExtAppSettingsItem&lt;Dictionary&lt;string, string&gt;&gt;(Settings.AttributeMapName);
                
        if(!string.IsNullOrEmpty(reporter) &amp;&amp; reporter.ToLower() != "default")
        {
            if(reporter.ToLower() == "requester")
            {
                var requester = Alvao.API.Common.Person.GetById(ticket.liHdTicketUserPersonId);
                var username = requester?.sPersonLogin;
                if(!string.IsNullOrEmpty(username)){
                    attrMap.Add("System.CreatedBy", username);
                }
            }
            else 
                attrMap.Add("System.CreatedBy", reporter);
        }
        var client = GetClient();
        var culture = CultureInfo.GetCultureInfo(GetServiceLanguage(ticket.liHdTicketHdSectionId));
        var workItemFields = PrepareWorkItemFields(attrMap, ticketId, culture);
        var patchDocument = MakePatchDocument(workItemFields);

        var workItemResult = client.CreateWorkItemAsync(patchDocument, GetProjectName(), GetWorkItemType(workItemFields)).Result;
        
        string externalId = workItemResult.Id.Value.ToString();
        
        if (workItemResult is not null)
            UploadInitialAttachments(attrMap, ticketId, externalId); 

        Act.Create(
            ticketId, 
            Settings.IssueCreatedMessage, 
            new Alvao.API.Common.Model.HtmlTextModel(Settings.IssueCreatedMessage), 
            Alvao.API.Common.Person.GetSystem().iPersonId, 
            null, 
            tActKind.ActKind.Process, 
            new Alvao.API.SD.Model.ActCreateSettings() { CallCustomApps = false }
            );
        return externalId;
    }

    private Dictionary&lt;string, string&gt; PrepareWorkItemFields(Dictionary&lt;string, string&gt; attributeMap, int ticketId, CultureInfo sectionCulture)
    {
        Dictionary&lt;string, string&gt; resultFields = new Dictionary&lt;string, string&gt;();

        foreach (var originalField in attributeMap)
        {
            var evaluatedMessage = Alvao.API.SD.MessageTemplate.EvaluateMessageForRequester(originalField.Value, ticketId, sectionCulture, formatDateTimeToUniversalSortable: true);
            evaluatedMessage = System.Text.RegularExpressions.Regex.Replace(evaluatedMessage, @"""[^""\\]*\[\$cid:(\d+)\$\]""", match =&gt; MakeAttachmentReference(Convert.ToInt32(match.Groups[1].Value)));
            resultFields.Add(originalField.Key, evaluatedMessage);
        }
        return resultFields;
    }

    private string MakeAttachmentReference(int attachmentId)
    {
        MemoryStream uploadData = null;
        string fileName = "";
        using (var scope = AlvaoContext.GetConnectionScope())
        {
            var att = scope.Connection.Query&lt;tDocument&gt;("SELECT sDocument, oDocument, sDocumentContentType FROM tDocument WHERE iDocumentId = @attachmentId", new { attachmentId }, scope.Transaction).FirstOrDefault();
            if(att == null)
                return string.Empty;
                
            uploadData = new MemoryStream((byte[])att.oDocument);
            fileName = att.sDocument;
        }
        return UploadAttachment(uploadData, fileName);
    }

    private string UploadAttachment(Stream data, string fileName)
    {
        var client = GetClient();
        var attachment = client.CreateAttachmentAsync(data, project: GetProjectName(), fileName: fileName).Result;
        return attachment.Url;
    }

    private void UploadInitialAttachments(Dictionary&lt;string, string&gt; attrMap, int ticketId, string externalId)
    {
        if (attrMap.ContainsKey("System.Description"))
        {
            var descriptionActId = GetDescriptionActId(attrMap, ticketId);
            if (descriptionActId == null)
                return;            

            var documents = Helpers.GetActDocuments((int)descriptionActId);
            if(documents == null || !documents.Any())
                return;
            
            var patch = CreateAttachmentPatchOperations(documents);
            UpdateWorkItemAttachments(patch, externalId);
        }
    }

    private int? GetDescriptionActId(Dictionary&lt;string, string&gt; attrMap, int ticketId)
    {   
        using var scope = AlvaoContext.GetConnectionScope();
        if (attrMap["System.Description"] == TemplatesVariablesConsts.OriginalMessage.Wrap())
                return Ticket.GetById(ticketId)?.liHdTicketStartingActId;
        else if (attrMap["System.Description"] == TemplatesVariablesConsts.LatestImportantMessage.Wrap())
            return scope.Connection.QueryFirstOrDefault&lt;int&gt;(@"select top 1 iActId from tAct where ActMarkId = 1 and liActHdTicketId = @ticketId order by dAct desc", new { ticketId}, scope.Transaction);
        else
            return null;
    }

    private JsonPatchDocument CreateAttachmentPatchOperations(IEnumerable&lt;tDocument&gt; docs)
    {
        var client = GetClient();
        var patch = new JsonPatchDocument();
        
        foreach (var doc in docs)
        {
            using var ms = new MemoryStream((byte[])doc.oDocument);
            var attachmentRef = client.CreateAttachmentAsync(ms, project: GetProjectName(), fileName: doc.sDocument).Result;

            patch.Add(new JsonPatchOperation
            {
                Operation = Microsoft.VisualStudio.Services.WebApi.Patch.Operation.Add,
                Path = "/relations/-",
                Value = new
                {
                    rel = "AttachedFile",
                    url = attachmentRef.Url,
                    attributes = new
                    {
                        name = doc.sDocument
                    }
                }
            });
        }
        return patch;
    }

    private void UpdateWorkItemAttachments(JsonPatchDocument patch, string externalId)
    {
        var client = GetClient();
        _ = client.UpdateWorkItemAsync(patch, GetProjectName(), int.Parse(externalId)).Result;
    }

    private JsonPatchDocument MakePatchDocument(Dictionary&lt;string, string&gt; workItemFields, Microsoft.VisualStudio.Services.WebApi.Patch.Operation operation = Microsoft.VisualStudio.Services.WebApi.Patch.Operation.Add)
    {
        var document = new JsonPatchDocument();
        foreach (var field in workItemFields)
        {
            document.Add(new JsonPatchOperation()
            {
                Operation = Microsoft.VisualStudio.Services.WebApi.Patch.Operation.Add,
                Path = "/fields/" + field.Key,
                Value = field.Value
            });
        }
        return document;
    }

    public void UploadAttachments(IEnumerable&lt;tDocument&gt; documents, string issueKey)
    {   
        if(documents == null || !documents.Any())
            return;
      
        foreach(var doc in documents){
            UploadAttachment(new MemoryStream((byte[])doc.oDocument), doc.sDocument);
        }
    }

    private string GetProjectName()
    {
        var url = IntegratedService.GetExtAppSettingsItem&lt;string&gt;(Settings.UrlSettingsName);
        var delimiterPosition = url.LastIndexOf('/');
        return url.Substring(delimiterPosition + 1);
    }

    private string GetWorkItemType(IReadOnlyDictionary&lt;string, string&gt; workItemFields)
    {
        var typeField = workItemFields.FirstOrDefault(item =&gt; "System.WorkItemType".Equals(item.Key, StringComparison.OrdinalIgnoreCase));
        return typeField.Value;
    }

    private VssConnection GetDevOpsConnection()
    {
        var url = IntegratedService.GetExtAppSettingsItem&lt;string&gt;(Settings.UrlSettingsName);
        var delimiterPosition = url.LastIndexOf('/');
        string tenantUrl = url.Substring(0, delimiterPosition);
        var credential = new VssBasicCredential(string.Empty, GetToken());
        var connection = new VssConnection(new Uri(tenantUrl), credential);
        return connection;
    }

    private static int GetServiceLanguage(int serviceId){
        using(var scope = AlvaoContext.GetConnectionScope()){
            return scope.Connection.ExecuteScalar&lt;int&gt;(
                "SELECT COALESCE((select DefaultLanguageId from tHdSection where iHdSectionId=@serviceId),(SELECT iPropertyValue FROM tProperty WHERE sProperty = 'DefaultLanguageId'), iDbLocaleId) FROM tDb",
                new { serviceId }, scope.Transaction );
        }
    }

    private static object[] GetActAttachments(int actId)
    {
        var documents = Helpers.GetActDocuments(actId);
        return DocumentsToAttachments(documents);
    }

    private static object[] DocumentsToAttachments(IEnumerable&lt;tDocument&gt; documents){
        if(documents == null){
            return null;
        }
        return documents.Where(d =&gt; !d.EmbededImage).Select(d =&gt; new { name = d.sDocument, data = System.Convert.ToBase64String(d.oDocument) }).ToArray();
    }

    public void UpdateTicket(int ticketId, string changedProperties, string externalTicketId, SqlConnection con, SqlTransaction trans)
    {
        if(string.IsNullOrEmpty(changedProperties))
            return;

        if(changedProperties.Contains("tHdTicket.TicketStateId"))
        {
            var ticket = Alvao.API.SD.Ticket.GetById(ticketId);
            var statusMap = IntegratedService.GetExtAppSettingsItem&lt;Dictionary&lt;string, string&gt;&gt;(Settings.StatusMapName);
            var status = Alvao.API.SD.TicketState.GetById(ticket.TicketStateId);
            string statusName = status?._TicketState;
            var state = statusMap.FirstOrDefault(m =&gt; m.Value == statusName);

            if(string.IsNullOrEmpty(state.Key)){
                return;
            }

            int? messageId = null;
            if(status.TicketStateBehaviorId == (int)TicketStateBehavior.BehaviorId.Resolve 
            || status.TicketStateBehaviorId == (int)TicketStateBehavior.BehaviorId.Close 
            || status.TicketStateBehaviorId == (int)TicketStateBehavior.BehaviorId.Reopen)
            {
                messageId = GetChangeStatusMessage(ticketId);
            }
            
            var statusChange = new Dictionary&lt;string, string&gt;(){
                {"System.State", state.Key}
            };
            var patchDocument = MakePatchDocument(statusChange, Microsoft.VisualStudio.Services.WebApi.Patch.Operation.Replace);
            var client = GetClient();
            _ = client.UpdateWorkItemAsync(patchDocument, GetProjectName(), int.Parse(externalTicketId)).Result;
                        
            if(messageId.HasValue)
                CreateAct(messageId.Value, externalTicketId, con, trans);
        }    
    }

    public int? GetChangeStatusMessage(int ticketId){
        using(var scope = AlvaoContext.GetConnectionScope())
        {
            return scope.Connection.ExecuteScalar&lt;int?&gt;("select top 1 iActId from tAct where liActHdTicketId = @ticketId and ActOperationId in (3,4) order by dAct desc", new { ticketId }, scope.Transaction);
        }
    }

}</Code>
          <IsLibCode>false</IsLibCode>
          <Codesign>SAXV4b6zyIaXvD/y3UbcGHpEW08TbFNsBjaZtLXbWu676zVOkTO8Qx/ywY77/gxM3LycX3R5lPaWQc6X/0aLV4oR9QrRWjvcO/1pgKP200KgDktv8MQScyAjxmuSe451cXTJxwTE0PSD6FwTBsdCnmFD0C88nXoAvgWKm0GWon5ukjd5uMdKXema4Ng2bU/yWNdNhgxWgIdZoPBTBII9PyjW4PURI+JsOfAn5gbGwPQv6BkoDxQC51yM3yWzfvc8wq6zqm9g8tyAnnE6vp6XY+wYipBQEUPqUU1ui4QbcQqBmbfHPCNK6lRFFvuh8tjIuz1I3dnBqHpU8kxhdfs5pA==</Codesign>
        </Script>
        <Script id="4115">
          <Name>OpenExternalTicketEntityCommand</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.API.Common.Model;

public class OpenExternalTicketEntityCommand : IEntityCommand 
{
    public string Id {get; set;}
    public Entity Entity {get; set;}

    public OpenExternalTicketEntityCommand()
    {
        Id = "OpenExternalTicketEntityCommand";
        Entity = Entity.Request;
    }

    public EntityCommandShowResult Show(int entityId, int personId)
    {   
        bool moduleActivated = Alvao.API.Common.Activation.IsModuleActivated(Alvao.Global.ModuleInfo.ModuleId.DevOpsConnectors);
        
        int position = 2;
        string icon = "open_20_regular";
        string name = "Open in Azure DevOps";  
        bool show = moduleActivated &amp;&amp; !string.IsNullOrEmpty(Database.ReadColumn(entityId, CustomTables.tHdTicket, Settings.ExternalTicketIdTicketColumn)); // Set if command should show for person

        if(show)
        {
            Service service = Helpers.GetTicketService(entityId, Settings.AppSettingsPersonColumn);
            show = service.GetExtAppSettingsItem&lt;bool?&gt;(Settings.ShowCommandName) ?? true;
        }
            
        return new EntityCommandShowResult (show, name, icon, position);
    }

    public CommandResult Run(int entityId, int personId)
    {
        Service service = Helpers.GetTicketService(entityId, Settings.AppSettingsPersonColumn);
        var extId = Database.ReadColumn(entityId, CustomTables.tHdTicket, Settings.ExternalTicketIdTicketColumn);
        MessageType messageType = MessageType.None ;
        string messageText = null; 
        string navigateToUrl = service.GetExtAppUrl() + "/_workitems/edit/" + extId; 
        return new CommandResult(messageType, messageText, navigateToUrl);
    }
}</Code>
          <IsLibCode>false</IsLibCode>
          <Codesign>CnLhzJJwvq99nCwvF+ul+E+iuXr7hNcWVCEmWR68l58h3YDW6myXNoUg+OP2NY4tuwsU4rGBjVNqqs7Blo+wYgSF5B41GyFU791n5y5z+Cq6zmm8ikk76LlKTjFEMqErPhKQxRQOe5G/+hN4GMs0EET5wwTZUCrwi0AIksQVVog4AWV0SsLIaN6nSKsyq3D7iF0Zc+kAbiSQEXJ5Q9FW60c5sbhmi3QNiHlrlzRLE/RQ3G/l/+bdH3aSzMgeikV15Z8vkFQeY6ww7Uan2WXKdMX7w0PI8vWjp63fYPzHb+AywqFInrmT9WJ5FtmmVmhGSAOrYkZu5QGUmaolH60Biw==</Codesign>
        </Script>
      </Scripts>
    </Application>
  </Applications>
</AlvaoApplication>