Skip to content

Creating an API Client

Creating an API Client

Using a client we can create a dynamic connection between the Connector and your API. In this step we’ll build an HTTP client so the data objects provided to the Connector will be from your system and not hard coded.

Key considerations

The data your client gets from the API should look similar to the data objects needed for your connector, but if the data does not match precisely, we can map the API data to the exact data object in the Data Reader.

Your client code will be executed by the App Xchange platform and as such will need to have access to your API.

CLI Command Options

Before running the CLI command to create a client, you have to know about the existing options it uses.

  • --type (aliases: -t)

    • It indicates the type of client to create. HTTP is used by default
    • Required: False
  • --auth-type (aliases: --auth)

    • It indicates the type of authentication to use. Possible values include ApiKey and Basic. If no auth-type is provided, the value will be None.
    • Required: False

If your client needs Basic or ApiKey authentication, you can use the --auth-type option with the specific authentication type. If you don't need authentication, or you need a customized one that's not covered by the CLI, you can run the command without the --auth-type option. The code is not designed to run both with and without --auth-type, so, choose one way of generating your client.

The CLI command with no specific Authentication Type:

xchange client new --type Http

The CLI command with specific Authentication Type:

xchange client new --type Http --auth-type {YOUR_AUTH_TYPE}

While in the root of your Connector project run the CLI command to create a new client. Depending of whether an --auth-type was specified or not, different files will be created (the files for each case will be explained in detail shortly). However, in both, a new Client directory and a client named ApiClient.cs will be created. This will also add the client to your dependency injection found in the ConnectorRegistration file.

The client will by default come with a retry policy. Policies can be removed or added as needed.

Clients created with no specific Authentication Type

Authenticating your transactions

One of the preferred methods for authentication in APIs is OAuth client credentials flow, so we will use it as a base for the following example. You can deploy a custom solution for authentication if this OAuth flow is not available for you.

Using OAuth client credentials

If you are using OAuth client credentials flow, you can use the existing ConnectorRegistrationConfig file in your client to establish your authentication. A class called ApiDetails will be generated by the CLI, in which you can add the properties needed to authenticate your client.

Example of a Connector registration config. ConnectorRegistrationConfig.cs
/// <summary>
/// Contains all configuration values necessary for execution of the connector, that are configurable by a connector implementation.
/// </summary>
public class ConnectorRegistrationConfig
{
    [JsonPropertyName("apiDetails")]
    public ApiDetails ApiDetails { get; set; } = new();
}

/// <summary>
/// Contains all of the settings needed for connecting to the API.
/// </summary>
public class ApiDetails
{
    [JsonPropertyName("baseUrl")]
    public string BaseUrl { get; set; } = string.Empty;

    [JsonPropertyName("clientId")]
    public string ClientId { get; set; } = string.Empty;

    [JsonPropertyName("clientSecret")]
    public string ClientSecret { get; set; } = string.Empty;

    [JsonPropertyName("tokenUrl")]
    public string TokenUrl { get; set; } = string.Empty;
}
Example of adding BaseUrl to your client. Connector/Client/ApiClient.cs
public class ApiClient
{
    private readonly HttpClient _httpClient;

    public ApiClient(HttpClient httpClient, string baseUrl)
    {
        _httpClient = httpClient;
        _httpClient.BaseAddress = baseUrl;
    }
}

To establish authentication, create a DelegatingHandler with the authentication logic you need. Create its constructor to take a ConnectorRegistrationConfig as an argument so the authentication properties created can be used.

Example of a delegating handler for your client. Connector/Client/OAuthTokenHandler.cs
public class OAuthTokenHandler : DelegatingHandler
{
    private readonly ApiDetails _apiDetails { get; init; }
    private string _token;

