In this blog post, I will explore the HttpClientFactory feature that was introduced in .NET Core 2.1 and also the shortcoming of HttpClient class, will discuss how HttpClientFactory can help us improve the performance and reliability of our HTTP-based communication with external services. Implementation of Polly is outside the scope of this post.

What is HttpClientFactory and why should we care 🤔?

HttpClient is a class that provides a base implementation for sending HTTP requests and receiving HTTP responses from a resource identified by a URI. It is one of the most commonly used types for interacting with web APIs and other HTTP-based services in .NET applications.

However, HttpClient also has few common pitfalls that we are aware of while using it -

  • HttpClient implements IDisposable, which means that we should dispose it after using it. However, disposing HttpClient also disposes the underlying HttpClientHandler that manages the HTTP connections. This can lead to socket exhaustion if we create and dispose HttpClient instances too frequently, as each socket needs some time to be released by the OS.
using var httpClient = new HttpClient(new Uri("https://www.example.com"));
using var httpResponseMessage = await httpClient.GetAsync();
  • HttpClient does not support DNS changes by default. If the IP address of a service changes due to DNS updates, HttpClient will not be able to resolve the new address unless we recreate it.
// Static instance of HttpClient
var httpResponseMessage = await _httpClient.GetAsync();

HttpClientFactory is a type serves as a factory abstraction that can create HttpClient instances with custom configurations and provides a central location for creating and managing HttpClient instances in our applications. Here are things HttpClientFactory come handy -

  • Managing the lifetime and pooling of the underlying HttpClientHandler instances. HttpClientFactory creates and caches a limited number of handlers based on some predefined policies and disposes them after a certain amount of time (by default 2 minutes). This way, we avoid socket exhaustion and DNS issues while still reusing existing handlers for better performance.

  • Providing a way to name and configure logical HttpClient instances. We can register named clients with different configurations or behaviors and then use them by providing their names to the HttpClientFactory.CreateClient method. We can also use typed clients, which are classes that accept an HttpClient as a constructor parameter and use it to make requests related to a specific service or resource. I will cover all these approaches below.

  • Providing extension methods for adding Polly-based middleware to our HttpClient instances. We can use these methods to easily configure various resiliency policies such as retries, timeouts, circuit breakers, fallbacks, etc. These policies are implemented as delegating handlers that wrap around the HttpClientHandler and execute before sending the request.

  • Providing logging and diagnostics for all requests sent through clients created by the factory. We can use ILogger to log information such as request URI, status code, duration, etc.

🏗️ How to use HttpClientFactory?

To use HttpClientFactory in our applications, we need to do the following steps:

  • Install the Microsoft.Extensions.Http package from NuGet.

  • Register the IHttpClientFactory service in our dependency injection (DI) container by calling services.AddHttpClient() in our Startup class or Program.cs file.

  • Request an IHttpClientFactory instance from the DI container in our class.

  • Use the IHttpClientFactory.CreateClient method to create a HttpClient instance.

🧑‍💻 Let’s write code!

HttpClientFactory basic usage -

// Program.cs

using System.Net.Http.Json;
using Microsoft.Extensions.DependencyInjection;

var serviceProvider = new ServiceCollection()
    .AddHttpClient() // Register HttpClientFactory in DI container.
    .AddSingleton<MessageService>() // Register MessageService as singleton.
    .BuildServiceProvider();

var messageService = serviceProvider.GetService<MessageService>();
var messages = await messageService.GetAsync();

public class MessageService
{
    private readonly IHttpClientFactory _httpClientFactory;

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

    public async Task<IEnumerable<string>> GetAsync()
    {
        // Create an HttpClient instance.
        var httpClient = _httpClientFactory.CreateClient();
        using var httpResponseMessage = await httpClient.GetAsync(new Uri("https://3449fa01-7854-4a28-b5d0-9a718ccb85ad.mock.pstmn.io/messages"));
        var messages = await httpResponseMessage.Content.ReadFromJsonAsync<List<string>>();
        return messages;
    }
}

From above code we’ll be able to fetch collection of messages from our HTTP server using HttpClient which was provided by HttpClientFactory. We register MessagesService as Singleton in DI container, which means only single instance will be created and reused until application restarts. _httpClientFactory.CreateClient() ensures that every time we call GetAsync() on MessageService new instance of HttpClient will be provided by attaching HttpClientHandler to it from a pool. In this way underlying HttpClientHandler getting reused even-tho new instance of HttpClient is getting created.

Now we understood how HttpClientFactory works lets also explore usage pattern & configuration it provides.

➡️ Named clients

As stated in .NET docs, Named clients are a good choice when:

  • The app requires many distinct uses of HttpClient.
  • Many HttpClient instances have different configuration.

Let’s modify our Program.cs to register name client.

// Program.cs 

using System.Net.Http.Json;
using Microsoft.Extensions.DependencyInjection;

var serviceCollection = new ServiceCollection();
// Register Named Client
serviceCollection.AddHttpClient("MockClient", client =>
    {
        client.BaseAddress = new Uri("https://3449fa01-7854-4a28-b5d0-9a718ccb85ad.mock.pstmn.io/");
        client.DefaultRequestHeaders.Add("Accept", "application/json");
    });

var serviceProvider = serviceCollection
    .AddSingleton<MessageService>()
    .BuildServiceProvider();

var messageService = serviceProvider.GetRequiredService<MessageService>();
var messages = await messageService.GetAsync();

public class MessageService
{
    private readonly IHttpClientFactory _httpClientFactory;

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

