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.
The authentication was added to the request directly in the connector's ApiClient, adding the header in the constructor.
Steps
- Update SDK to v >= 1.3.3 or above
- Run command to create a new client. If your client needs
ApiKey
,Basic
,OAuth2CodeFlow
andOAuth2ClientCredentials
authentication, use the--auth-type
option with the specific authentication type. If you need a type of authentication that is not listed, you can useCustom
an implement what you need. For this example, we will useCustom
. 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
- 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 inCustomAuth
, so theConnectorRegistrationConfig
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 theApiResponse
, sinceTestConnection()
usesresponse.StatusCode
which was not in the previous version. However, the implementation of theTestConnection()
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
- Run command to generate the
test-settings.json
file.xchange test init
- 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
. - Run
Test Connection Local Development
which will run your ConnectionTestHandler. If the connection is correctly implemented, it should print a successful message.
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.
-
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
-
Delete the injection of the
ApiClient
and related classes fromConnectorRegistration
since they should be on theClientHelper
class now. By default this will be theApiClient
andRetryPolicyHandler
, but you may have added more. -
Check that your
ApiClient
hasBaseUrl
instead ofConnectorRegistrationConfig
(or the other information formerly stored in ApiDetails), which is made available toApiClient
by theClientHelper
class viaConnectorRegistration
automatically. -
Delete ApiDetails.cs file and remove it from
ConnectorRegistrationConfig
. This may leave yourConnecterRegistrationConfig
empty. You can use it to create user specified workspace level configurable fields, filters, etc. -
Move code to authenticate into the
DelegatingHandler
created in the Client Folder, even if it was previously located inApiClient
or some other user-created file. -
Implement
TestConnection()
in ConnectionTestHandler.cs.