    public OAuthTokenHandler(ConnectorRegistrationConfig registrationConfig)
    {
        _apiDetails = registrationConfig.ApiDetails;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 
    {
        await InitializeToken(request, cancellationToken);

        var response = await base.SendAsync(request, cancellationToken);
        if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
        {
            await InitializeToken(request, cancellationToken, true);
            return await base.SendAsync(request, cancellationToken);
        }

        return response;
    }

    private async Task InitializeToken(HttpRequestMessage request, CancellationToken cancellationToken, bool forceRefresh = false)
    {
        if (_token != null && !forceRefresh) return;
        var tokenResponse = await base.SendAsync(GetRefreshTokenRequest(), cancellationToken);
        tokenResponse.EnsureSuccessStatusCode();
        var response = await tokenResponse.Content.ReadFromJsonAsync<JsonNode>(cancellationToken);
        _token = response["accessToken"]!.ToString();
    }

    private HttpRequestMessage GetRefreshTokenRequest()
    {
        return new HttpRequestMessage(HttpMethod.Post, _apiDetails.TokenUrl)
        {
            new[] {
                new KeyValuePair<string, string>("grant_type", "client_credentials"),
                new KeyValuePair<string, string>("client_id", _apiDetails.ClientId ),
                new KeyValuePair<string, string>("client_secret", _apiDetails.ClientSecret )
            }
        }
    }
}
Example of adding API token handler to your dependency injection and API client in ConnectorRegistration.cs. (The highlighted lines are the changes)
public class ConnectorRegistration : IConnectorRegistration<ConnectorRegistrationConfig>
{
    public void ConfigureServices(IServiceCollection serviceCollection, IHostContext hostContext)
    {
        var connectorRegistrationConfig = JsonSerializer.Deserialize<ConnectorRegistrationConfig>(hostContext.GetSystemConfig().Configuration);
        serviceCollection.AddSingleton(connectorRegistrationConfig!);
        serviceCollection.AddTransient<RetryPolicyHandler>();
        serviceCollection.AddTransient<OAuthTokenHandler>();
        // Configure ApiClient
        serviceCollection
            .AddHttpClient<ApiClient>(client =>
            {
                client.BaseAddress = new Uri(connectorRegistrationConfig.ApiDetails.BaseUrl);
            })
            .AddHttpMessageHandler<OAuthTokenHandler>()
            .AddHttpMessageHandler<RetryPolicyHandler>();
    }
}

At this point you already have done the initial setup for your client, you will be returning to the client to add the necessary methods to read and write data between the App Xchange platform and your system.

OpenAPI spec client generation

If your HTTP API has an OpenAPI Spec you can generate a client through the use of tooling such as NSwag.

Note About Generation

Ensure that the generated code is not using Newtonsoft for its JSON library. For proper support with your Connector's deserialization and schema generation you should ensure that your client is generated using System.Text.Json library instead.

If you use another tool to generate a client, bring it over to your Connector codebase's Client folder and then add it to your Connector's dependency injection container. The constructor of your client may be very different based on the library and options used in its generation. The constructor can be tweaked or what the constructor needs can be added to your Connector's container to properly initiate the API client.

You may find it easier to only have your generated client's constructor to just be an HttpClient and then leverage HTTP handlers to handle the authentication. Below is an example of handling that for your dependency injection container.

Example of adding a custom API client to your dependency injection. ConnectorRegistration.cs
public class ConnectorRegistration : IConnectorRegistration<ConnectorRegistrationConfig>
{
    public void ConfigureServices(IServiceCollection serviceCollection, IHostContext hostContext)
    {
        var connectorRegistrationConfig = JsonSerializer.Deserialize<ConnectorRegistrationConfig>(hostContext.GetSystemConfig().Configuration);
        serviceCollection.AddSingleton(connectorRegistrationConfig!);
        serviceCollection.AddTransient<RetryPolicyHandler>();
        serviceCollection.AddTransient<OAuthTokenHandler>();

        // Configure ApiClient
        serviceCollection
            .AddHttpClient<ApiClient>(client =>
            {
                client.BaseAddress = new Uri(connectorRegistrationConfig.ApiDetails.BaseUrl);
            })
            .AddHttpMessageHandler<OAuthTokenHandler>()
            .AddHttpMessageHandler<RetryPolicyHandler>();
    }
}

Clients created with Authentication Types

Defining Connections for Authentication

When a connector builder using the App Xchange CLI tools wants to create a client with authentications, the connector will need the proper credentials to create valid requests, as well as a handler that adds the authentication headers the requests may need.

All the the configuration needed to get an authenticated requests will be contained in an AuthType class, and, depending of the type of authentication, its respective handler will take care of authenticating the requests.

About AuthType classes

The AuthType class contains what the client will use in order to get authenticated requests. The AuthType class will contain the secrets, as well as the Connection Environment and Base Url the secret belongs to.

Depending on the type of authentication the API used in the client needs, a different AuthType class will be created.

Currently, there are two types of authentication whose creation is supported: ApiKeyAuth and BasicAuth. Each implements an interface: IApiKeyAuth and IBasicAuth respectively. The IApiKeyAuth needs properties ApiKey and BaseUrl. The IBasicAuth needs properties Username, Password and BaseUrl.

When the AuthType class is created, it includes a property called ConnectionEnvironment. This property will have the configuration of the possible environments. Depending of the ConnectionEnvironment, the BaseUrl will be set.

On creation, the BaseUrl will be filled with placeholders. Be sure to remove those and use your actual URLs.

Example of an AuthType class of ApiKeyAuth
[ConnectionDefinition(title: "ApiKeyAuth", description: "")]
public class ApiKeyAuth : IApiKeyAuth
{
    [ConnectionProperty(title: "ApiKey", description: "", isRequired: true, isSensitive: true)]
    public string ApiKey { get; init; } = string.Empty;

