How to Use IHttpClientFactory in .NET: The Right Way to Make HTTP Requests
Stop creating new HttpClient instances. This guide explains the problems with traditional HttpClient usage and shows how to use IHttpClientFactory in .NET to create resilient and performant HTTP clients.
Making HTTP requests is a fundamental part of most modern applications. In .NET, the primary tool for this is the HttpClient
class. However, using it correctly is not as straightforward as it might seem, and incorrect usage can lead to serious problems like socket exhaustion.
The modern, recommended solution for this is the IHttpClientFactory
.
The Problem with HttpClient
There are two common but incorrect ways to use HttpClient
:
Anti-Pattern 1: Creating a new HttpClient
for every request.
// Don't do this!
public async Task<string> GetDataAsync()
{
using (var client = new HttpClient())
{
return await client.GetStringAsync("https://api.example.com/data");
}
}
While HttpClient
is disposable, it's not meant to be created and disposed of for every request. Each time you create a new HttpClient
, it holds onto a socket for a period of time after it's disposed (in a TIME_WAIT
state). Under high load, you can run out of available sockets, a problem known as socket exhaustion.
Anti-Pattern 2: Using a single, static HttpClient
for the application's lifetime.
// This is better, but still has a problem.
private static readonly HttpClient _client = new HttpClient();
public async Task<string> GetDataAsync()
{
return await _client.GetStringAsync("https://api.example.com/data");
}
This solves the socket exhaustion problem, but it introduces a new one: this single HttpClient
instance will not respect DNS changes. If the IP address of api.example.com
changes, your application will not pick up that change without being restarted.
The Solution: IHttpClientFactory
IHttpClientFactory
was introduced in .NET Core 2.1 to solve both of these problems. It's a factory that manages the lifecycle of the underlying HttpClientMessageHandler
instances, pooling them to prevent socket exhaustion while also rotating them periodically to ensure DNS changes are respected.
It's the best of both worlds.
How to Use It
Using IHttpClientFactory
is simple and integrates perfectly with the built-in dependency injection container.
1. Basic Usage
First, register the factory in your Program.cs
:
// Program.cs
builder.Services.AddHttpClient();
Then, you can inject IHttpClientFactory
into your services and use it to create HttpClient
instances.
public class MyService
{
private readonly IHttpClientFactory _httpClientFactory;
public MyService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<string> GetDataAsync()
{
var client = _httpClientFactory.CreateClient();
return await client.GetStringAsync("https://api.example.com/data");
}
}
Even though you are calling CreateClient()
each time, the factory is intelligently managing and reusing the underlying handlers, so this is safe and efficient.
2. Named Clients
If you need to call a specific API with a pre-configured base address and default headers, you can create a named client.
Configuration in Program.cs
:
builder.Services.AddHttpClient("GitHub", client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
client.DefaultRequestHeaders.Add("User-Agent", "MyAwesomeApp");
});
Usage in a service:
public class GitHubService
{
private readonly IHttpClientFactory _httpClientFactory;
public GitHubService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<string> GetUserDataAsync(string username)
{
var client = _httpClientFactory.CreateClient("GitHub");
return await client.GetStringAsync($"users/{username}");
}
}
This is a great way to centralize the configuration for the different APIs your application consumes.
3. Typed Clients (The Best Approach)
For an even cleaner and more strongly-typed approach, you can use typed clients. With this pattern, you create a service class that encapsulates all the logic for calling a specific API, and this class is registered with the DI container.
Create a service class that accepts HttpClient
in its constructor:
public class GitHubService
{
private readonly HttpClient _httpClient;
public GitHubService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<string> GetUserDataAsync(string username)
{
return await _httpClient.GetStringAsync($"users/{username}");
}
}
Register the typed client in Program.cs
:
builder.Services.AddHttpClient<GitHubService>(client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
client.DefaultRequestHeaders.Add("User-Agent", "MyAwesomeApp");
});
Inject and use your service:
Now, you can inject GitHubService
directly into your controllers or other services. The IHttpClientFactory
will automatically create an instance of GitHubService
and inject a correctly configured HttpClient
into its constructor.
public class MyController : ControllerBase
{
private readonly GitHubService _githubService;
public MyController(GitHubService githubService)
{
_githubService = githubService;
}
[HttpGet("/github-user/{username}")]
public async Task<IActionResult> GetUser(string username)
{
var userData = await _githubService.GetUserDataAsync(username);
return Content(userData, "application/json");
}
}
Conclusion
IHttpClientFactory
is the standard, correct, and robust way to make HTTP requests in any modern .NET application. It solves the subtle but serious problems associated with direct HttpClient
instantiation. By using typed clients, you can take this a step further to create clean, testable, and well-structured service classes for interacting with external APIs.