Setup prior to version 1.4

Nexus Async Caller SchemaVersion 0

The configuration for Nexus AsyncCaller has implicit SchemaVersion 0, where there is no support for multiple queues and the queue name is explicitly given.

Components

To get Async Caller going you need

  • An Azure Storage account
  • An Azure Function App
  • Nexus configuration

Flow

In version 1 of Async Caller, the chain of requests looks like this:
Async Caller chain of requests

  1. An adapter or other service provider uses the business api that exposes the async functionality
  2. The sending system (Nexus Business Events or Business API) asks Nexus Async Caller to handle an asynchronous request
  3. Async Caller puts the request on a storage queue in the customer's Azure environment
  4. A function in the customer's Azure environment is triggered and asks Async Caller to actually make the request
  5. Async Caller makes the request to the receiving system

Nexus configuration

Use this configuration variant:

{
    "ConnectionString": "...",
    "QueueName": "..."
}

or with explicit SchemaVersion:

{
    "SchemaVersion": 0,
    "ConnectionString": "...",
    "QueueName": "..."
}

See Configure Async Caller on how to setup Async Caller in the customer's environment.

Azure function app

Create an Azure Function App which has an "AsyncCaller" function in it, listening to the QueueName in the AC settings. It could look something like

public static class AsyncCaller
{
    [FunctionName("AsyncCaller")]
    public static async Task Run([QueueTrigger("async-request-queue", Connection = "AzureWebJobsStorage")] string queueItem, ILogger log, ExecutionContext context)
    {
        await AsyncCallsClient.HandleDistribute(queueItem, log, context);
    }
}

The AsyncCallsClient will contain the code for accessing Async Caller; which could look something like this (with logging stripped away and support for multi-tenant):

using Nexus.Link.Authentication.Sdk;
using Nexus.Link.Libraries.Core.Application;
using Nexus.Link.Libraries.Core.MultiTenant.Model;
using Nexus.Link.Libraries.Core.Platform.Authentication;
//...

public class AsyncCallsClient
{
    private static readonly HttpClient HttpClient = new HttpClient() { Timeout = TimeSpan.FromSeconds(120) };

    public static async Task HandleDistribute(string queueItem, ILogger log, ExecutionContext context)
    {
        try
        {
            var asyncCallerRequest = JsonConvert.DeserializeObject<AsyncCallsRequest>(queueItem);
            var environment = asyncCallerRequest.Environment;
            var organization = asyncCallerRequest.Organization;
            var relativeUrl = $"api/v1/{organization}/{environment}/AsyncCalls/Distribute";
            var postUrl = $"{ConfigurationHelper.GetSetting("AsyncCaller.PostUrl", context)}/{relativeUrl}";

            var request = new HttpRequestMessage(HttpMethod.Post, postUrl)
            {
                Content = new StringContent(myQueueItem, System.Text.Encoding.UTF8, "application/json")
            };
            await AuthorizationHelper.AddAuthorization(request, log, context, organization, environment, "asyncCalls");

            var response = await HttpClient.SendAsync(request);
            if (!response.IsSuccessStatusCode)
            {
                var content = await response.Content.ReadAsStringAsync();
                var error = $"Bad response from Dispatcher. PostUrl: {postUrl}" +
                             " | ResponseCode: {response.StatusCode} | Content: {content}" +
                             " | Message {queueItem}";
                if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
                {
                    error += $" | Token: {request.Headers.Authorization}";
                }
                log.LogError(error);
                throw new Exception(error);
            }
        }
        catch (Exception e)
        {
            // Throwing error will put back item on queue. There will be 5 tries, then put on poison queue.
            log.LogError($"Error: {e.Message} {e.InnerException?.Message}");
            throw;
        }
    }
}

where AuthorizationHelper may help out with

using Nexus.Link.Authentication.Sdk;
using Nexus.Link.Libraries.Core.Application;
using Nexus.Link.Libraries.Core.MultiTenant.Model;
using Nexus.Link.Libraries.Core.Platform.Authentication;
using FulcrumApplicationHelper = Nexus.Link.Libraries.Web.AspNet.Application.FulcrumApplicationHelper;
// ...

public class AuthorizationHelper
{
    private static readonly MemoryCache MemoryCache = new MemoryCache(new MemoryCacheOptions());
    private const int CacheHours = 24;

    public static async Task AddAuthorization(HttpRequestMessage request, ILogger log, ExecutionContext context,
                                              string organization, string environment, string function)
    {
        var cacheKey = $"{function}|token|{organization}|{environment}";
        var token = MemoryCache.Get<AuthenticationToken>(cacheKey);
        if (token == null)
        {
            var tenant = new Tenant(organization, environment);
            FulcrumApplicationHelper.WebBasicSetup($"function-apps-{environment}", tenant, RunTimeLevelEnum.Production);

            var clientId = ConfigurationHelper.GetSetting($"Authentication.ClientId.{environment}", context) ??
                           ConfigurationHelper.GetSetting($"Authentication.ClientId", context);
            var clientSecret = ConfigurationHelper.GetSetting($"Authentication.ClientSecret.{environment}", context) ??
                               ConfigurationHelper.GetSetting($"Authentication.ClientSecret", context);

            var tokenCredentials = new AuthenticationCredentials
            {
                ClientId = clientId,
                ClientSecret = clientSecret
            };

            var authServiceUrl = ConfigurationHelper.GetSetting($"Authentication.Url", context);
            var authenticationManager = new NexusAuthenticationManager(tenant, authServiceUrl);

            token = await authenticationManager
                .GetJwtTokenAsync(tokenCredentials, TimeSpan.FromHours(1), TimeSpan.FromHours(CacheHours));
            MemoryCache.Set(cacheKey, token, DateTimeOffset.Now.AddMinutes(CacheHours * 60 - 30));
            log.LogInformation($"Fetched a new token with cache key '{cacheKey}'");
        }

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