    [ConnectionProperty(title: "Connection Environment", description: "", isRequired: true, isSensitive: false)]
    public ConnectionEnvironmentApiKeyAuth ConnectionEnvironment { get; set; } = ConnectionEnvironmentApiKeyAuth.Unknown;

    public string BaseUrl
    {
        get
        {
            switch (ConnectionEnvironment)
            {
                case ConnectionEnvironmentApiKeyAuth.Production:
                    return "http://prod.example.com";
                case ConnectionEnvironmentApiKeyAuth.Test:
                    return "https://test.example.com";
                default:
                    throw new Exception("No base url was set.");
            }
        }
    }
}

public enum ConnectionEnvironmentApiKeyAuth
{
    Unknown = 0,
    Production = 1,
    Test = 2
}

About AuthType Handlers

The AuthType Handler decides how the requests should behave. Depending on the type of authentication needed, it adds the headers with its values.

The AuthType Handler exist in the Client folder, and connector builders can customize it depending of the API they're working with.

Example of an AuthType handling of type ApiKey
public class ApiKeyAuthHandler : DelegatingHandler
{
    private readonly IApiKeyAuth _apiKeyAuth;

    public ApiKeyAuthHandler(IApiKeyAuth apiKeyAuth)
    {
        _apiKeyAuth = apiKeyAuth;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Remove("X-Api-Key");
        request.Headers.Add("X-Api-Key", _apiKeyAuth.ApiKey);

        return await base.SendAsync(request, cancellationToken);
    }
}

As seen previously with clients created with no specific Authentication Type, a class called ApiDetails is created in ConnectorRegistrationConfig.cs. This will not be the case if we're using authentication types, since the AuthType class will handle the credentials needed.

While the ConnectorRegistrationConfig class will remain empty, it can still be used to add different kinds of configuration, like filters for the DataReader.

Example of ConnectorRegistrationConfig with filters for a Data Reader about Recipes
public class ConnectorRegistrationConfig
{
    [JsonPropertyName("cuisine")]
    [Description("Options are: mexican, italian, chinese, indian")]
    public string Cuisine { get; set; } = string.Empty;

    [JsonPropertyName("diet")]
    [Description("Options are: vegan, ketogenic, pescetarian")]
    public string Diet { get; set; } = string.Empty;

    [JsonPropertyName("type")]
    [Description("Options are: breakfast, appetizer, dessert, salad")]
    public string Type { get; set; } = string.Empty;
}

Working with connections

If you created a client with an authentication type, you will have two new folders in your repository, named Client (with the ApiClient and the AuthType handler) and Connections (with the AuthType class and a ConnectionTestHandler class). It will also implement IConfigureConnectorApiClient in the ConnectorRegistration file, where the injections needed for the client will be added.

Example of ApiClient
public class ApiClient
{
    private readonly HttpClient _httpClient;

    public ApiClient (HttpClient httpClient, string baseUrl)
    {
        _httpClient = httpClient;
        _httpClient.BaseAddress = new System.Uri(baseUrl);
    }

