I have a durable orchestration for an ETL job that downloads data from an external API. The token for the external API is stored in an Azure Key Vault. The orchestration triggers several suborchestrations with many activity functions which need to add the auth token to the request headers.
What is the best practice for using an httpClient with a DelegatingHandler, if I want to avoid unnecessarily calling the Azure Key Vault to get the same token?
I am injecting an IHttpClientFactory into the orchestration class and then CreateClient in each activity function.
Should I just create one http client in the orchestrator function and pass it as a parameter to each activity function, thus implementing a delegating handler to append the auth header?
Here is sample orchestration code. In my implementation I have many activity functions and create an httpclient in each of them.
public class ETLOrchestration
{
private readonly IHttpClientFactory _factory;
public ETLOrchestration(IHttpClientFactory factory)
{
_factory = factory;
}
[Function(nameof(ETLOrchestration))]
public static async Task RunOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
ILogger logger = context.CreateReplaySafeLogger(nameof(ETLOrchestration));
string token = await context.CallActivityAsync<string>(nameof(GetAccesToken));
if (string.IsNullOrEmpty(token))
{
throw new Exception("No access token found for ETL in the KeyVault.");
}
logger.LogInformation("Access token retrieved from KeyVault.");
// Should I be creating the http client here and passing it instead of the token?
await context.CallActivityAsync(nameof(ETLActivity), token);
}
[Function("ETLOrchestration_HttpStart")]
public static async Task<HttpResponseData> HttpStart(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
[DurableClient] DurableTaskClient client,
FunctionContext executionContext)
{
ILogger logger = executionContext.GetLogger("ETLOrchestration_HttpStart");
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(ETLOrchestration));
return await client.CreateCheckStatusResponseAsync(req, instanceId);
}
[Function(nameof(GetAccesToken))]
public async Task<string> GetAccesToken([ActivityTrigger] FunctionContext executionContext)
{
ILogger logger = executionContext.GetLogger(nameof(GetAccesToken));
string accessToken = string.Empty;
// Use the KeyVault to get the access token
string kvUri = "https://key-vault";
var kv_client = new SecretClient(new Uri(kvUri), new DefaultAzureCredential());
try
{
Response<KeyVaultSecret> accessTokenTask = await kv_client.GetSecretAsync($"ETL-secret");
return accessTokenTask.Value.Value;
}
catch (RequestFailedException)
{
logger.LogCritical("No access token found for ETL in the KeyVault.");
return string.Empty;
}
}
[Function(nameof(ETLActivity))]
public async Task ETLActivity([ActivityTrigger] string accessToken, FunctionContext executionContext)
{
ILogger logger = executionContext.GetLogger(nameof(ETLActivity));
HttpClient http_client = _factory.CreateClient("SomeAPIClient");
http_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var httpResponse = await http_client.GetAsync($"some-api/data");
//...
}
}
And in Program.cs I have the named http client:
services.AddHttpClient("SomeAPIClient", http_client =>
{
http_client.BaseAddress = new Uri(Environment.GetEnvironmentVariable("BaseUrlSomeAPI");
});
