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 HttpClient instances does not immediately release the underlying socket. Sockets remain in a TIME_WAIT state, leading to socket exhaustion under heavy load.
  • Stale DNS Records: Maintaining a single static or singleton HttpClient avoids 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:

  1. Basic Usage: Requesting an ad-hoc client via _httpClientFactory.CreateClient().
  2. Named Clients: Registering preconfigured clients identified by a string key via services.AddHttpClient("GitHub", ...).
  3. 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.