    public async Task<IEnumerable<string>> GetAsync()
    {
        var httpClient = _httpClientFactory.CreateClient("MockClient");
        using var httpResponseMessage = await httpClient.GetAsync("/messages");
        var messages = await httpResponseMessage.Content.ReadFromJsonAsync<List<string>>();
        return messages;
    }
}

This time we provided the BaseAddress and added Accept:application/json header while registering the Client to DI container, when we call _httpClientFactory.CreateClient("MockClient");, factory will provide the HttpClient instance with configured options.

➡️ Typed Client

A typed client is a strongly-typed wrapper around HttpClient that provides an easy-to-use interface for making requests to the API. With a typed client, you can encapsulate the logic for accessing the API in a separate class and use dependency injection to inject the client into other classes.

Let’s convert our named client to typed client.

// Program.cs

using System.Net.Http.Json;
using Microsoft.Extensions.DependencyInjection;

var serviceCollection = new ServiceCollection();
// Register Typed Client
serviceCollection.AddHttpClient<IMessageClient, MessageClient>(client =>
    {
        // Option 1: Provide configuration while registering typed instance.
        // --------
        client.BaseAddress = new Uri("https://3449fa01-7854-4a28-b5d0-9a718ccb85ad.mock.pstmn.io/");
        client.DefaultRequestHeaders.Add("Accept", "application/json");
    });

var serviceProvider = serviceCollection
    .BuildServiceProvider();

var messageClient = serviceProvider.GetRequiredService<IApiClient>();
var messages = await messageClient.GetMessage();

public interface IApiClient
{
    Task<IEnumerable<string>> Get();
}

public class ApiClient : IApiClient
{
    private readonly HttpClient _httpClient;

    public ApiClient(HttpClient httpClient)
    {
        /*
         Option 2: Provide configuration from constructor
         --------
         httpClient.BaseAddress = new Uri("https://3449fa01-7854-4a28-b5d0-9a718ccb85ad.mock.pstmn.io/");
         httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
        */
        
        _httpClient = httpClient;
    }
    
    public async Task<IEnumerable<string>> GetMessage()
    {
        using var httpResponseMessage = await _httpClient.GetAsync("/messages");
        var messages = await httpResponseMessage.Content.ReadFromJsonAsync<List<string>>();
        return messages;
    }
}

While registering typed client the lifetime is transient, which means that a new instance of the client will be created every time it is requested.

💡 Typed client supposed to be register as transient scope, avoid using typed client in singleton services, read drawback of doing it here - Use typed clients in singleton services.

At this stage our code is pulling list of messages from HTTP Server using HttpClient instance from HttpClientFactory. What if you want to test your application and provide a mock data instead of pull from HTTP Server. Let’s discuss it in next section.

🎭 Mock HttpClient to test our Application.

As far we know when we create an instance of HttpClient, it internally creates an instance of the HttpMessageHandler class to handle the actual sending and receiving of HTTP messages. HttpClientFactory also manages the lifetime of HttpMessageHandler in pool.

.AddHttpClient<T>().ConfigurePrimaryHttpMessageHandler<T>() provide a way to configure the primary HttpMessageHandler from the dependency injection container for a named and typed client. This will make our mocking process easy as we only need to provide a mocked implementation of HttpMessageHandler and register it in DI.

// Program.cs

using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using Microsoft.Extensions.DependencyInjection;

var serviceCollection = new ServiceCollection();
// Configure primary handler for typed client
serviceCollection.AddHttpClient<IMessageClient, MessageClient>() // Used Option 2 to provide configuration value.
    .ConfigurePrimaryHttpMessageHandler<ApiMockHandler>();

// Configure primary handler for named client
serviceCollection.AddHttpClient("MockClient", client =>
{
    client.BaseAddress = new Uri("https://3449fa01-7854-4a28-b5d0-9a718ccb85ad.mock.pstmn.io/");
    client.DefaultRequestHeaders.Add("Accept", "application/json");
}).ConfigurePrimaryHttpMessageHandler<ApiMockHandler>();

var serviceProvider = serviceCollection
    .AddSingleton<ApiMockHandler>() // IMPORTANT - To register our Mock handler in DI container
    .AddSingleton<MessageService>()
    .BuildServiceProvider();

// Mock Handler
public class ApiMockHandler : HttpMessageHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        const string messages = "[\"Mock-Message-1\", \"Mock-Message-2\", \"Mock-Message-3\"]";
        var responseMessage = new HttpResponseMessage
        {
            StatusCode = HttpStatusCode.OK,
            Content = new StringContent(messages)
        };

        responseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
        return await Task.FromResult(responseMessage);
    }
}

// Rest of the code remains same.

If you try to run the application you should be able to see the result from our Mock handler.

For Typed Client there is also a alternate way exists. We have a Typed Client class which we can mock and register to DI container.

// Program.cs

using Moq;

var mockMessageClient = new Mock<IMessageClient>();
mockMessageClient.Setup(x => x.Get())
    .ReturnsAsync(new[] { "Moq-Message-1", "Moq-Message-2", "Moq-Message-3" });

serviceCollection.AddScoped<IMessageClient>(x => mockMessageClient.Object);

When IMessageClient is requested, a mock instance will be provided by DI.

💭 Closing Thoughts

  • Avoid using HttpClient directly.
  • Use HttpClientFactory for handling the HttpClient and HttpMessageHandler instances.
  • HttpClientFactory can only be register though DI so your application must have DI container to work with.
  • Use Named Client if you application is dealing with multiple HttpClient configuration.
  • Typed Client will provide a typed wrapper around your API.
  • Avoid using Typed Client in singleton services.

📖 References