ResilientApiClient 1.0.0

dotnet add package ResilientApiClient --version 1.0.0
                    
NuGet\Install-Package ResilientApiClient -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ResilientApiClient" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ResilientApiClient" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="ResilientApiClient" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ResilientApiClient --version 1.0.0
                    
#r "nuget: ResilientApiClient, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ResilientApiClient@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ResilientApiClient&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=ResilientApiClient&version=1.0.0
                    
Install as a Cake Tool

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-After header'ı 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 ResilienceEvent event'i, kaynak bazlı istatistikler ve opsiyonel ILogger entegrasyonu.
  • Tamamen async, her yerde CancellationToken desteğ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);

ResilientHttpClient uzun ö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.Core 8.x, Microsoft.Extensions.Logging.Abstractions 8.x

Lisans

MIT

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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