Alternative approach to HttpClient in Azure Functions 2.0 revisited - move to dependency injection
Recently I've received a question regarding my previous post on using HttpClient
with Azure Functions. It made me realize that I'm long overdue with a follow-up because that's not the best approach anymore.
Moving to Dependency Injection
In my previous approach, I was creating an extension to leverage internally available dependency injection for the purpose of using HttpClientFactory
. Back then it was the only way to use dependency injection in Azure Functions, but that's no longer the case. A few months ago Azure Functions has been given the first-class support for dependency injection. This means that extension is no longer needed, HttpClientFactory
can be registered and used directly from the functions project level.
Registering services with Azure Functions dependency injection requires a startup class. This class must inherit from FunctionsStartup
, which allows for overriding the Configure
method. The Configure
method will be passed an instance of IFunctionsHostBuilder
by the host, with which we can register HttpClientFactory
. In order for the host to find the startup class, there must be an assembly level FunctionsStartupAttribute
pointing to it.
[assembly: FunctionsStartup(typeof(Startup))]
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddHttpClient();
}
}
Consuming a service available through dependency injection requires instance-based approach to functions implementation. This is because Azure Functions supports constructor-based dependency injection. The original version of this post suggested having HttpClient
as a constructor parameter, but as pointed out in comments it doesn't work. What needs to be injected is the registered HttpClientFactory
, which later can be used to create an instance of HttpClient
.
public class PeriodicHealthCheckFunction
{
private readonly IHttpClientFactory _httpClientFactory;
public PeriodicHealthCheckFunction(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
}
[FunctionName("PeriodicHealthCheckFunction")]
public async Task Run(
[TimerTrigger("0 */1 * * * *")]TimerInfo healthCheckTimer,
ILogger log)
{
HttpClient httpClient = _httpClientFactory.CreateClient();
string status = await httpClient.GetStringAsync("https://localhost:5001/healthcheck");
log.LogInformation($"Health check performed at: {DateTime.UtcNow} | Status: {status}");
}
}
This works exactly the same as my extension from the previous post but it's much simpler.
Greater Flexibility
This approach, in addition to being simpler, also provides greater flexibility. Using named or typed clients with the previous approach was possible, but not that easy. Now it just requires changing the registration code and constructor parameter. For example, we might want to encapsulate HttpClient
and instead, use a service that exposes methods for handling specific calls (it will also allow us to avoid mentioned above issue).
public interface IHealthCheckClient
{
Task<String> GetStatusAsync();
}
public class HealthCheckClient: IHealthCheckClient
{
private readonly HttpClient _client;
public HealthCheckClient(HttpClient client)
{
_client = client;
}
public Task<String> GetStatusAsync()
{
return _client.GetStringAsync("/");
}
}
We can use one of many AddHttpClient
overloads to register that service with some default configuration.
[assembly: FunctionsStartup(typeof(Startup))]
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddHttpClient<IHealthCheckClient, HealthCheckClient>(client =>
{
client.BaseAddress = new Uri("https://localhost:5001/healthcheck");
});
}
}
Now we only need to change the constructor parameter to the interface of the service.
public class PeriodicHealthCheckFunction
{
private readonly IHealthCheckClient _healthCheckClient;
public PeriodicHealthCheckFunction(IHealthCheckClient healthCheckClient)
{
_healthCheckClient = healthCheckClient ?? throw new ArgumentNullException(nameof(healthCheckClient));
}
[FunctionName("PeriodicHealthCheckFunction")]
public async Task Run(
[TimerTrigger("0 */1 * * * *")]TimerInfo healthCheckTimer,
ILogger log)
{
string status = await _healthCheckClient.GetStatusAsync();
log.LogInformation($"Health check performed at: {DateTime.UtcNow} | Status: {status}");
}
}
This is much better than the previous approach, so I'm going to obsolete/archive my GitHub repository with sample code.