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. If we want an instance of HttpClient from registered HttpClientFactory, it has to be a constructor parameter.

public class PeriodicHealthCheckFunction
{
    private readonly HttpClient _httpClient;

    public PeriodicHealthCheckFunction(HttpClient httpClient)
    {
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
    }

    [FunctionName("PeriodicHealthCheckFunction")]
    public async Task Run(
        [TimerTrigger("0 */1 * * * *")]TimerInfo healthCheckTimer,
        ILogger log)
    {
        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.

public interface IHealthCheckClient
{
    Task<String> GetStatusAsync();
}

public class HealthCheckClient: IHealthCheckClient
{
    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.