Introduction
In modern .NET development, HttpClient is the core class used for sending HTTP requests and receiving responses. However, directly using HttpClient can lead to several issues, such as socket exhaustion and inability to adapt to DNS changes. To address these problems, .NET Core 2.1 introduced HttpClientFactory. This article will delve into how HttpClientFactory works, its internal mechanisms, usage patterns, and best practices, aiming to provide comprehensive guidance for .NET developers.
Introduction to HttpClient and HttpMessageHandler
Role of HttpClient
HttpClient is a class provided by .NET for sending HTTP requests to web resources and handling responses. It identifies the target resource through a URI and supports various HTTP methods (such as GET, POST). HttpClient relies on HttpMessageHandler to perform the actual network communication.
Role of HttpMessageHandler
HttpMessageHandler is a core component of HttpClient, responsible for managing the underlying network connections, including sockets, TCP connections, and TLS handshakes. The default implementation of HttpMessageHandler is HttpClientHandler, which interacts directly with the network. Custom HttpMessageHandlers (such as DelegatingHandler) can be inserted into the processing pipeline for functionalities like logging and authentication.
Problems with Directly Using HttpClient
Directly using HttpClient has the following common issues:
- Socket Exhaustion: Each time a new HttpClient instance is created, a new HttpClientHandler is instantiated, leading to new socket connections. If HttpClient is frequently created and destroyed, it may exhaust available sockets, resulting in
<span>SocketException</span>. - DNS Change Issues: If a single long-running HttpClient instance is used, the underlying HttpMessageHandler will not re-resolve DNS, which may cause requests to fail, especially in microservices architectures where service addresses may change dynamically.
- Resource Management Complexity: Although HttpClient implements the IDisposable interface, creating and destroying HttpClient directly within a
<span>using</span>statement is not best practice, as this leads to frequent closing and reopening of underlying connections, impacting performance.
How HttpClientFactory Works
HttpClientFactory addresses the above issues by managing the lifecycle of HttpMessageHandler. Below is a detailed explanation of its core mechanisms:
Pooling and Lifecycle Management of HttpMessageHandler
The core of HttpClientFactory lies in the pooling and reuse of HttpMessageHandler instances. Each time <span>IHttpClientFactory.CreateClient()</span> is called, a new HttpClient instance is returned, but these instances share a pool of underlying HttpMessageHandlers. This design makes HttpClient instances lightweight, while expensive network resources (such as socket connections) are managed by HttpMessageHandler.
- LifetimeTrackingHttpMessageHandler: HttpClientFactory uses a special implementation of HttpMessageHandler called
<span>LifetimeTrackingHttpMessageHandler</span>, which has a default lifetime of 2 minutes. This lifetime can be configured using the<span>SetHandlerLifetime</span>method. - Cleanup Mechanism: HttpClientFactory maintains an active handler queue (
<span>_activeHandlers</span>) and an expired handler queue (<span>_expiredHandlers</span>). A timer (<span>CleanupTimer</span>) runs every 10 seconds to check if expired handlers are still in use. By tracking the reference status of handlers through<span>WeakReference</span>, if a handler is no longer referenced by any HttpClient (i.e.,<span>WeakReference.IsAlive</span>is false), it will be disposed of. - DNS Adaptability: By periodically refreshing HttpMessageHandler (default 2 minutes), HttpClientFactory ensures that new DNS resolutions can take effect, addressing the issue of long-running HttpClient instances being unable to adapt to DNS changes.
Dependency Injection (DI) Integration
HttpClientFactory is tightly integrated with Microsoft.Extensions.DependencyInjection. The default implementation of <span>IHttpClientFactory</span> is <span>DefaultHttpClientFactory</span>, which is registered as a singleton service. HttpClient instances are treated as transient objects, while HttpMessageHandler instances have their own scope, independent of the application’s scope (such as ASP.NET request scope).
This separation of scope design can lead to issues. For example, if a custom HttpMessageHandler needs to access services in the request scope (such as EF Core’s DbContext), it may not obtain the correct instance. The solution is to use <span>IHttpContextAccessor</span> to retrieve the required service from the request’s service provider.
Cleanup Timer and WeakReference
HttpClientFactory uses a timer and <span>WeakReference</span> to manage the lifecycle of HttpMessageHandler:
- Cleanup Timer: Runs every 10 seconds to check the handlers in the
<span>_expiredHandlers</span>queue. If a handler’s<span>WeakReference</span>indicates it is no longer referenced (i.e., has been garbage collected), the<span>Dispose</span>method is called to release resources. - Role of WeakReference: Through
<span>WeakReference</span>, HttpClientFactory can determine whether a handler is still in use without holding a strong reference to it, thus avoiding memory leaks.
Below is a pseudocode example of the cleanup mechanism:
internal class ExpiredHandlerTrackingEntry
{
private readonly WeakReference _livenessTracker;
public ExpiredHandlerTrackingEntry(ActiveHandlerTrackingEntry other)
{
Name = other.Name;
_livenessTracker = new WeakReference(other.Handler);
InnerHandler = other.Handler.InnerHandler;
}
public bool CanDispose => !_livenessTracker.IsAlive;
public HttpMessageHandler InnerHandler { get; }
public string Name { get; }
}
Performance and DNS Change Trade-offs
The lifecycle of HttpMessageHandler is a trade-off between performance and adaptability:
- Short Lifetimes: More frequent refreshes of handlers help quickly adapt to DNS changes but can lead to more TCP connections, TLS handshakes, and other overheads, impacting performance.
- Long Lifetimes: Reusing handlers can reduce connection overhead and improve performance, but may not adapt quickly to DNS changes.
The default 2-minute lifetime is a compromise, and developers can adjust it based on application needs using <span>SetHandlerLifetime</span>. For example:
services.AddHttpClient("MyClient")
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
Usage Patterns
HttpClientFactory supports three main usage patterns, each suitable for different scenarios:
Basic Usage
Directly create HttpClient instances using <span>IHttpClientFactory.CreateClient()</span>, suitable for simple scenarios:
public class MyService
{
private readonly IHttpClientFactory _clientFactory;
public MyService(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task<string> GetDataAsync()
{
var client = _clientFactory.CreateClient();
return await client.GetStringAsync("https://api.example.com/data");
}
}
</string>
Named Clients
Named clients allow configuring multiple HttpClient instances for different APIs. For example:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient("MyApi", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
client.DefaultRequestHeaders.Add("Authorization", "Bearer token");
});
}
public class MyService
{
private readonly IHttpClientFactory _clientFactory;
public MyService(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task<string> GetDataAsync()
{
var client = _clientFactory.CreateClient("MyApi");
return await client.GetStringAsync("/data");
}
}
</string>
Typed Clients
Typed clients provide a strongly-typed way to use HttpClient by defining interfaces and implementing classes, recommended for complex scenarios:
public interface IMyApiClient
{
Task<string> GetDataAsync();
}
public class MyApiClient : IMyApiClient
{
private readonly HttpClient _client;
public MyApiClient(HttpClient client)
{
_client = client;
}
public async Task<string> GetDataAsync()
{
return await _client.GetStringAsync("/data");
}
}
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient<imyapiclient, myapiclient="">(client =>
{
client.BaseAddress = new Uri("https://api.example.com");
});
}
</imyapiclient,></string></string>
| Usage Pattern | Advantages | Applicable Scenarios |
|---|---|---|
| Basic Usage | Simple, no additional configuration required | Simple HTTP requests, temporary use |
| Named Clients | Supports multiple configurations, flexible | Need different settings for different APIs |
| Typed Clients | Strongly typed, easy to test and maintain | Complex business logic, dependency injection |
Configuring HttpClient Instances
HttpClientFactory provides rich configuration options, including:
Setting Basic Properties
Base addresses, default request headers, etc., can be configured:
services.AddHttpClient("MyApi", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
});
Integrating Polly Policies
Using Polly, retry, circuit breaker, and other policies can be added to HttpClient:
services.AddHttpClient("MyApi")
.AddPolicyHandler(Policy<httpresponsemessage>
.HandleTransientHttpError()
.RetryAsync(3));
</httpresponsemessage>
Adding Custom DelegatingHandler
Custom DelegatingHandlers can be used for logging, authentication, etc.:
public class LoggingHandler : DelegatingHandler
{
protected override async Task<httpresponsemessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Console.WriteLine($"Request: {request}");
var response = await base.SendAsync(request, cancellationToken);
Console.WriteLine($"Response: {response.StatusCode}");
return response;
}
}
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient("MyApi")
.AddHttpMessageHandler<logginghandler>();
services.AddTransient<logginghandler>();
}
</logginghandler></logginghandler></httpresponsemessage>
DI Scope and HttpClientFactory
HttpMessageHandler instances have independent scopes, separate from the application’s scope (such as ASP.NET request scope). This can lead to the following issues:
- Scope Service Access: If a handler needs to access services in the request scope (such as HttpContext), it may obtain the wrong instance.
- Solution: Use
<span>IHttpContextAccessor</span>to retrieve services from the request’s service provider:
public class MyHandler : DelegatingHandler
{
private readonly IHttpContextAccessor _httpContextAccessor;
public MyHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override async Task<httpresponsemessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var context = _httpContextAccessor.HttpContext;
// Use context to access request scope services
return await base.SendAsync(request, cancellationToken);
}
}
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddHttpClient("MyApi")
.AddHttpMessageHandler<myhandler>();
services.AddTransient<myhandler>();
}
</myhandler></myhandler></httpresponsemessage>
Best Practices and Common Pitfalls
Best Practices
- Always Use HttpClientFactory: Avoid directly creating HttpClient instances to ensure resource management and DNS adaptability.
- Configure Lifetimes Appropriately: Adjust the lifecycle of HttpMessageHandler based on application needs. For environments with frequent DNS changes, shorten the lifetime; for high-performance requirements, extend the lifetime.
- Use Typed Clients: In complex applications, typed clients provide better encapsulation and testability.
- Integrate Polly: Add resilience strategies to HTTP requests to handle transient failures.
- Be Cautious with Scoped Services: When accessing scoped services in handlers, use
<span>IHttpContextAccessor</span>.
Common Pitfalls
- Frequent Creation and Destruction of HttpClient: May lead to socket exhaustion.
- Single Long-Running HttpClient: May fail to adapt to DNS changes.
- Using Typed Clients in Singleton Services: Typed clients are transient; if injected into singleton services, handlers may not refresh. The solution is to use named clients or configure
<span>PooledConnectionLifetime</span>. - Ignoring Cookie Management: Handlers pooled by HttpClientFactory share a CookieContainer, which may lead to cookie leakage. Applications requiring cookies should consider alternative approaches.
Conclusion
HttpClientFactory is the recommended way to manage HttpClient instances in .NET. By pooling HttpMessageHandlers, supporting resilience strategies, and integrating with the DI system, it addresses common issues associated with directly using HttpClient. Developers should gain a deep understanding of its internal mechanisms, choose appropriate usage patterns, and follow best practices to build efficient and reliable HTTP client applications.
References
- https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests
- https://andrewlock.net/exporing-the-code-behind-ihttpclientfactory/
- https://www.stevejgordon.co.uk/introduction-to-httpclientfactory-aspnetcore
- https://andrewlock.net/understanding-scopes-with-ihttpclientfactory-message-handlers/
- https://programmerall.com/article/9354146001/