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.