Skip to content

Migrations to Connections

Overview

New connectors should be implemented with structured connections, and the now-required --auth-type on new clients provides template code. Structured connections were introduced to better protect and mange the security of client access credentials. This guide exists to support updating connectors built without structured connections implemented. If your connector code already contains a "connections" folder and xchange client new was used with an auth type then you do not need this guide.

Updating the Connector

Here's an example of migrating an existing connector build on v1.1.1, and its authentication is Token Api.

The project structure of the connector can be seen in the following picture.

alt text

The authentication was added to the request directly in the connector's ApiClient, adding the header in the constructor.

alt text

Steps
  • Update SDK to v >= 1.3.3 or above
  • Run command to create a new client. If your client needs ApiKey, Basic, OAuth2CodeFlow and OAuth2ClientCredentials authentication, use the --auth-type option with the specific authentication type. If you need a type of authentication that is not listed, you can use Custom an implement what you need. For this example, we will use Custom. This will add the Connections with CustomAuth.cs and ConnectionTestHandler.cs to the project, and CustomAuthHandler.cs and ClientHelper.cs were added as well, but in the Client directory.
    xchange client new --auth-type custom
    
    alt text
  • Remove ApiClient Configuration from ConnectorRegistration.cs. Now the ApiClient configuration will be done in ClientHelper.cs.
ConnectorRegistration.cs
public class ConnectorRegistration : IConnectorRegistration<ConnectorRegistrationConfig>, IConfigureConnectorApiClient
{
    public void ConfigureServices(IServiceCollection serviceCollection, IHostContext hostContext)
    {
        var connectorRegistrationConfig = JsonSerializer.Deserialize<ConnectorRegistrationConfig>(hostContext.GetSystemConfig().Configuration);
        serviceCollection.AddSingleton(connectorRegistrationConfig!);
        serviceCollection.AddTransient<RetryPolicyHandler>();
-        // Configure ApiClient
-         serviceCollection.AddHttpClient();
-         serviceCollection.AddScoped<IApiClient, ApiClient>();
    }

    public void RegisterServiceDefinitions(IServiceCollection serviceCollection)
    {
        serviceCollection.AddSingleton<IConnectorServiceDefinition, AppV1ActionProcessorServiceDefinition>();
        serviceCollection.AddSingleton<IConnectorServiceDefinition, AppV1CacheWriterServiceDefinition>();
    }

    public void ConfigureConnectorApiClient(IServiceCollection serviceCollection, IHostConnectionContext hostConnectionContext)
    {
        var activeConnection = hostConnectionContext.GetConnection();
        serviceCollection.ResolveServices(activeConnection);
    }

    public void RegisterConnectionTestHandler(IServiceCollection serviceCollection)
    {
        serviceCollection.AddSingleton<IConnectionTestHandler, ConnectionTestHandler>();
    }
}
ClientHelper.cs
using Microsoft.Extensions.DependencyInjection;
using System;
using Xchange.Connector.SDK.Client.ConnectivityApi.Models;
using ESR.Hosting.Client;
using System.Text.Json;
using Connector.Connections;

namespace Connector.Client
{
    public static class ClientHelper
    {
        public static class AuthTypeKeyEnums
        {
            public const string CustomAuth = "customAuth";
        }

        public static void ResolveServices(this IServiceCollection serviceCollection, ConnectionContainer activeConnection)
        {
            serviceCollection.AddTransient<RetryPolicyHandler>();
            var options = new JsonSerializerOptions
            {
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase
            };
            switch (activeConnection.DefinitionKey)
            {
                case AuthTypeKeyEnums.CustomAuth:
                    var configCustomAuth = JsonSerializer.Deserialize<CustomAuth>(activeConnection.Configuration, options);
                    serviceCollection.AddSingleton<CustomAuth>(configCustomAuth!);
                    serviceCollection.AddTransient<RetryPolicyHandler>();
                    serviceCollection.AddTransient<CustomAuthHandler>();
                    serviceCollection.AddHttpClient<ApiClient, ApiClient>(client => new ApiClient(client, configCustomAuth!.BaseUrl))
                        .AddHttpMessageHandler<CustomAuthHandler>().AddHttpMessageHandler<RetryPolicyHandler>();
                    break;
                default:
                    throw new Exception($"Unable to find services for definition key {activeConnection.DefinitionKey}");
            }
        }
    }
}
  • Update dependencies for IApiClient / ApiClient. The ClientHelper.cs class generated by the CLI injects the ApiClient implementation, you could either update the data readers and actions to use ApiClient as a dependency as in the following example.
