Alternative approach to HttpClient in Azure Functions 2.0 - bringing in HttpClientFactory


This post no longer describes the best approach. You can read the revisited version here.


HttpClient is not as straightforward to use as it may seem. A lot has been written about improper instantiation antipattern. Long story short, you shouldn't be creating a new instance of HttpClient whenever you need it, you should be reusing them. In order to avoid this antipattern, many technologies provide guidance for efficient usage of HttpClient, Azure Functions are no different. The current recommendation is to use a static client, something like the code below.

public static class PeriodicHealthCheckFunction
{
    private static HttpClient _httpClient = new HttpClient();

    [FunctionName("PeriodicHealthCheckFunction")]
    public static async Task Run(
        [TimerTrigger("0 */5 * * * *")]TimerInfo healthCheckTimer,
        ILogger log)
    {
        string status = await _httpClient.GetStringAsync("https://localhost:5001/healthcheck");

        log.LogInformation($"Health check performed at: {DateTime.UtcNow} | Status: {status}");
    }
}

This approach avoids the socket exhaustion problem and brings performance benefits, but creates another problem. Static HttpClient doesn't respect DNS changes. Sometimes this might be irrelevant, but sometimes it might lead to serious issues. Is there a better approach?

HttpClientFactory

One of the areas where both sides of HttpClient instantiation problem are often encountered is ASP.NET Core. Two most common use cases for ASP.NET Core are web applications and microservices. The socket exhaustion problem can be deadly for those kinds of applications and they often suffer from DNS changes not being respected (for example when external dependencies are using blue-green deployments). This is why The ASP.NET Team decided to create an opinionated factory for creating HttpClient instances. Main responsibility of that factory is managing the lifetime of HttpClientMessageHandler in a way which avoids the issues. It has been released with .NET Core 2.1 as Microsoft.Extensions.Http package and depends only on DI, logging and options primitives.

HttpClientFactory and Azure Functions 2.0

Azure Functions 2.0 runs on .NET Core 2.1 and already internally uses DI, logging and options primitives. This means that all HttpClientFactory dependencies are fulfilled and using it should be simple. A good idea might be creating an extension which will allow for binding HttpClient instance to function parameter decorated with an attribute.

[Binding]
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue)]
public sealed class HttpClientFactoryAttribute : Attribute
{ }

If you have read my previous post on Azure Functions 2.0 extensibility, you already know that changing such attribute into input binding requires IExtensionConfigProvider implementation. In this case, the implementation needs to take IHttpClientFactory from DI and create a binding rule which will map HttpClientFactoryAttribute with input parameters of type HttpClient. The builder will simply call IHttpClientFactory.CreateClient to obtain an instance.

[Extension("HttpClientFactory")]
internal class HttpClientFactoryExtensionConfigProvider : IExtensionConfigProvider
{
    private readonly IHttpClientFactory _httpClientFactory;

    public HttpClientFactoryExtensionConfigProvider(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public void Initialize(ExtensionConfigContext context)
    {
        var bindingAttributeBindingRule = context.AddBindingRule<HttpClientFactoryAttribute>();
        bindingAttributeBindingRule.BindToInput<HttpClient>((httpClientFactoryAttribute) =>
        {
            return _httpClientFactory.CreateClient();
        });
    }
}

Of course, the IExtensionConfigProvider and IHttpClientFactory need to be registered with Azure Functions runtime. This is exactly what below implementation of IWebJobsStartup does (it assumes that Microsoft.Extensions.Http package has been added).

[assembly: WebJobsStartup(typeof(HttpClientFactoryWebJobsStartup))]

public class HttpClientFactoryWebJobsStartup : IWebJobsStartup
{
    public void Configure(IWebJobsBuilder builder)
    {
        builder.AddExtension<HttpClientFactoryExtensionConfigProvider>();

        builder.Services.AddHttpClient();
        builder.Services.Configure<HttpClientFactoryOptions>(options => options.SuppressHandlerScope = true);
    }
}

That's all. Now the original function can be refactored to use HttpClient instance created by the factory.

public static class PeriodicHealthCheckFunction
{
    [FunctionName("PeriodicHealthCheckFunction")]
    public static async Task Run(
        [TimerTrigger("0 */5 * * * *")]TimerInfo healthCheckTimer,
        [HttpClientFactory]HttpClient httpClient,
        ILogger log)
    {
        string status = await httpClient.GetStringAsync("https://localhost:5001/healthcheck");

        log.LogInformation($"Health check performed at: {DateTime.UtcNow} | Status: {status}");
    }
}

This is much better. The function is now using factory which removes the potential issues of a static instance. At the same time, testability has been improved because HttpClient is now a dependency.

Possibilities

The above implementation does the simplest possible thing - uses HttpClientFactory to manage a standard instance of HttpClient. That's not all what HttpClientFactory offers. It allows pre-configuring clients and supports named clients. It even comes with ready to use integration with Polly which makes it really simple to add resilience and transient-fault handling capabilities. All of that requires only a few changes to the code above. You don't have to start from scratch, you can find that code on GitHub.