Strongly Typed Clients and HttpClientFactory in C
This article explores the implementation and benefits of using
strongly typed clients alongside the IHttpClientFactory
pattern in C#. You will learn how the factory pattern resolves resource
issues like socket exhaustion and outdated DNS records, compare the
different consumption approaches, and understand how to build
maintainable, strongly typed HTTP abstractions in modern .NET
applications.
The Problem with Traditional HttpClient Usage
In early .NET development, developers commonly instantiated
HttpClient directly using a using statement or
kept a single static instance. Both approaches introduce severe
network-level issues:
- Socket Exhaustion: Disposing of
HttpClientinstances does not immediately release the underlying socket. Sockets remain in aTIME_WAITstate, leading to socket exhaustion under heavy load. - Stale DNS Records: Maintaining a single static or
singleton
HttpClientavoids socket exhaustion, but it keeps connections open indefinitely, failing to detect DNS updates or infrastructure changes.
The IHttpClientFactory Pattern
Introduced in .NET Core 2.1, IHttpClientFactory solves
these issues by managing the lifecycle of the underlying
HttpMessageHandler instances via a pool.
Instead of caching the HttpClient itself, the factory
pools and reuses the HttpMessageHandler (specifically
SocketsHttpHandler or HttpClientHandler). By
default, handlers are recycled every two minutes. This architecture
prevents socket exhaustion by reusing existing connections while
periodically rotating connections to respect DNS updates.
IHttpClientFactory supports three primary usage
patterns:
- Basic Usage: Requesting an ad-hoc client via
_httpClientFactory.CreateClient(). - Named Clients: Registering preconfigured clients
identified by a string key via
services.AddHttpClient("GitHub", ...). - Typed Clients: Encapsulating the client configuration and communication logic within dedicated classes.
What Are Strongly Typed Clients?
A strongly typed client is a standard C# class that accepts an
HttpClient via constructor injection. It acts as an
abstraction layer over an external HTTP service, encapsulating
endpoints, serialization logic, authorization headers, and error
handling.
Key benefits of strongly typed clients include:
- Separation of Concerns: Business logic communicates with a domain-specific interface rather than raw HTTP primitives.
- Compile-Time Safety: API interactions return strongly typed models instead of raw strings or JSON streams.
- Dependency Injection Integration: Typed clients integrate naturally with the .NET dependency injection (DI) container and can be resolved directly into controllers, minimal APIs, or services.
- Testability: The custom client can be paired with
an interface, simplifying unit testing through mocking without needing
to mock
HttpMessageHandler.
Implementing a Strongly Typed Client
1. Define the Client Class
Create a class that accepts HttpClient in its
constructor and exposes domain-specific methods:
public record UserProfile(int Id, string Username, string Email);
public class UserService
{
private readonly HttpClient _httpClient;
public UserService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<UserProfile?> GetUserByIdAsync(int id, CancellationToken cancellationToken = default)
{
return await _httpClient.GetFromJsonAsync<UserProfile>($"users/{id}", cancellationToken);
}
}2. Register the Client in Dependency Injection
In Program.cs, use the
AddHttpClient<TClient> extension method to register
the client and configure its default settings, such as base addresses
and timeout limits:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient<UserService>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.Timeout = TimeSpan.FromSeconds(30);
});
var app = builder.Build();When using AddHttpClient<UserService>, the
framework registers UserService with a
transient lifetime. Every time UserService
is resolved, the factory provisions a short-lived
HttpClient instance backed by a pooled, long-lived
HttpMessageHandler.
3. Consume the Typed Client
Inject the typed client directly into consumers, such as Minimal API endpoints, controllers, or background services:
app.MapGet("/api/users/{id:int}", async (int id, UserService userService) =>
{
var user = await userService.GetUserByIdAsync(id);
return user is not null ? Results.Ok(user) : Results.NotFound();
});
app.Run();By decoupling raw HTTP operations from application logic and
delegating socket management to IHttpClientFactory,
strongly typed clients ensure high performance, testability, and
resilience in .NET network communication.