DataReader.cs
public class ProjectDataReader : TypedAsyncDataReaderBase<ProjectDataObject>
{
    private readonly ILogger<ProjectDataReader> _logger;
    private readonly IHostContext _hostContext;
-   private readonly IApiClient _apiClient;
+   private readonly ApiClient _apiClient;

    public ProjectDataReader(
        ILogger<ProjectDataReader> logger,
        IHostContext hostContext,
-       IApiClient client)
+       ApiClient client)
    {
        _logger = logger;
        _hostContext = hostContext;
        _apiClient = client;
    }

...
}

Or you could update the ClientHelper class to use an interface of ApiClient as a dependency and keep the interface dependency in data readers and actions.

ClientHelper.cs
using Microsoft.Extensions.DependencyInjection;
using System;
using Xchange.Connector.SDK.Client.ConnectivityApi.Models;
using ESR.Hosting.Client;
using System.Text.Json;
using Connector.Connections;

namespace Connector.Client
{
    public static class ClientHelper
    {
        public static class AuthTypeKeyEnums
        {
            public const string CustomAuth = "customAuth";
        }

        public static void ResolveServices(this IServiceCollection serviceCollection, ConnectionContainer activeConnection)
        {
            serviceCollection.AddTransient<RetryPolicyHandler>();
            var options = new JsonSerializerOptions
            {
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase
            };
            switch (activeConnection.DefinitionKey)
            {
                case AuthTypeKeyEnums.CustomAuth:
                    var configCustomAuth = JsonSerializer.Deserialize<CustomAuth>(activeConnection.Configuration, options);
                    serviceCollection.AddSingleton<CustomAuth>(configCustomAuth!);
                    serviceCollection.AddTransient<RetryPolicyHandler>();
                    serviceCollection.AddTransient<CustomAuthHandler>();
                    serviceCollection.AddHttpClient<
+                   IApiClient,
-                   ApiClient
                        ApiClient>(client => new ApiClient(client, configCustomAuth!.BaseUrl))
                        .AddHttpMessageHandler<CustomAuthHandler>().AddHttpMessageHandler<RetryPolicyHandler>();
                    break;
                default:
                    throw new Exception($"Unable to find services for definition key {activeConnection.DefinitionKey}");
            }
        }
    }
}
  • Move connection properties from ConnectorRegistrationConfig.cs to CustomAuth.cs. In this case, all properties in ConnectorRegistrationConfig class are connection properties needed in CustomAuth, so the ConnectorRegistrationConfig will end up without properties.
The ConnectorRegistrationConfig class
namespace Connector;

using System.Text.Json.Serialization;

public class ConnectorRegistrationConfig
{
-    [JsonPropertyName("apiDetails")]
-    public ApiDetails ApiDetails { get; set; } = new ApiDetails();
}

- public class ApiDetails
- {
-    [JsonPropertyName("baseUrl")]
-    public string BaseUrl { get; set; } = string.Empty;
-
-    [JsonPropertyName("apiToken")]
-    public string ApiToken { get; set; } = string.Empty;
- }
The CustomAuth class
using System;
using Xchange.Connector.SDK.Client.AuthTypes;
using Xchange.Connector.SDK.Client.ConnectionDefinitions.Attributes;
namespace Connector.Connections;

