EFEF Core Handbook

UZMAN

Connection Resiliency & DbContext Pooling

Bulut ortamlarında geçici ağ hataları kaçınılmaz. Retry mekanizması yoksa uygulama ilk geçici hatada çöker. Connection Resiliency otomatik yeniden deneme sağlar; DbContext Pooling ise context nesne oluşturma maliyetini azaltarak throughput artırır.

Connection Resiliency (Bağlantı Dayanıklılığı)

// Program.cs — retry stratejisi
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString, sqlOptions =>
    {
        sqlOptions.EnableRetryOnFailure(
            maxRetryCount: 5,                         // Maks deneme
            maxRetryDelay: TimeSpan.FromSeconds(30),  // Denemeler arası maks bekleme
            errorNumbersToAdd: null);                 // Ek SQL error code'ları
    }));

Özel Retry Stratejisi

// Kendi execution strategy'nizi yazabilirsiniz
public class CustomRetryStrategy : SqlServerRetryingExecutionStrategy
{
    public CustomRetryStrategy(DbContext context)
        : base(context, maxRetryCount: 3, maxRetryDelay: TimeSpan.FromSeconds(10),
               errorNumbersToAdd: new[] { 4060, 4221 }) // Login failed, timeout
    { }

    protected override bool ShouldRetryOn(Exception exception)
    {
        // Özel logic: sadece belirli hatalarda retry yap
        if (exception is SqlException sqlEx)
        {
            // Deadlock (1205) ve Timeout (-2)
            return sqlEx.Number is 1205 or -2;
        }
        return base.ShouldRetryOn(exception);
    }
}

// Kullanım
options.UseSqlServer(connectionString, sql =>
    sql.ExecutionStrategy(ctx => new CustomRetryStrategy(ctx)));

Manual Transaction ile Retry

// ⚠️ Retry etkin ise, transaction'lar özel handling gerektirir!
var strategy = context.Database.CreateExecutionStrategy();

await strategy.ExecuteAsync(async () =>
{
    await using var transaction = await context.Database.BeginTransactionAsync();
    try
    {
        // İşlemler...
        context.Orders.Add(order);
        await context.SaveChangesAsync();

        context.Inventory.Update(stock);
        await context.SaveChangesAsync();

        await transaction.CommitAsync();
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
});

İdempotency Kuralları

Retry mekanizması aynı işlemi tekrar çalıştırabileceği için işlemlerin idempotent olması gerekir:

Kural Neden
Unique constraint + upsert Retry aynı INSERT'i tekrar çalıştırabilir
Idempotency key (request ID) Aynı işlemin 2 kez uygulanmasını engeller
SELECT before INSERT Retry'da "zaten var" kontrolü
// İdempotent kayıt oluşturma
var strategy = context.Database.CreateExecutionStrategy();

await strategy.ExecuteAsync(async () =>
{
    var exists = await context.Payments
        .AnyAsync(p => p.IdempotencyKey == request.RequestId);

    if (!exists)
    {
        context.Payments.Add(new Payment
        {
            IdempotencyKey = request.RequestId,
            Amount = request.Amount,
            OrderId = request.OrderId
        });
        await context.SaveChangesAsync();
    }
});

Retry Davranışı (Exponential Backoff)

Deneme 1 → Hata (transient) → Bekle ~1s
Deneme 2 → Hata (transient) → Bekle ~3s  (exponential backoff + jitter)
Deneme 3 → Hata (transient) → Bekle ~7s
Deneme 4 → Başarılı ✓

EF retry sadece transient hatalar için çalışır. Constraint violation, syntax error gibi kalıcı hatalar anında fırlatılır.

Advanced: Built-in vs Polly Karşılaştırması

Özellik EF Core Built-in Microsoft.Extensions.Resilience (Polly v8)
Retry
Circuit Breaker
Timeout
Fallback
Hedging (parallel request)
Bulkhead (concurrency limit)
Telemetry entegrasyonu Temel OpenTelemetry native

Ne zaman Polly gerekir?

  • Circuit breaker lazımsa (DB sürekli down → fast-fail, cascade engelle)
  • Timeout kontrolü istiyorsan (EF retry sonsuza kadar bekleyebilir)
  • Fallback stratejisi (read-replica'ya düş, cache'den oku)
  • Multi-service senaryosu (HTTP + DB + Redis hepsi aynı pipeline'da)
// NuGet: Microsoft.Extensions.Resilience (>= 8.0.0)
// Gereksinim: .NET 8+ ve Polly v8

// EF Core execution strategy'yi KAPAT, Polly'ye devret:
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString, sql =>
        sql.ExecutionStrategy(deps => new NonRetryingExecutionStrategy(deps))));

