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
implementsIDisposable
, which means that we should dispose it after using it. However, disposingHttpClient
also disposes the underlyingHttpClientHandler
that manages the HTTP connections. This can lead to socket exhaustion if we create and disposeHttpClient
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 theHttpClientFactory.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 theHttpClientHandler
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 callingservices.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 aHttpClient
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 theHttpClient
andHttpMessageHandler
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.