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
IHttpClientFactoryinstead of directHttpClientinstantiation - Don't store
HttpClientinstances as fields - create them as needed - Use
usingstatements 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.