HttpClient
Standard HTTP client built into .NET framework. Supports dependency injection and lifecycle management through HttpClientFactory. Provides async communication, HTTP/2 support, automatic compression, and detailed timeout configuration. Recommended standard implementation for enterprise applications.
GitHub Overview
dotnet/runtime
.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
Topics
Star History
Library
HttpClient
Overview
HttpClient is the standard HTTP client library for .NET. Available from .NET Framework 4.5 onwards and .NET Core/.NET 5+, it provides full support for asynchronous programming (async/await) and achieves high performance and resource efficiency through integration with IHttpClientFactory. In 2025, it remains the most recommended HTTP client solution for .NET developers.
Details
HttpClient was developed as a successor to WebRequest from the early versions of .NET Framework, addressing the needs of modern HTTP communication. Particularly from .NET Core/.NET 5 onwards, integration with IHttpClientFactory has resolved socket exhaustion and DNS change issues, making it safe for use in enterprise-level applications. Integration with System.Text.Json and coordination with Polly enable robust HTTP communication.
Key Features
- Asynchronous Processing: Full support for async/await patterns
- IHttpClientFactory: Dependency injection and lifecycle management
- High Performance: Connection pooling and socket reuse
- JSON Integration: Efficient JSON processing with System.Text.Json
- Authentication Support: Bearer Token, JWT, and custom authentication support
- Error Handling: Comprehensive exception handling capabilities
- HTTP/2 Support: Latest protocol support
- Retry Policies: Integration with Polly library
- Timeout Control: Detailed timeout configuration
- Header Management: Custom headers and user agent configuration
Pros and Cons
Pros
- .NET standard library with no additional dependencies required
- Optimized resource management through IHttpClientFactory
- Excellent asynchronous processing with async/await support
- Excellent integration with dependency injection (DI)
- Reliability and continuous updates through Microsoft official support
- High-speed JSON processing through System.Text.Json integration
Cons
- Not available in older versions (.NET Framework 4.x and earlier)
- Requires some boilerplate code even for basic usage
- External libraries needed for advanced features (retry, circuit breaker, etc.)
- Synchronous methods are deprecated (to avoid deadlocks)
- Mocking for unit tests is somewhat complex
Reference Pages
- HttpClient Class - Microsoft Docs
- IHttpClientFactory - Microsoft Docs
- HttpClient Best Practices
- Making HTTP requests in .NET
Code Examples
Basic Setup (IHttpClientFactory)
// Program.cs (.NET 6+)
using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);
// Register IHttpClientFactory
builder.Services.AddHttpClient();
// Register named HttpClient
builder.Services.AddHttpClient("ApiClient", client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
client.Timeout = TimeSpan.FromSeconds(30);
});
// Register typed HttpClient
builder.Services.AddHttpClient<ApiService>();
var app = builder.Build();
// Service class example
public class ApiService
{
private readonly HttpClient _httpClient;
public ApiService(HttpClient httpClient)
{
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri("https://api.example.com/");
}
}
Basic Requests
public class HttpClientService
{
private readonly HttpClient _httpClient;
public HttpClientService(HttpClient httpClient)
{
_httpClient = httpClient;
}
// GET request
public async Task<string> GetDataAsync()
{
try
{
var response = await _httpClient.GetAsync("https://api.example.com/data");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Request error: {ex.Message}");
throw;
}
}
// POST request
public async Task<string> PostDataAsync(object data)
{
var json = JsonSerializer.Serialize(data);
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("https://api.example.com/users", content);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
// PUT request
public async Task<bool> UpdateDataAsync(int id, object data)
{
var json = JsonSerializer.Serialize(data);
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
var response = await _httpClient.PutAsync($"https://api.example.com/users/{id}", content);
return response.IsSuccessStatusCode;
}
// DELETE request
public async Task<bool> DeleteDataAsync(int id)
{
var response = await _httpClient.DeleteAsync($"https://api.example.com/users/{id}");
return response.IsSuccessStatusCode;
}
}
Authentication Processing
public class AuthenticatedHttpService
{
private readonly HttpClient _httpClient;
public AuthenticatedHttpService(HttpClient httpClient)
{
_httpClient = httpClient;
}
// Bearer Token authentication
public void SetBearerToken(string token)
{
_httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
// Basic authentication
public void SetBasicAuth(string username, string password)
{
var credentials = Convert.ToBase64String(
System.Text.Encoding.ASCII.GetBytes($"{username}:{password}"));
_httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", credentials);
}
// JWT Token acquisition and setting
public async Task<string> LoginAndGetTokenAsync(string username, string password)
{
var loginData = new { Username = username, Password = password };
var json = JsonSerializer.Serialize(loginData);
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("/auth/login", content);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
var tokenResponse = JsonSerializer.Deserialize<TokenResponse>(responseContent);
// Set token in header
SetBearerToken(tokenResponse.AccessToken);
return tokenResponse.AccessToken;
}
// Custom authentication header
public async Task<string> GetWithCustomAuthAsync(string apiKey)
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/api/secure-data");
request.Headers.Add("X-API-Key", apiKey);
request.Headers.Add("X-Client-Version", "1.0");
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
public class TokenResponse
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public int ExpiresIn { get; set; }
}
JSON Processing (System.Text.Json Integration)
public class JsonHttpService
{
private readonly HttpClient _httpClient;
private readonly JsonSerializerOptions _jsonOptions;
public JsonHttpService(HttpClient httpClient)
{
_httpClient = httpClient;
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
PropertyNameCaseInsensitive = true
};
}
// Send object
public async Task<TResponse> PostJsonAsync<TRequest, TResponse>(string endpoint, TRequest data)
{
var json = JsonSerializer.Serialize(data, _jsonOptions);
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(endpoint, content);
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<TResponse>(responseJson, _jsonOptions);
}
// Get JSON response
public async Task<T> GetJsonAsync<T>(string endpoint)
{
var response = await _httpClient.GetAsync(endpoint);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(json, _jsonOptions);
}
// Streaming JSON processing (for large responses)
public async Task<T> GetJsonStreamAsync<T>(string endpoint)
{
var response = await _httpClient.GetAsync(endpoint);
response.EnsureSuccessStatusCode();
using var stream = await response.Content.ReadAsStreamAsync();
return await JsonSerializer.DeserializeAsync<T>(stream, _jsonOptions);
}
// Practical example
public async Task<User> CreateUserAsync(CreateUserRequest request)
{
return await PostJsonAsync<CreateUserRequest, User>("/api/users", request);
}
public async Task<List<User>> GetUsersAsync()
{
return await GetJsonAsync<List<User>>("/api/users");
}
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
}
public class CreateUserRequest
{
public string Name { get; set; }
public string Email { get; set; }
}
Error Handling
public class RobustHttpService
{
private readonly HttpClient _httpClient;
private readonly ILogger<RobustHttpService> _logger;
public RobustHttpService(HttpClient httpClient, ILogger<RobustHttpService> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<ApiResult<T>> SafeGetAsync<T>(string endpoint)
{
try
{
using var response = await _httpClient.GetAsync(endpoint);
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<T>(json);
return ApiResult<T>.Success(data);
}
// Process by HTTP status code
return response.StatusCode switch
{
HttpStatusCode.NotFound => ApiResult<T>.Error("Resource not found"),
HttpStatusCode.Unauthorized => ApiResult<T>.Error("Authentication required"),
HttpStatusCode.Forbidden => ApiResult<T>.Error("Access denied"),
HttpStatusCode.BadRequest => ApiResult<T>.Error("Invalid request"),
HttpStatusCode.InternalServerError => ApiResult<T>.Error("Server error occurred"),
_ => ApiResult<T>.Error($"Unexpected error: {response.StatusCode}")
};
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP request failed for endpoint: {Endpoint}", endpoint);
return ApiResult<T>.Error($"Network error: {ex.Message}");
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.LogWarning("Request timeout for endpoint: {Endpoint}", endpoint);
return ApiResult<T>.Error("Request timed out");
}
catch (TaskCanceledException ex)
{
_logger.LogWarning("Request cancelled for endpoint: {Endpoint}", endpoint);
return ApiResult<T>.Error("Request was cancelled");
}
catch (JsonException ex)
{
_logger.LogError(ex, "JSON deserialization failed for endpoint: {Endpoint}", endpoint);
return ApiResult<T>.Error("Failed to parse response");
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error for endpoint: {Endpoint}", endpoint);
return ApiResult<T>.Error("An unexpected error occurred");
}
}
// Request with retry processing
public async Task<T> GetWithRetryAsync<T>(string endpoint, int maxRetries = 3)
{
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
try
{
var response = await _httpClient.GetAsync(endpoint);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(json);
}
catch (HttpRequestException ex) when (attempt < maxRetries)
{
_logger.LogWarning("Attempt {Attempt} failed, retrying... Error: {Error}",
attempt, ex.Message);
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt))); // Exponential backoff
}
}
throw new Exception($"Failed after {maxRetries} attempts");
}
}
public class ApiResult<T>
{
public bool IsSuccess { get; set; }
public T Data { get; set; }
public string ErrorMessage { get; set; }
public static ApiResult<T> Success(T data) => new() { IsSuccess = true, Data = data };
public static ApiResult<T> Error(string message) => new() { IsSuccess = false, ErrorMessage = message };
}
Asynchronous Processing and Performance Optimization
public class PerformantHttpService
{
private readonly HttpClient _httpClient;
private readonly SemaphoreSlim _semaphore;
public PerformantHttpService(HttpClient httpClient)
{
_httpClient = httpClient;
_semaphore = new SemaphoreSlim(10, 10); // Limit concurrent execution
}
// Concurrent fetching of multiple URLs
public async Task<Dictionary<string, string>> GetMultipleUrlsAsync(IEnumerable<string> urls)
{
var tasks = urls.Select(async url =>
{
await _semaphore.WaitAsync();
try
{
var response = await _httpClient.GetStringAsync(url);
return new KeyValuePair<string, string>(url, response);
}
finally
{
_semaphore.Release();
}
});
var results = await Task.WhenAll(tasks);
return results.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
// Using CancellationToken
public async Task<string> GetWithCancellationAsync(string endpoint, CancellationToken cancellationToken = default)
{
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken, timeoutCts.Token);
try
{
var response = await _httpClient.GetAsync(endpoint, combinedCts.Token);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested)
{
throw new TimeoutException("Request timed out after 30 seconds");
}
}
// Streaming download
public async Task DownloadLargeFileAsync(string url, string filePath,
IProgress<long> progress = null, CancellationToken cancellationToken = default)
{
using var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
response.EnsureSuccessStatusCode();
var contentLength = response.Content.Headers.ContentLength;
using var stream = await response.Content.ReadAsStreamAsync();
using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
var buffer = new byte[8192];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)
{
await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken);
totalBytesRead += bytesRead;
progress?.Report(totalBytesRead);
}
}
// Batch processing
public async Task<List<T>> ProcessBatchAsync<T>(IEnumerable<string> endpoints, int batchSize = 5)
{
var results = new List<T>();
var batches = endpoints.Chunk(batchSize);
foreach (var batch in batches)
{
var batchTasks = batch.Select(async endpoint =>
{
var response = await _httpClient.GetStringAsync(endpoint);
return JsonSerializer.Deserialize<T>(response);
});
var batchResults = await Task.WhenAll(batchTasks);
results.AddRange(batchResults);
// Wait between batches (rate limiting protection)
await Task.Delay(100);
}
return results;
}
// Concurrent requests with HTTP/2 support
public async Task<List<ApiResponse>> GetConcurrentHttp2Async(List<string> endpoints)
{
// HTTP/2 can process multiple requests concurrently on the same connection
var tasks = endpoints.Select(async endpoint =>
{
try
{
var response = await _httpClient.GetAsync(endpoint);
return new ApiResponse
{
Endpoint = endpoint,
StatusCode = response.StatusCode,
Content = await response.Content.ReadAsStringAsync(),
Headers = response.Headers.ToDictionary(h => h.Key, h => h.Value.FirstOrDefault())
};
}
catch (Exception ex)
{
return new ApiResponse
{
Endpoint = endpoint,
Error = ex.Message
};
}
});
return (await Task.WhenAll(tasks)).ToList();
}
}
public class ApiResponse
{
public string Endpoint { get; set; }
public HttpStatusCode StatusCode { get; set; }
public string Content { get; set; }
public Dictionary<string, string> Headers { get; set; }
public string Error { get; set; }
}
// Extension method (Chunk available in .NET 6+, for earlier versions)
public static class EnumerableExtensions
{
public static IEnumerable<T[]> Chunk<T>(this IEnumerable<T> source, int size)
{
var chunk = new List<T>(size);
foreach (var item in source)
{
chunk.Add(item);
if (chunk.Count == size)
{
yield return chunk.ToArray();
chunk.Clear();
}
}
if (chunk.Count > 0)
yield return chunk.ToArray();
}
}