Yes, you can and should use dependency injection with HttpClient
in C#. The recommended approach is using IHttpClientFactory
, which was introduced in ASP.NET Core 2.1 to solve common issues like socket exhaustion and DNS changes that occur with direct HttpClient
instantiation.
Why Use Dependency Injection with HttpClient?
- Proper Resource Management: Prevents socket exhaustion and handles connection pooling
- DNS Changes: Automatically respects DNS TTL settings
- Testability: Easier to mock and unit test HTTP interactions
- Configuration Management: Centralized configuration for all HTTP clients
- Performance: Reuses connections and reduces overhead
Basic Setup
1. Register IHttpClientFactory
In Program.cs
(or Startup.cs
in older versions):
var builder = WebApplication.CreateBuilder(args);
// Register IHttpClientFactory
builder.Services.AddHttpClient();
// Register your services
builder.Services.AddScoped<IApiService, ApiService>();
var app = builder.Build();
2. Inject IHttpClientFactory
public interface IApiService
{
Task<string> GetDataAsync(string endpoint);
}
public class ApiService : IApiService
{
private readonly IHttpClientFactory _httpClientFactory;
public ApiService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<string> GetDataAsync(string endpoint)
{
using var httpClient = _httpClientFactory.CreateClient();
var response = await httpClient.GetAsync(endpoint);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
Advanced Patterns
Named Clients
Configure specific clients with predefined settings:
// Registration
builder.Services.AddHttpClient("ApiClient", client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.Timeout = TimeSpan.FromSeconds(30);
});
// Usage
public class ApiService : IApiService
{
private readonly IHttpClientFactory _httpClientFactory;
public ApiService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<ApiResponse> GetUserAsync(int userId)
{
using var httpClient = _httpClientFactory.CreateClient("ApiClient");
var response = await httpClient.GetAsync($"users/{userId}");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<ApiResponse>(json);
}
}
Typed Clients
Create strongly-typed HTTP clients:
public class GitHubApiClient
{
private readonly HttpClient _httpClient;
public GitHubApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri("https://api.github.com/");
_httpClient.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
}
public async Task<GitHubUser> GetUserAsync(string username)
{
var response = await _httpClient.GetAsync($"users/{username}");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<GitHubUser>(json);
}
public async Task<IEnumerable<GitHubRepo>> GetUserReposAsync(string username)
{
var response = await _httpClient.GetAsync($"users/{username}/repos");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<IEnumerable<GitHubRepo>>(json);
}
}
// Registration
builder.Services.AddHttpClient<GitHubApiClient>();
// Usage in controller
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly GitHubApiClient _gitHubClient;
public UsersController(GitHubApiClient gitHubClient)
{
_gitHubClient = gitHubClient;
}
[HttpGet("{username}")]
public async Task<IActionResult> GetUser(string username)
{
try
{
var user = await _gitHubClient.GetUserAsync(username);
return Ok(user);
}
catch (HttpRequestException)
{
return NotFound();
}
}
}
Configuration with Options Pattern
Combine with the Options pattern for flexible configuration:
public class ApiClientOptions
{
public string BaseUrl { get; set; } = string.Empty;
public int TimeoutSeconds { get; set; } = 30;
public string ApiKey { get; set; } = string.Empty;
}
// Registration
builder.Services.Configure<ApiClientOptions>(
builder.Configuration.GetSection("ApiClient"));
builder.Services.AddHttpClient<ApiService>((serviceProvider, client) =>
{
var options = serviceProvider.GetRequiredService<IOptions<ApiClientOptions>>().Value;
client.BaseAddress = new Uri(options.BaseUrl);
client.Timeout = TimeSpan.FromSeconds(options.TimeoutSeconds);
client.DefaultRequestHeaders.Add("X-API-Key", options.ApiKey);
});
// appsettings.json
{
"ApiClient": {
"BaseUrl": "https://api.example.com/",
"TimeoutSeconds": 60,
"ApiKey": "your-api-key-here"
}
}
Adding Resilience with Polly
Enhance your HTTP clients with retry policies:
builder.Services.AddHttpClient<ApiService>()
.AddPolicyHandler(GetRetryPolicy());
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return Policy
.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
.Or<HttpRequestException>()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (outcome, timespan, retryCount, context) =>
{
Console.WriteLine($"Retry {retryCount} after {timespan} seconds");
});
}
Best Practices
- Always use
IHttpClientFactory
instead of directHttpClient
instantiation - Don't store
HttpClient
instances as fields - create them as needed - Use
using
statements when creating clients from factory - Configure base addresses and headers at registration time
- Implement proper error handling for HTTP operations
- Consider using typed clients for complex API interactions
- Add retry policies for improved resilience
Using dependency injection with HttpClient
through IHttpClientFactory
is the recommended pattern for modern C# applications, providing better resource management, testability, and maintainability.