IHttpClientFactory and Typed Clients
Register each outbound HTTP client once at startup and let the factory manage handler lifetime for you.
services.AddHttpClient<TClient>(client => { client.BaseAddress = new Uri("https://host/"); });An HttpClient has to be reused, it has to be configured, and the class that calls out is rarely the class that runs at startup. IHttpClientFactory, from the Microsoft.Extensions.Http package, handles all three. You describe each outbound client once when the application starts, and the factory hands out ready-made clients wherever they are needed through dependency injection, the pattern where a class lists what it needs in its constructor and the container supplies it.
Named clients
services.AddHttpClient("payments", client => ...) stores a configuration under a name. A class injects IHttpClientFactory and calls CreateClient("payments"). Every call returns a new HttpClient object with that base address and those default headers already set.
Typed clients
A typed client replaces the name with a class of your own. AddHttpClient<PaymentsClient>(...) registers PaymentsClient, whose constructor takes an HttpClient. Now the compiler catches a typo that a string name would not, and every call to that service lives in one place.
Why the factory, and what trips people up
Underneath each client sits a handler, the object that actually owns the connection pool. The factory keeps one handler per client name and recycles it after a handler lifetime, two minutes by default, which both shares connections and lets the client notice DNS changes. Disposing a client the factory gave you is harmless, because it does not dispose the shared handler. The trap is the opposite: hold a factory client in a singleton, a service created once for the process, and it keeps one handler forever, which is the problem the factory existed to prevent.
Example
// dotnet add package Microsoft.Extensions.Http
using Microsoft.Extensions.DependencyInjection;
ServiceCollection services = new();
// Describe the outbound client once, under a name.
services.AddHttpClient("catalog", client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.DefaultRequestHeaders.UserAgent.ParseAdd("orders-service/1.0");
});
ServiceProvider provider = services.BuildServiceProvider();
IHttpClientFactory factory = provider.GetRequiredService<IHttpClientFactory>();
// Each call returns a new HttpClient, already configured.
HttpClient first = factory.CreateClient("catalog");
HttpClient second = factory.CreateClient("catalog");
Console.WriteLine($"base address: {first.BaseAddress}");
Console.WriteLine($"user agent: {first.DefaultRequestHeaders.UserAgent}");
// Two clients, but they share one pooled handler behind the scenes.
Console.WriteLine($"same client object: {ReferenceEquals(first, second)}");
// Output:
// base address: https://api.example.com/
// user agent: orders-service/1.0
// same client object: FalseWhen to use it
- Put a downstream service's base address, user agent and timeout in one place at startup.
- Give each outbound dependency its own class so callers never build URLs by hand.
- Stop a singleton background worker from pinning one connection pool for the life of the process.
- Attach a client certificate to one outbound client without affecting the others.
More examples
A typed client, and how it is registered
This is the shape to reach for by default. Every detail of how you talk to the catalog service, including the URL paths, lives inside CatalogClient, so the endpoint code says what it wants rather than how to fetch it. Swapping the real client for a fake one in a test is then a matter of hiding CatalogClient behind an interface, or of handing it a fake handler.
// Program.cs
using Microsoft.Extensions.DependencyInjection;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient<CatalogClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.Timeout = TimeSpan.FromSeconds(10);
});
WebApplication app = builder.Build();
// CatalogClient is injected; nothing here mentions HttpClient or a string name.
app.MapGet("/products/{id:int}", async (int id, CatalogClient catalog) =>
await catalog.GetProductAsync(id) is Product product
? Results.Ok(product)
: Results.NotFound());
app.Run();
// CatalogClient.cs
using System.Net.Http.Json;
public sealed class CatalogClient(HttpClient http)
{
public async Task<Product?> GetProductAsync(int id) =>
await http.GetFromJsonAsync<Product>($"products/{id}");
}
public record Product(int Id, string Name);The configuration shorthand and its longhand
The two registrations do the same thing. The longhand is worth knowing because ConfigureHttpClient can be called more than once and from anywhere that holds the IHttpClientBuilder, which is how a library adds its own defaults on top of yours. Every other builder method, including the resilience handler in the next lesson, chains onto that same builder.
using Microsoft.Extensions.DependencyInjection;
ServiceCollection services = new();
// Shorthand: pass the configuration straight to AddHttpClient.
services.AddHttpClient<CatalogClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
});
// Longhand: the same two steps, separated. AddHttpClient returns an
// IHttpClientBuilder, and ConfigureHttpClient adds the delegate to it.
services.AddHttpClient<ReportsClient>()
.ConfigureHttpClient(client =>
{
client.BaseAddress = new Uri("https://reports.example.com/");
});
public sealed class CatalogClient(HttpClient http);
public sealed class ReportsClient(HttpClient http);Taking the handler lifetime into your own hands
Use this when you need low-level control, such as a client certificate or a proxy, and still want the factory's registration and logging. One warning: the factory recycles handlers so that DNS changes are picked up, so disabling SetHandlerLifetime without setting PooledConnectionLifetime leaves you with a handler that may keep connections open indefinitely.
using Microsoft.Extensions.DependencyInjection;
ServiceCollection services = new();
services.AddHttpClient("catalog")
// Configure the handler that actually owns the connections.
.UseSocketsHttpHandler((handler, _) =>
handler.PooledConnectionLifetime = TimeSpan.FromMinutes(5))
// The handler now recycles its own connections, so switch off the
// factory's two-minute rotation rather than having both.
.SetHandlerLifetime(Timeout.InfiniteTimeSpan);
Discussion