[ConnectionDefinition(title: "Api Token Auth", description: "")]
public class CustomAuth : ICustomAuth
{
+   [ConnectionProperty(title: "Api Token", description: "", isRequired: true, isSensitive: true)]
+   public string ApiToken { get; init; } = string.Empty;
+
+   [ConnectionProperty(title: "Connection Environment", description: "", isRequired: true, isSensitive: false)]
+   public ConnectionEnvironmentCustomAuth ConnectionEnvironment { get; set; } = ConnectionEnvironmentCustomAuth.Unknown;+
+
+   public string BaseUrl
+    {
+        get
+        {
+            switch (ConnectionEnvironment)
+            {
+                case ConnectionEnvironmentCustomAuth.Production:
+                    return "https://base-url.com";
+                case ConnectionEnvironmentCustomAuth.Test:
+                    return "https://test.base-url.com";
+                    throw new Exception("No base url was set.");
+        default:
+            }
+        }
+    }
}
+
+public enum ConnectionEnvironmentCustomAuth
+{
+    Unknown = 0,
+    Production = 1,
+    Test = 2
+}

If there's only one valid BaseUrl, you can set it like this.

The CustomAuth class with only one valid BaseUrl
using System;
using Xchange.Connector.SDK.Client.AuthTypes;
using Xchange.Connector.SDK.Client.ConnectionDefinitions.Attributes;
namespace Connector.Connections;

[ConnectionDefinition(title: "Api Token Auth", description: "")]
public class CustomAuth : ICustomAuth
{
+   [ConnectionProperty(title: "Api Token", description: "", isRequired: true, isSensitive: true)]
+   public string ApiToken { get; init; } = string.Empty;
+
+   public string BaseUrl { get { return "https://base-url.com"; } }
-   [ConnectionProperty(title: "Connection Environment", description: "", isRequired: true, isSensitive: false)]
-   public ConnectionEnvironmentCustomAuth ConnectionEnvironment { get; set; } = ConnectionEnvironmentCustomAuth.Unknown;+
-
-   public string BaseUrl
-    {
-        get
-        {
-            switch (ConnectionEnvironment)
-            {
-                case ConnectionEnvironmentCustomAuth.Production:
-                    return "https://base-url.com";
-                case ConnectionEnvironmentCustomAuth.Test:
-                    return "https://test.base-url.com";
-                    throw new Exception("No base url was set.");
-        default:
-            }
-        }
-    }
}
-
-public enum ConnectionEnvironmentCustomAuth
-{
-    Unknown = 0,
-    Production = 1,
-    Test = 2
-}
  • Update ApiClient so it has baseUrl as a dependency instead of ConnectorRegistrationConfig
The ApiClient class
public class ApiClient : IApiClient
{
    private readonly HttpClient _httpClient;
-   private readonly ConnectorRegistrationConfig _systemConfig;

    public ApiClient(HttpClient httpClient,
-   ConnectorRegistrationConfig systemConfig)
+   string baseUrl)
    {
-        _systemConfig = systemConfig;
        _httpClient = httpClient;
        httpClient.BaseAddress = new Uri(
-        _systemConfig.ApiDetails.BaseUrl)
+        baseUrl);
-        httpClient.DefaultRequestHeaders.Add("Authorization", "Token api=" + _systemConfig.ApiDetails.ApiToken);
    }
...
}
  • Move authentication logic from ApiClient to CustomAuthHandler
The CustomAuthHandler class
namespace Connector.Client;

public class CustomAuthHandler : DelegatingHandler
{
    private readonly CustomAuth _customAuth;

