HttpClient ve Refit¶
HttpClient yönetimi ve Refit ile deklaratif HTTP istemcileri oluşturmak API entegrasyonlarını kolaylaştırır; yanlış kullanımlar socket tükenmesine ve bakım zorluğuna yol açar.
1. Her Çağrıda HttpClient Oluşturmak¶
❌ Yanlış Kullanım: new HttpClient() ile socket exhaustion riski.
public async Task<Product> GetProductAsync(int id)
{
using var client = new HttpClient(); // Her çağrıda yeni socket
client.BaseAddress = new Uri("https://api.example.com");
return await client.GetFromJsonAsync<Product>($"/products/{id}");
}
// Socket'lar TIME_WAIT durumunda birikir, port tükenir
✅ İdeal Kullanım: IHttpClientFactory ile typed client kullanın.
builder.Services.AddHttpClient<IProductApiClient, ProductApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com");
client.Timeout = TimeSpan.FromSeconds(10);
client.DefaultRequestHeaders.Add("Accept", "application/json");
});
public class ProductApiClient : IProductApiClient
{
private readonly HttpClient _client;
public ProductApiClient(HttpClient client) => _client = client;
public async Task<Product> GetProductAsync(int id)
=> await _client.GetFromJsonAsync<Product>($"/products/{id}");
}
2. Manuel HTTP Çağrıları Yazmak¶
❌ Yanlış Kullanım: Her endpoint için boilerplate kod tekrarlamak.
public async Task<List<Product>> GetAllAsync()
{
var response = await _client.GetAsync("/products");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<List<Product>>(json);
}
public async Task<Product> CreateAsync(Product product)
{
var json = JsonSerializer.Serialize(product);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _client.PostAsync("/products", content);
response.EnsureSuccessStatusCode();
return JsonSerializer.Deserialize<Product>(await response.Content.ReadAsStringAsync());
}
✅ İdeal Kullanım: Refit ile deklaratif API tanımı yapın.
public interface IProductApi
{
[Get("/products")]
Task<List<Product>> GetAllAsync();
[Get("/products/{id}")]
Task<Product> GetByIdAsync(int id);
[Post("/products")]
Task<Product> CreateAsync([Body] Product product);
[Put("/products/{id}")]
Task<Product> UpdateAsync(int id, [Body] Product product);
[Delete("/products/{id}")]
Task DeleteAsync(int id);
}
builder.Services.AddRefitClient<IProductApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"));
3. Delegating Handler Kullanmamak¶
❌ Yanlış Kullanım: Her istekte manuel header ekleme.
public async Task<Product> GetProductAsync(int id)
{
_client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", await GetTokenAsync());
_client.DefaultRequestHeaders.Add("X-Correlation-Id", Guid.NewGuid().ToString());
return await _client.GetFromJsonAsync<Product>($"/products/{id}");
}
✅ İdeal Kullanım: Delegating handler ile cross-cutting concern’leri yönetin.
public class AuthenticationHandler : DelegatingHandler
{
private readonly ITokenService _tokenService;
public AuthenticationHandler(ITokenService tokenService) => _tokenService = tokenService;
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken ct)
{
var token = await _tokenService.GetAccessTokenAsync();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Headers.Add("X-Correlation-Id", Activity.Current?.Id ?? Guid.NewGuid().ToString());
return await base.SendAsync(request, ct);
}
}
builder.Services.AddTransient<AuthenticationHandler>();
builder.Services.AddRefitClient<IProductApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"))
.AddHttpMessageHandler<AuthenticationHandler>();
4. Hata Yönetimi Yapmamak¶
❌ Yanlış Kullanım: HTTP hata kodlarını görmezden gelmek.
public async Task<Product?> GetProductAsync(int id)
{
return await _client.GetFromJsonAsync<Product>($"/products/{id}");
// 404, 500 vb. durumlar exception fırlatır, kullanıcıya anlamsız hata döner
}
✅ İdeal Kullanım: HTTP status code’larına göre yapılandırılmış hata yönetimi yapın.
public async Task<Result<Product>> GetProductAsync(int id)
{
var response = await _client.GetAsync($"/products/{id}");
return response.StatusCode switch
{
HttpStatusCode.OK => Result<Product>.Success(
await response.Content.ReadFromJsonAsync<Product>()),
HttpStatusCode.NotFound => Result<Product>.Failure("Ürün bulunamadı"),
HttpStatusCode.TooManyRequests => Result<Product>.Failure("Rate limit aşıldı"),
_ => Result<Product>.Failure($"API hatası: {response.StatusCode}")
};
}
// Refit ile hata yönetimi
builder.Services.AddRefitClient<IProductApi>(new RefitSettings
{
ExceptionFactory = async response =>
{
if (response.IsSuccessStatusCode) return null;
var content = await response.Content.ReadAsStringAsync();
return new ApiException(response.RequestMessage, response.StatusCode, content);
}
});
5. Resilience Eklememek¶
❌ Yanlış Kullanım: Geçici hatalarda yeniden deneme yapmamak.
builder.Services.AddHttpClient<IProductApiClient, ProductApiClient>();
// Retry, circuit breaker, timeout yok
✅ İdeal Kullanım: Microsoft.Extensions.Http.Resilience ile resilience ekleyin.
builder.Services.AddHttpClient<IProductApiClient, ProductApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com");
})
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.Retry.Delay = TimeSpan.FromSeconds(1);
options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(30);
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5);
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(30);
});