Your HttpClient is Broken: The IHttpClientFactory Playbook
🌐 Making HTTP Requests in
.NET: Best Practices
When developing API-driven services in .NET, the strategy
used for managing HTTP communication is critical to application performance,
reliability, and scalability. This document details the modern, recommended
approach using IHttpClientFactory.
HttpClient vs. IHttpClientFactory in Modern Architectures
Historically, developers often instantiated HttpClient directly:
C#
using var client = new HttpClient();
// Use client to make a request
While simple, this pattern is highly problematic in
production environments, especially in microservice and high-load
architectures.
⚠️ Common Pitfalls of Direct HttpClient Instantiation
|
Pattern |
Issue |
Description |
|
new HttpClient() in a
using block |
Socket Exhaustion |
Each new instance
allocates a new HttpMessageHandler and potentially a new socket
connection. While the HttpClient instance is disposed, the underlying
socket may take time to close, leading to the application running out of
available sockets under heavy load. |
|
Static or Singleton HttpClient |
Stale DNS Entries |
A long-lived static or
singleton HttpClient instance reuses the underlying handler
indefinitely. The handler does not respect Time-To-Live (TTL) changes for DNS
records, meaning it will never refresh the DNS entry for a target service,
leading to failed requests if the service's IP address changes. |
✨ The Modern Solution: IHttpClientFactory
.NET introduced IHttpClientFactory to address these
limitations. It acts as a managed factory that provides a robust, modern
approach to HttpClient management.
Key Benefits of IHttpClientFactory:
- Handler
Pooling: The factory manages the lifecycle of the underlying HttpMessageHandler
objects, ensuring that they are pooled and reused efficiently, which
prevents socket exhaustion.
- DNS
Refresh: Handlers in the pool have a configurable lifetime, after
which they are refreshed, mitigating the stale DNS entry problem.
- Resilience
Integration: It natively integrates with libraries like Polly to
easily apply resilience patterns (e.g., Retry, Timeout, Circuit Breaker)
across all outgoing HTTP calls.
- Testability:
It integrates with .NET's dependency injection (DI) system, making the
code more testable.
🛠️ IHttpClientFactory
Usage Patterns
The IHttpClientFactory is registered via the Microsoft.Extensions.Http
NuGet package and is the officially recommended approach.
1. Basic Usage (Named or Typed Clients Recommended)
This pattern injects IHttpClientFactory directly into
a service and uses it to create a transient HttpClient instance for each
request.
Registration (Implicit): The factory is typically
registered when configuring the DI container.
Usage Example:
C#
public class ExternalApiService
{
private readonly IHttpClientFactory _factory;
// Factory is injected via DI
public ExternalApiService(IHttpClientFactory factory)
{
_factory = factory;
}
public async Task<string> GetUserData(int userId)
{
// Creates a new HttpClient instance,
but uses a pooled, managed handler
var client = _factory.CreateClient();
var response = await client.GetAsync($"https://api.external.com/users/{userId}");
response.EnsureSuccessStatusCode();
return await
response.Content.ReadAsStringAsync();
}
}
2. Named Clients
Named clients are useful for configuring multiple external
services, each with distinct settings (Base Address, Headers, Timeouts, etc.)
and handlers.
Registration in Program.cs:
C#
builder.Services.AddHttpClient("GraphAPI", client
=>
{
client.BaseAddress
= new Uri("https://graph.microsoft.com");
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.Timeout =
TimeSpan.FromSeconds(15);
});
Usage:
C#
// Inject IHttpClientFactory
public class GraphConsumerService
{
private readonly
IHttpClientFactory _factory;
// ... constructor
...
public async
Task<string> GetUserData()
{
// Retrieve
the specifically configured client
var client =
_factory.CreateClient("GraphAPI");
var response =
await client.GetAsync("/v1.0/me");
// ...
return await
response.Content.ReadAsStringAsync();
}
}
3. Typed Clients (✅ Best Practice)
Typed Clients are the most recommended approach for
production-grade systems. This pattern registers a custom class (the
"Typed Client") that takes an HttpClient in its constructor.
Benefits:
- Decoupled:
The client logic is encapsulated in a dedicated service.
- Testable:
The HttpClient is injected, making it easy to mock for unit
testing.
- IntelliSense:
Clear method signatures and type safety.
Typed Client Implementation:
C#
public class GraphAPIService
{
// The HttpClient
is provided by the IHttpClientFactory-managed instance
private readonly
HttpClient _httpClient;
public GraphAPIService(HttpClient
httpClient)
{
_httpClient =
httpClient;
}
public async
Task<string> GetUserDetails(string id)
{
// Note:
BaseAddress is configured at registration, allowing a relative URI here
var response =
await _httpClient.GetAsync($"/{id}");
response.EnsureSuccessStatusCode();
return await
response.Content.ReadAsStringAsync();
}
}
Registration in Program.cs:
C#
// Registers GraphAPIService and configures the underlying
HttpClient
builder.Services.AddHttpClient<GraphAPIService>(client
=>
{
client.BaseAddress
= new Uri("https://graph.microsoft.com/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
});
💡 Conclusion
Modern, distributed, and cloud-native applications require
resilient and efficiently managed HTTP communication. The use of new
HttpClient() directly is considered an anti-pattern. IHttpClientFactory
is the official and recommended standard for building stable, scalable,
testable, and high-performance .NET applications, with Typed Clients
being the preferred usage pattern.
Comments
Post a Comment