    public CustomAuthHandler(CustomAuth customAuth)
    {
        _customAuth = customAuth;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
+       request.Headers.Remove("Authorization");
+       request.Headers.Add("Authorization", $"Token api={_customAuth.ApiToken}");

        return await base.SendAsync(request, cancellationToken);
    }
}
  • Implement ConnectionTestHandler. This is the method that will be run every time a user wants to test the connection with the credentials entered in the properties of your custom authentication. This should call an endpoint of the external API and verify that it has been successful. In this case, we will call the GET /projects endpoint. It was decided to update the ApiResponse, since TestConnection() uses response.StatusCode which was not in the previous version. However, the implementation of the TestConnection() method can be adapted to any ApiResponse, using the CLI generated code is not mandatory.
ApiResponse.cs
public class ApiResponse<TResult>
{
    public bool IsSuccessful { get; set; }
    public TResult? Result { get; set; } // nullable
    public string? ErrorMessage { get; set; } // nullable
+   public int StatusCode { get; set; }
}

This is how the implementation of TestConnection() would look like:

ConnectionTestHandler.cs
namespace Connector.Connections
{
    public class ConnectionTestHandler : IConnectionTestHandler
    {
        private readonly ILogger<IConnectionTestHandler> _logger;
        private readonly ApiClient _apiClient;

        public ConnectionTestHandler(ILogger<IConnectionTestHandler> logger, ApiClient apiClient)
        {
            _logger = logger;
            _apiClient = apiClient;
        }

        public async Task<TestConnectionResult> TestConnection()
        {
+          var response = await _apiClient.GetProjects();

            if (response == null)
            {
                return new TestConnectionResult()
                {
                    Success = false,
                    Message = "Failed to get response from server",
                    StatusCode = 500
                };
            }

            if (response.IsSuccessful)
            {
                return new TestConnectionResult()
                {
                    Success = true,
                    Message = "Successful test.",
                    StatusCode = response.StatusCode
                };
            }

            switch (response.StatusCode)
            {
                case 403:
                    return new TestConnectionResult()
                    {
                        Success = false,
                        Message = "Invalid Credentials: Forbidden.",
                        StatusCode = response.StatusCode
                    };
                case 401:
                    return new TestConnectionResult()
                    {
                        Success = false,
                        Message = "Invalid Credentials: Unauthorized",
                        StatusCode = response.StatusCode
                    };
                default:
                    return new TestConnectionResult()
                    {
                        Success = false,
                        Message = "Unknown Issue.",
                        StatusCode = response.StatusCode
                    };
            }
        }
    }
}
Verify it works
  1. Run command to generate the test-settings.json file.
    xchange test init
    
  2. Go to test-settings.json file and fill properties in for the connection that will be tested. In this case apiToken for connection with the connection definition key customAuth. alt text
  3. Run Test Connection Local Development which will run your ConnectionTestHandler. If the connection is correctly implemented, it should print a successful message. alt text

Conclusion on migrating connector that uses SDK

Depending on the version used in the connector project there could be more or less changes needed to adopt structure connections, however, these next changes will be necessary for all scenarios.

  1. Move properties required for connections from ConnectorRegistrationConfig.cs to ConnectionDefinition Type class. (ex. CustomAuth.cs) Which are the properties required for connections? All properties required to make a successful request to the external API. Examples:

    • ApiKey
    • ClientId
    • ClientSecret
    • Username
    • Password
    • TokenUrl
    • AuthenticationUrl
  2. Delete the injection of the ApiClient and related classes from ConnectorRegistration since they should be on the ClientHelper class now. By default this will be the ApiClient and RetryPolicyHandler, but you may have added more.

  3. Check that your ApiClient has BaseUrl instead of ConnectorRegistrationConfig (or the other information formerly stored in ApiDetails), which is made available to ApiClient by the ClientHelper class via ConnectorRegistration automatically.

  4. Delete ApiDetails.cs file and remove it from ConnectorRegistrationConfig. This may leave your ConnecterRegistrationConfig empty. You can use it to create user specified workspace level configurable fields, filters, etc.

  5. Move code to authenticate into the DelegatingHandler created in the Client Folder, even if it was previously located in ApiClient or some other user-created file.

  6. Implement TestConnection() in ConnectionTestHandler.cs.