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.

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, Basic, Custom, OAuth2CodeFlow, OAuth2ClientCredentials and OAuth2Password.
    • Required: True

If your client needs ApiKey, Basic, OAuth2CodeFlow, OAuth2ClientCredentials or OAuth2Password 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.

The CLI command:

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 on which --auth-type was specified, different files will be created (the files for each case will be explained in detail shortly). 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 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 configuration needed to get authenticated requests will be contained in an AuthType class, and, depending on 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 6 types of authentication whose creation is supported: ApiKeyAuth, BasicAuth, CustomAuth, OAuth2CodeFlow, OAuth2ClientCredentials and OAuth2Password. In this page, we will focus on the first three, which implement an interface each: IApiKeyAuth, IBasicAuth and ICustomAuth respectively.

If you need to use any OAuth2 authentication, see Creating an API Client with OAuth2.

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

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

Connection definition classes are decorated with an attribute called ConnectionProperty. This attribute is necessary for the property to show up in the extracted metadata of the connector.

Key considerations

Currently, only properties of type string are supported.

BasicAuth class

The IBasicAuth needs properties Username, Password and BaseUrl.

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

    [ConnectionProperty(title: "Password", description: "", isRequired: true, isSensitive: true)]
    public string Password { get; init; } = string.Empty;

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

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

public enum ConnectionEnvironmentBasicAuth
{
    Unknown = 0,
    Production = 1,
    Test = 2
}
ApiKeyAuth class

The IApiKeyAuth needs properties ApiKey and BaseUrl.

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
}
CustomAuth class

The ICustomAuth only needs property BaseUrl, and it's meant to be expanded by the properties the API needs.

Example of an AuthType class of CustomAuth
[ConnectionDefinition(title: "CustomAuth", description: "")]
public class CustomAuth : ICustomAuth
{
    //Create your own properties here like this:
    //[ConnectionProperty(title: "Custom Header", description: "", isRequired: true, isSensitive: false)]
    //public string CustomHeader { 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 "http://prod.example.com";
                case ConnectionEnvironmentCustomAuth.Test:
                    return "http://test.example.com";
                default:
                    throw new Exception("No base url was set.");
            }
        }
    }
}

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

About AuthType Handlers

To establish authentication in a transaction, an AuthType Handler will be created with the authentication logic you need. The AuthType Handler is a DelegatingHandler that 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.

ApiKeyAuth handler
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);
    }
}
BasicAuth handler
Example of an AuthType handling of type Basic
public class BasicAuthHandler : DelegatingHandler
{
    private readonly IBasicAuth _basicAuth;

    public BasicAuthHandler(IBasicAuth basicAuth)
    {
        _basicAuth = basicAuth;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Remove("Authorization");
        var byteArray = Encoding
            .ASCII
            .GetBytes($"{_basicAuth.Username}:{_basicAuth.Password}");
        request.Headers.Add("Authorization", $"Basic {Convert.ToBase64String(byteArray)}");

        return await base.SendAsync(request, cancellationToken);
    }
}
CustomAuth handler
Template of an AuthType handling of type Custom
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("X-Custom-Header");
        //request.Headers.Add("X-Custom-Header, _customAuth.CustomHeader");

        return await base.SendAsync(request, cancellationToken);
    }
}
Example of an implemented AuthType handling of type Custom that uses a Username, Password and a User Agent in exchange for a token
public class CustomAuthHandler : DelegatingHandler
{
    private readonly CustomAuth _customAuth;
    private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
    private readonly ILogger<CustomAuthHandler> _logger;
    private string? _token;

    public CustomAuthHandler(CustomAuth customAuth, ILogger<CustomAuthHandler> logger)
    {
        _customAuth = customAuth;
        _logger = logger;
    }

    /// <summary>
    /// Sends an HTTP request using a Username, Password and a User Agent to get a token authentication.
    /// </summary>
    /// <param name="request">The HTTP request message.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>The HTTP response message.</returns>
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        await GetToken(request, cancellationToken);
        var response = await base.SendAsync(request, cancellationToken);

        //If the token that exists has expired, the Status Code will be 401: "Unauthorized"
        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            //The method GetToken will either get or refresh the token in order to retry to send the request.
            //If the retry fails for a second time, the response will be returned as "Unauthorized".
            await GetToken(request, cancellationToken, true);
            return await base.SendAsync(request, cancellationToken);
        }

        return response;
    }

    /// <summary>
    /// Gets or refreshes the token and adds it to the request headers. The use of the semaphore will prevent multiple concurrent logins.
    /// 1. If there's no token and "refresh" is false, it means there's no token yet and it will try to authenticate.
    /// 2. If there's a token and "refresh" is false, it will only add the same token to the headers.
    /// 3. If there's a token and "refresh" is true, it will try to authenticate in order to refresh the token.
    /// </summary>
    /// <param name="request">The HTTP request message.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <param name="refresh">Indicates whether to refresh the token.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    private async Task GetToken(HttpRequestMessage request, CancellationToken cancellationToken, bool refresh = false)
    {
        if (string.IsNullOrEmpty(_token) || refresh)
        {
            try
            {
                //Stores the current token to reference later and check if it was already refreshed by another thread.
                var previousToken = _token;

                // Uses a semaphore to prevent concurrent logins -- this makes it so only one thread can enter this block at a time
                await _tokenSemaphore.WaitAsync(cancellationToken);

                // If the previous and current token no longer match after entering this block then another thread has already logged in, and there is no need for this thread to continue the log in process
                if (previousToken != _token)
                {
                    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
                    return;
                }

                var response = await base.SendAsync(GetTokenRequest(), cancellationToken);

                if (response.IsSuccessStatusCode)
                {
                    var tokenRespose = await response.Content.ReadFromJsonAsync<JsonDocument>(cancellationToken: cancellationToken);
                    if (tokenRespose == null)
                    {
                        _logger.LogError("Error in JSON response while retrieving token");
                        throw new JsonException("Error deserializing authentication JSON response while retrieving token");
                    }

                    _token = tokenRespose.RootElement.GetProperty("access_token").GetString();
                    if (_token == null)
                    {
                        _logger.LogError($"Response did not contain an 'access_token' property. Token Response Content: {tokenRespose.RootElement.ToString()}");
                        throw new InvalidOperationException("Response did not contain an 'access_token' property.");
                    }
                }
                else
                {
                    _token = null;
                    _logger.LogError($"Unsuccessful response while retrieving token. Please, verify your credentials. Status Code: {response.StatusCode}");
                }
            }
            finally
            {
                _tokenSemaphore.Release();
            }
        }

        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
    }

    public HttpRequestMessage GetTokenRequest()
    {
        var tokenRequest = new HttpRequestMessage(HttpMethod.Post, _customAuth.BaseUrl)
        {
            Content = new FormUrlEncodedContent(new[]
            {
                        new KeyValuePair<string, string>("username", _customAuth.Username),
                        new KeyValuePair<string, string>("password", _customAuth.Password),
                }),
            Headers =
                {
                    { "User-Agent", $"{_customAuth.UserAgent}" }
                }
        };

        return tokenRequest;
    }
}

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 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.

Example of an implemented ConnectionTestHandler
public async Task<TestConnectionResult> TestConnection()
{
    // Make a call to your API/system to obtain the connection test result.
    var response = await _apiClient.TestConnection();

    // Depending on the response, make your own specific messages.
    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
            };
    }
}

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 purpose of the _apiClient.TestConnection() method is to ensure the connector can make authenticated calls to the API. 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.

For running your connection test locally, follow the next guide: Testing A Connector Locally.

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.