HttpClient, .NET'te en çok yanlış kullanılan sınıflardan biridir. Her request'te new HttpClient() yazmak ya da onu Singleton olarak paylaşmak farklı ama eşit derecede ciddi sorunlara yol açar.
Sorunun Temeli: Socket Exhaustion
// Kötü — her çağrıda yeni HttpClient
public async Task<Product?> GetProductAsync(int id)
{
using var client = new HttpClient(); // Her seferinde yeni TCP bağlantısı açılır
return await client.GetFromJsonAsync<Product>($"https://api.example.com/products/{id}");
// Dispose edildi ama TCP bağlantısı TIME_WAIT'te bekler (~240 sn)
// Yüksek trafikte OS socket limiti aşılır → bağlantı reddi
}
// Yanıltıcı çözüm — Singleton HttpClient
private static readonly HttpClient _client = new();
// DNS değişikliklerini yakalamaz — hafızada eski IP kalır
IHttpClientFactory ile Doğru Çözüm
// Program.cs
builder.Services.AddHttpClient(); // Temel kayıt
// Named client
builder.Services.AddHttpClient("products", client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.Timeout = TimeSpan.FromSeconds(30);
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Add("X-API-Version", "2");
});
// Kullanım
public class ProductService(IHttpClientFactory factory)
{
public async Task<Product?> GetAsync(int id)
{
var client = factory.CreateClient("products"); // Pool'dan alınır
return await client.GetFromJsonAsync<Product>($"products/{id}");
}
}
Typed Client: En Temiz Yol
Typed client, bir servisi HttpClient ile birlikte kapsüller. DI'ya kayıt yapılır, constructor'a inject edilir:
// Typed client tanımı
public class ProductApiClient(HttpClient http)
{
public async Task<Product?> GetByIdAsync(int id, CancellationToken ct = default)
{
var response = await http.GetAsync($"products/{id}", ct);
if (response.StatusCode == HttpStatusCode.NotFound)
return null;
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Product>(cancellationToken: ct);
}
public async Task<List<Product>> GetAllAsync(
int page = 1, int size = 20, CancellationToken ct = default)
{
var response = await http.GetAsync($"products?page={page}&size={size}", ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<List<Product>>(cancellationToken: ct)
?? [];
}
public async Task<Product> CreateAsync(CreateProductRequest request, CancellationToken ct = default)
{
var response = await http.PostAsJsonAsync("products", request, ct);
response.EnsureSuccessStatusCode();
return (await response.Content.ReadFromJsonAsync<Product>(cancellationToken: ct))!;
}
}
// Program.cs — kayıt
builder.Services.AddHttpClient<ProductApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "api-token");
});
// Kullanım — normal DI inject
public class OrderService(ProductApiClient products)
{
public async Task<Order> CreateOrderAsync(int productId, int qty)
{
var product = await products.GetByIdAsync(productId)
?? throw new NotFoundException($"Ürün bulunamadı: {productId}");
// ...
}
}
Message Handler Pipeline
Handler'lar HttpClient'ın middleware sistemidir. Her request/response üzerinde merkezi işlem yapılabilir:
// Loglama handler'ı
public class LoggingHandler(ILogger<LoggingHandler> logger) : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken ct)
{
var sw = Stopwatch.StartNew();
logger.LogInformation("→ {Method} {Uri}", request.Method, request.RequestUri);
var response = await base.SendAsync(request, ct);
sw.Stop();
logger.LogInformation("← {Status} {Uri} ({Ms}ms)",
(int)response.StatusCode, request.RequestUri, sw.ElapsedMilliseconds);
return response;
}
}
// Retry + Circuit Breaker için Polly handler'ı
public class ResilienceHandler : DelegatingHandler
{
private static readonly ResiliencePipeline<HttpResponseMessage> _pipeline =
new ResiliencePipelineBuilder<HttpResponseMessage>()
.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(300),
BackoffType = DelayBackoffType.Exponential,
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>()
.HandleResult(r => r.StatusCode is
HttpStatusCode.TooManyRequests or
HttpStatusCode.ServiceUnavailable)
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
{
FailureRatio = 0.5,
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(30)
})
.Build();
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken ct)
=> _pipeline.ExecuteAsync(
async token => await base.SendAsync(request, token), ct).AsTask();
}
// Program.cs — handler pipeline
builder.Services.AddTransient<LoggingHandler>();
builder.Services.AddTransient<ResilienceHandler>();
builder.Services.AddHttpClient<ProductApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
})
.AddHttpMessageHandler<LoggingHandler>()
.AddHttpMessageHandler<ResilienceHandler>();
JSON Seçenekleri ve Tip Güvenliği
// Global JSON ayarları
builder.Services.ConfigureHttpClientDefaults(defaults =>
{
defaults.ConfigureHttpClient(client =>
{
client.Timeout = TimeSpan.FromSeconds(30);
});
});
// Özel JsonSerializerOptions
var jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() }
};
// Extension method ile temiz kullanım
public static class HttpClientExtensions
{
public static async Task<T?> GetFromJsonSafeAsync<T>(
this HttpClient client, string url, CancellationToken ct = default)
{
try
{
var response = await client.GetAsync(url, ct);
if (!response.IsSuccessStatusCode) return default;
return await response.Content.ReadFromJsonAsync<T>(cancellationToken: ct);
}
catch (HttpRequestException)
{
return default;
}
}
}
Primary Handler Yapılandırması
// SocketsHttpHandler: Connection pool ayarları
builder.Services.AddHttpClient<ProductApiClient>()
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(2), // DNS değişikliği için
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1),
MaxConnectionsPerServer = 10,
EnableMultipleHttp2Connections = true
});
Kural basit: new HttpClient() yerine her zaman IHttpClientFactory veya typed client kullanın. Handler pipeline ile retry, circuit breaker ve loglama gibi cross-cutting concern'leri merkezi olarak yönetin.