// Polly pipeline tanımla:
builder.Services.AddResiliencePipeline("db-pipeline", pipeline =>
{
    pipeline
        .AddRetry(new RetryStrategyOptions
        {
            MaxRetryAttempts = 5,
            BackoffType = DelayBackoffType.Exponential,
            ShouldHandle = new PredicateBuilder().Handle<SqlException>(ex => ex.IsTransient)
        })
        .AddCircuitBreaker(new CircuitBreakerStrategyOptions
        {
            FailureRatio = 0.5,
            SamplingDuration = TimeSpan.FromSeconds(10),
            MinimumThroughput = 8,
            BreakDuration = TimeSpan.FromSeconds(30)
        })
        .AddTimeout(TimeSpan.FromSeconds(15));
});

// Kullanım:
public class OrderService
{
    private readonly ResiliencePipeline _pipeline;
    private readonly AppDbContext _context;

    public OrderService(
        [FromKeyedServices("db-pipeline")] ResiliencePipeline pipeline,
        AppDbContext context)
    {
        _pipeline = pipeline;
        _context = context;
    }

    public async Task<Order?> GetOrderAsync(int id)
    {
        return await _pipeline.ExecuteAsync(async ct =>
            await _context.Orders.FindAsync([id], ct),
            CancellationToken.None);
    }
}

Tavsiye: Çoğu proje için EF Core'un built-in retry'ı yeterlidir. Polly'ye geçiş sadece circuit breaker veya multi-strategy ihtiyacı olduğunda değer yaratır.

DbContext Pooling

// ❌ Normal — her request'te yeni DbContext oluşturulur
builder.Services.AddDbContext<AppDbContext>(options => ...);

// ✅ Pooling — DbContext instance'ları yeniden kullanılır
builder.Services.AddDbContextPool<AppDbContext>(options =>
    options.UseSqlServer(connectionString),
    poolSize: 1024);  // Varsayılan: 1024

Pooling Farkı

Yöntem Davranış Bellek
AddDbContext Her request'te yeni instance oluşturur Her seferinde allocation + GC baskısı
AddDbContextPool Havuzdan hazır instance alır, işi bitince geri koyar Sabit havuz, GC yükü minimal

Pooling kısıtlamaları:

  • Constructor'da DI kullanamazsın (singleton gibi davranır)
  • DbContext'te state tutma (her kullanımda sıfırlanır)
  • OnConfiguring override etme
PostgreSQL Connection & PgBouncer

Connection & Pooling Limitleri:

Limit PostgreSQL SQL Server
Max eş zamanlı bağlantı (varsayılan) 100 32.767
Max bağlantı (artırılabilir) ~500-1000 (RAM'e bağlı) 32.767
Connection string Max Pool Size (varsayılan) 100 (Npgsql) 100
DbContext Pool Size (varsayılan) 1024 1024

PostgreSQL connection limiti neden düşük?

  • PG her connection için ayrı bir process fork eder (~5-10 MB RAM/connection)
  • 100 bağlantı = ~1 GB RAM sadece connection'lar için
  • SQL Server thread-based → binlerce bağlantı daha ucuz

Çözüm: Connection Pooler (PgBouncer / Supavisor)

Uygulama (1000 request) → PgBouncer (50 connection) → PostgreSQL (50 backend)
// Connection string'de pool ayarları:
"Host=localhost;Database=mydb;Username=app;Password=***;"
+ "Maximum Pool Size=50;"     // Npgsql iç pool: max 50 bağlantı
+ "Minimum Pool Size=5;"      // Başlangıçta 5 bağlantı hazır tut
+ "Connection Idle Lifetime=60;"  // 60 sn boş kalan bağlantıyı kapat
+ "Timeout=30;"               // Havuzda boş connection yoksa 30 sn bekle

// PgBouncer kullanılıyorsa:
// - Npgsql pool size = PgBouncer'daki pool size ile eşleş
// - "No Reset On Close=true" ekle (PgBouncer session modda)
// - Prepared statements devre dışı: "Max Auto Prepare=0"

Yaygın hata: "Npgsql.NpgsqlException: The connection pool has been exhausted"

  • Sebep: Bağlantılar geri verilmiyor (DbContext Dispose edilmemiş veya long-running transaction)
  • Çözüm: using kullan, DI lifetime doğru ayarla (Scoped), pool size artır