    public async Task<ApiResponse<JsonDocument>> GetExampleRecord(CancellationToken cancellationToken = default)
    {
        var response = await _httpClient
            .GetAsync("example", cancellationToken: cancellationToken)
            .ConfigureAwait(false);

        return new ApiResponse<JsonDocument>
        {
            IsSuccessful = response.IsSuccessStatusCode,
            StatusCode = (int)response.StatusCode,
            Result = response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<JsonDocument>(cancellationToken: cancellationToken) : default,
            RawResult = await response.Content.ReadAsStreamAsync(cancellationToken: cancellationToken)
        };
    }
}
Example of ConfigureConnectorApiClient and RegisterConnectionTestHandler
public void ConfigureConnectorApiClient(IServiceCollection serviceCollection, IHostConnectionContext hostConnectionContext)
{
    var activeConnection = hostConnectionContext.GetConnection();
    serviceCollection.ResolveServices(activeConnection);
}

public void RegisterConnectionTestHandler(IServiceCollection serviceCollection)
{
    serviceCollection.AddSingleton<IConnectionTestHandler, ConnectionTestHandler>();
}
RegisterConnectionTestHandler applies to version 1.2.3 or above

Another file that will be created is the ClientHelper, which will help resolve what we need in case there's multiple connection definitions. Depending on the definitionKey of the active connection, it will inject the dependencies to support the AuthType in use. It will also instantiate the ApiClient, using the configuration of the connector and the Base Url of the active connection.

Example of a ClientHelper
public static class ClientHelper
{
    public static class AuthTypeKeyEnums
    {
        public const string ApiKeyAuth = "apiKeyAuth";
    }

    public static void ResolveServices(this IServiceCollection serviceCollection, ConnectionContainer activeConnection)
    {
        serviceCollection.AddTransient<RetryPolicyHandler>();
        var options = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };
        switch (activeConnection.DefinitionKey)
        {
            case AuthTypeKeyEnums.ApiKeyAuth:
                var configApiKeyAuth = JsonSerializer.Deserialize<ApiKeyAuth>(activeConnection.Configuration, options);
                serviceCollection.AddSingleton<IApiKeyAuth>(configApiKeyAuth);
                serviceCollection.AddTransient<RetryPolicyHandler>();
                serviceCollection.AddTransient<ApiKeyAuthHandler>();
                serviceCollection.AddHttpClient<ApiClient, ApiClient>(client => new ApiClient(client, configApiKeyAuth.BaseUrl)).AddHttpMessageHandler<ApiKeyAuthHandler>().AddHttpMessageHandler<RetryPolicyHandler>();
                break;
            default:
                throw new Exception($"Unable to find services for definition key {activeConnection.DefinitionKey}");
        }
    }
}

If another AuthType class such as BasicAuth is created, the case statement with the dependency injections needed will be added to the file.

Testing a Connection

NOTE: Applies to version 1.2.3-beta.2 or above

When a connection is created, we need a way to test the connection to see if it works before we decide to use it. For that purpose, the CLI will also generate in the Connections folder a class called that implements IConnectionTestHandler called ConnectionTestHandler. This class will implement a method called TestConnection, in which a test process for the connection should be implemented.

The ConnectionTestHandler generated by the CLI will look like this.

Template of a ConnectionTestHandler
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()
    {
        // Make a call to your API/system to obtain the connection test result.

        //var response = await _apiClient.GetTestConnection(
        //    cancellationToken: cancellationToken)
        //    .ConfigureAwait(false);

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

The method TestConnection() should make a call to the ApiClient and it could go from testing a simple GET endpoint to something more complicated; the point of the test is that it ensures that when a DataReader or an ActionHandler is called, the connection is sufficient. Depending of the response of the ApiClient call, Success should indicate if the test as successful, the StatusCode of the response should be set, and, if the test is unsuccessful, a Message should be set in order to give more information.

Example of an implemented ConnectionTestHandler
public async Task<TestConnectionResult> TestConnection()
{
    var response = await _apiClient.GetConnectionTest();

    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
            };
    }
}

To test the connection locally, use the profile Test Connection Local Development in the launchSettings.json file.

Command

dotnet run --launch-profile "Test Connection Local Development"

This or any profile can be run via the IDE of your choosing as well. Feel free to use which one you are comfortable with.

Expected Results on Success

Local development is currently configured. You'll need to use the Xchange.Connector.SDK.Test.Local library
[09:55:33 INF] Connector (1.2.3.0) runtime started in 1.5096147 seconds
[09:55:36 INF] The connection test was successful
[09:55:36 INF] Service runner shutting down...

In summary what is being done is:

  1. Given an AuthType, a class and a handler will be created and have its dependencies injected.
  2. The ApiClient will receive the BaseUrl from the AuthType class configured.
  3. Since multiple Auth Types are supported, the ClientHelper will help resolve the dependencies needed for the active connection.
  4. A handler for testing a connection is created to ensure the connector can perform as needed with the given connection.