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.