ResilientApiClient 1.0.0
dotnet add package ResilientApiClient --version 1.0.0
NuGet\Install-Package ResilientApiClient -Version 1.0.0
<PackageReference Include="ResilientApiClient" Version="1.0.0" />
<PackageVersion Include="ResilientApiClient" Version="1.0.0" />
<PackageReference Include="ResilientApiClient" />
paket add ResilientApiClient --version 1.0.0
#r "nuget: ResilientApiClient, 1.0.0"
#:package ResilientApiClient@1.0.0
#addin nuget:?package=ResilientApiClient&version=1.0.0
#tool nuget:?package=ResilientApiClient&version=1.0.0
ResilientApiClient
Birden fazla dış REST API'ye istek atarken rate limit aşımı, geçici hatalar ve servis kesintilerine karşı dayanıklı, tekrar kullanılabilir bir .NET 8 HTTP client sarmalayıcısı.
- Kaynak bazlı token bucket rate limiting: her API'nin kendi kuralı (ör. "Binance: dakikada 1200", "OpenWeather: saniyede 1"), kendi FIFO kuyruğu ve eşzamanlılık limiti var. Bir kaynağın limite takılması diğerlerini etkilemez.
- Circuit breaker (Closed → Open → Half-Open → Closed): art arda N hatada devre açılır, istekler sunucuya hiç gitmeden reddedilir; süre dolunca tek bir deneme isteği gönderilir.
- Retry (Polly v8): exponential backoff + jitter; 503, timeout ve ağ hatalarında otomatik tekrar. 429 / 503 yanıtlarında
Retry-Afterheader'ı okunur ve o kadar beklenir; 429 alındığında o kaynağın tüm istekleri duraklatılır. - Gözlemlenebilirlik: her olay (başarılı, retry edildi, rate limit'e takıldı, circuit açıldı, …) için
ResilienceEventevent'i, kaynak bazlı istatistikler ve opsiyonelILoggerentegrasyonu. - Tamamen async, her yerde
CancellationTokendesteği, thread-safe, test edilebilir (TimeProvider).
Kurulum
dotnet add package ResilientApiClient
Yerel olarak paketlemek için bkz. NuGet paketi oluşturma.
Hızlı başlangıç
1. API kaynaklarını tanımlayın
Her dış servis IApiSource arayüzünü implemente eder. Zorunlu üyeler: Name, BaseUrl, RateLimiterPolicy, DefaultHeaders. Circuit breaker, retry, eşzamanlılık ve timeout ayarları opsiyoneldir (verilmezse client'ın varsayılanları kullanılır).
using ResilientApiClient;
public sealed class BinanceApiSource : IApiSource
{
public string Name => "Binance";
public Uri BaseUrl { get; } = new("https://api.binance.com/api/v3/");
public RateLimiterPolicy RateLimiterPolicy { get; } = RateLimiterPolicy.PerMinute(1200);
public IReadOnlyDictionary<string, string> DefaultHeaders { get; } = new Dictionary<string, string>
{
["X-MBX-APIKEY"] = Environment.GetEnvironmentVariable("BINANCE_API_KEY") ?? string.Empty,
};
// Opsiyonel: bu kaynağa özel ayarlar
public CircuitBreakerOptions CircuitBreakerOptions { get; } = new() { FailureThreshold = 5, BreakDuration = TimeSpan.FromSeconds(30) };
public RetryOptions RetryOptions { get; } = new() { MaxRetryAttempts = 3, BaseDelay = TimeSpan.FromMilliseconds(500) };
public int? MaxConcurrentRequests => 10;
}
Özel mantık gerekmiyorsa hazır ApiSource sınıfını kullanabilirsiniz:
var openWeather = new ApiSource("OpenWeather", new Uri("https://api.openweathermap.org/data/2.5/"), RateLimiterPolicy.PerSecond(1))
{
DefaultHeaders = new Dictionary<string, string> { ["Accept"] = "application/json" },
AttemptTimeout = TimeSpan.FromSeconds(10),
};
2. Client'ı oluşturun ve istek gönderin
using var client = new ResilientHttpClient();
var binance = new BinanceApiSource();
// Tipli GET: JSON otomatik deserialize edilir
TickerPrice? ticker = await client.GetAsync<TickerPrice>(binance, "ticker/price?symbol=BTCUSDT", cancellationToken);
// Tam kontrol: kendi HttpRequestMessage'ınız ile
using var request = new HttpRequestMessage(HttpMethod.Get, "ticker/24hr?symbol=ETHUSDT");
request.Headers.Add("X-Trace-Id", traceId);
Ticker24h? stats = await client.SendAsync<Ticker24h>(binance, request, cancellationToken);
// Ham yanıt (durum kodu ne olursa olsun döner, dispose sorumluluğu sizde)
using HttpResponseMessage response = await client.SendAsync(binance, request, cancellationToken);
// JSON gövdeli POST
OrderResult? order = await client.PostAsync<OrderRequest, OrderResult>(binance, "order", newOrder, cancellationToken);
public sealed record TickerPrice(string Symbol, decimal Price);
ResilientHttpClientuzun ömürlü olacak şekilde tasarlanmıştır: tek bir örnek oluşturup paylaşın (ör. DI singleton). Her istekte yeni örnek oluşturmak rate limiter ve circuit breaker durumunu sıfırlar.
3. Olayları izleyin
client.ResilienceEvent += (_, e) =>
{
switch (e.EventType)
{
case ResilienceEventType.RetryScheduled:
Console.WriteLine($"[{e.SourceName}] #{e.RequestId} retry, {e.Delay?.TotalMilliseconds} ms sonra: {e.Message}");
break;
case ResilienceEventType.RateLimitDelayed:
Console.WriteLine($"[{e.SourceName}] token için {e.Delay?.TotalMilliseconds} ms beklendi");
break;
case ResilienceEventType.ServerRateLimited:
Console.WriteLine($"[{e.SourceName}] 429! Retry-After: {e.Delay}");
break;
case ResilienceEventType.CircuitOpened:
alerting.Notify($"{e.SourceName} devresi açıldı: {e.Message}");
break;
}
};
// Anlık istatistikler
foreach (SourceStatistics s in client.GetAllStatistics())
{
Console.WriteLine($"{s.SourceName}: {s.Succeeded}/{s.TotalRequests} başarılı, {s.Retries} retry, {s.ServerRateLimited}x 429, circuit={s.CircuitState}");
}
Dependency Injection
builder.Services.AddSingleton<BinanceApiSource>();
builder.Services.AddSingleton(sp => new ResilientHttpClient(
new ResilientHttpClientOptions
{
DefaultAttemptTimeout = TimeSpan.FromSeconds(15),
DefaultMaxConcurrentRequests = 32,
},
sp.GetRequiredService<ILogger<ResilientHttpClient>>()));
Parametresiz constructor, bağlantıları 2 dakikada bir yenileyen (DNS değişikliklerini yakalayan) bir SocketsHttpHandler ile kendi HttpClient'ını oluşturur. Kendi HttpClient'ınızı (ör. proxy, sertifika veya test handler'ı ile) vermek için:
var client = new ResilientHttpClient(myHttpClient, options, logger, disposeHttpClient: false);
Nasıl çalışır?
Her deneme (retry'lar dahil) şu adımlardan geçer; tüm durum kaynağa özeldir:
SendAsync ─► RetryPolicy (Polly: exponential backoff + jitter, Retry-After)
│ her deneme için:
├─ 1. Circuit ön kontrolü → açıksa CircuitBrokenException (sunucuya gitmez)
├─ 2. Kuyruk kabul kontrolü → kuyruk dolu / bekleme çok uzun ise RateLimitRejectedException
├─ 3. Eşzamanlılık slotu → MaxConcurrentRequests
├─ 4. Token bucket (FIFO) → token alınır alınmaz gönderilir (politika asla aşılmaz)
├─ 5. Circuit lease → Half-Open'da yalnızca tek deneme isteği geçer
├─ 6. HTTP isteği (deneme timeout'u ile)
└─ 7. Sonuç raporlanır → 5xx/timeout/ağ hatası = hata; 429 → kaynağın bucket'ı Retry-After kadar duraklatılır
- Circuit retry'lar sırasında açılırsa kalan retry'lar hemen durur.
- Eşzamanlılık slotu token'dan önce alınır; böylece slotlar doluyken biriken token'lar slotlar boşalınca bir anda harcanıp sunucuya ani yük (burst) bindirmez.
- 429, servisin sağlığı hakkında bilgi vermediğinden circuit breaker'da hata sayılmaz.
Rate limiting (RateLimiterPolicy)
| Parametre | Açıklama | Varsayılan |
|---|---|---|
PermitLimit / Window |
Pencere başına izin verilen istek (PerSecond, PerMinute, PerHour kısayolları) |
– |
BurstSize |
Bucket kapasitesi, art arda gönderilebilecek maksimum istek | PermitLimit |
QueueLimit |
Aynı anda bekleyebilecek maksimum istek; aşılırsa RateLimitRejectedException |
10 000 |
MaxQueueWait |
Tahmini bekleme bu süreyi aşıyorsa istek beklemeden reddedilir | sınırsız |
Token'lar sürekli dolar (5 req/s = her 200 ms'de bir token). Bucket dolu başlar. Sunucu katı sabit pencere uyguluyorsa (ör. "hiçbir dakikada 1200'ü geçme"), ilk burst için pay bırakmak adına BurstSize'ı PermitLimit'ten küçük seçin.
Circuit breaker (CircuitBreakerOptions)
| Parametre | Açıklama | Varsayılan |
|---|---|---|
FailureThreshold |
Devreyi açan art arda hata sayısı | 5 |
BreakDuration |
Devrenin açık kalma süresi | 30 sn |
FailureStatusCodes |
Hata sayılan durum kodları (ağ hataları ve timeout'lar her zaman hata sayılır) | 408, 500, 502, 503, 504 |
CircuitBreaker sınıfı bağımsız olarak da kullanılabilir (Acquire / RecordSuccess / RecordFailure / Release, StateChanged event'i). Manuel sıfırlama: client.ResetCircuit("Binance").
Retry (RetryOptions)
| Parametre | Açıklama | Varsayılan |
|---|---|---|
MaxRetryAttempts |
İlk denemeye ek maksimum retry (0 = kapalı) |
3 |
BaseDelay / MaxDelay |
Exponential backoff medyan başlangıcı / üst sınırı | 500 ms / 30 sn |
UseJitter |
Polly decorrelated jitter | true |
MaxRetryAfterDelay |
Kabul edilen en uzun Retry-After; daha uzunsa retry yapılmaz, yanıt döner |
60 sn |
RetryableStatusCodes |
Retry edilen kodlar | 408, 429, 500, 502, 503, 504 |
RetryNonIdempotentRequests |
POST/PATCH gibi istekler 5xx/timeout sonrası da retry edilsin mi | false |
Idempotent olmayan istekler (POST, PATCH) varsayılan olarak yalnızca güvenli durumlarda tekrar denenir: 429, 503 ve bağlantı kurulamama hataları (sunucu isteği hiç işlememiştir).
RetryPolicy sınıfı da tek başına kullanılabilir:
var retry = new RetryPolicy(new RetryOptions { MaxRetryAttempts = 5 });
using var response = await retry.ExecuteAsync(HttpMethod.Get, ct => new ValueTask<HttpResponseMessage>(httpClient.GetAsync(url, ct)));
Olaylar ve hatalar
ResilienceEventType |
Ne zaman |
|---|---|
RequestSucceeded / RequestFailed / RequestCanceled |
İstek sonuçlandığında (Elapsed, Attempt dolu) |
RetryScheduled |
Retry planlandığında (Delay = bekleme, mesajda sebep ve strateji) |
RateLimitDelayed |
İstemci tarafı token beklendiğinde (Delay = bekleme süresi) |
RateLimitRejected |
Kuyruk dolu veya bekleme çok uzun |
ServerRateLimited |
Sunucu 429 döndürdüğünde (Delay = Retry-After) |
CircuitOpened / CircuitHalfOpened / CircuitClosed |
Devre durum değişimleri |
CircuitRejected |
Açık devre nedeniyle istek gönderilmediğinde |
| Exception | Anlamı |
|---|---|
ApiRequestException (HttpRequestException'dan türer) |
Tipli metotlarda son yanıt başarısız; StatusCode, ResponseBody, SourceName içerir |
CircuitBrokenException |
Devre açık, istek gönderilmedi; RetryAfter deneme isteğine kalan süre |
RateLimitRejectedException |
İstemci tarafı kuyruk reddi; Reason, EstimatedWait |
ApiTimeoutException (TimeoutException'dan türer) |
Deneme timeout'u aşıldı |
OperationCanceledException |
Sizin CancellationToken'ınız iptal edildi |
Event handler'ları isteği işleyen thread'de senkron çalışır; hızlı tutun. Handler'da fırlatılan exception'lar loglanır ve yutulur, istek akışını bozmaz.
Demo uygulaması
samples/ResilientApiClient.Demo, ağ bağlantısı gerektirmeyen simüle edilmiş iki sunucuya 100 istek gönderir:
- CryptoExchange (gevşek: 20 req/s, burst 10) – sunucu gerçekte yalnızca 8 req/s kabul ediyor ve %7 oranında 503 dönüyor → 429 + Retry-After ve backoff + jitter retry gösterilir.
- Weather (sıkı: 5 req/s, burst 2) – istekler token bucket kuyruğunda bekler; 1.5–4.5 sn arasında sunucu çöker (HTTP 500) → circuit açılır, başarısız half-open denemesi, yeniden açılma, başarılı deneme ve kapanma gösterilir.
dotnet run --project samples/ResilientApiClient.Demo
Örnek çıktı (kısaltılmış):
+ 1,345s CryptoExchange #015 HTTP-429 Server returned 429 with Retry-After 1000 ms; pausing all requests to this source for 1000 ms.
+ 1,626s Weather #018 RETRY Retry 1/2 after HTTP 500 InternalServerError; waiting 127 ms (exponential backoff with jitter).
+ 2,326s Weather #022 THROTTLED waited 405 ms in the client-side token bucket queue
+ 2,394s Weather CIRCUIT-OPEN Circuit Closed -> Open. 5 consecutive failures. Last failure: HTTP 500 InternalServerError.
+ 2,483s Weather #028 CB-REJECT circuit Open, not sent (trial allowed in 1376 ms)
+ 3,918s Weather HALF-OPEN Circuit Open -> HalfOpen. The break duration elapsed; allowing a single trial request.
+ 3,966s Weather CIRCUIT-OPEN Circuit HalfOpen -> Open. The trial request failed: HTTP 500 InternalServerError.
+ 5,531s Weather HALF-OPEN Circuit Open -> HalfOpen. The break duration elapsed; allowing a single trial request.
+ 5,586s Weather CIRCUIT-CLOSED Circuit HalfOpen -> Closed. The trial request succeeded.
SUMMARY
Source Requests OK Failed Attempts Retries RL waits RL wait total HTTP 429 CB opened CB rejects Circuit Finished at
CryptoExchange 50 50 0 69 19 52 48,69s 9 0 0 Closed 11,33s
Weather 50 28 22 34 6 16 4,01s 0 2 22 Closed 9,22s
Retry jitter'ı ve iş parçacığı zamanlaması nedeniyle sayılar her çalıştırmada biraz farklı olur.
NuGet paketi oluşturma
dotnet pack src/ResilientApiClient -c Release -o ./artifacts
artifacts/ altında ResilientApiClient.<sürüm>.nupkg ve sembol paketi ResilientApiClient.<sürüm>.snupkg oluşur. Paket XML dokümantasyonu, bu README'yi ve MIT lisans ifadesini içerir. Sürümü değiştirmek için:
dotnet pack src/ResilientApiClient -c Release -o ./artifacts -p:Version=1.1.0
Testler
dotnet test
Token bucket, circuit breaker, retry policy ve ResilientHttpClient entegrasyonu için birim testleri tests/ResilientApiClient.Tests altındadır. Zaman bağımlı testler FakeTimeProvider kullanır.
Proje yapısı
ResilientApiClient.sln
├── src/ResilientApiClient/ # Kütüphane (NuGet paketi)
│ ├── IApiSource.cs, ApiSource.cs
│ ├── ResilientHttpClient.cs, ResilientHttpClientOptions.cs
│ ├── RateLimiting/ # RateLimiterPolicy, TokenBucket
│ ├── CircuitBreaking/ # CircuitBreaker, seçenekler, olaylar
│ ├── Retry/ # RetryPolicy (Polly), RetryOptions
│ ├── Diagnostics/ # ResilienceEventArgs, SourceStatistics
│ ├── Exceptions/
│ └── Internal/ # İstek klonlama, kaynak durumu, loglama
├── samples/ResilientApiClient.Demo/ # 100 isteklik konsol demosu
└── tests/ResilientApiClient.Tests/ # xUnit testleri
Gereksinimler
- .NET 8.0 veya üzeri
- Bağımlılıklar:
Polly.Core8.x,Microsoft.Extensions.Logging.Abstractions8.x
Lisans
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.3)
- Polly.Core (>= 8.8.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 103 | 9/27/2026 |