EF Core'da (Entity Framework Core) connection resiliency, geçici (transient) hatalar yüzünden başarısız olan veritabanı komutlarını otomatik olarak yeniden dener. SQL Server sağlayıcısında bunu UseSqlServer çağrısının sağlayıcı seçeneklerinde EnableRetryOnFailure() ile açarsınız. Açtıktan sonra her sorgu ve her SaveChangesAsync çağrısı ayrı bir yeniden denenebilir birim olur; kendi açtığınız transaction'ları ise CreateExecutionStrategy() ile alınan stratejinin içinde çalıştırmanız gerekir.

Bu yazıda yürütme stratejisi (execution strategy) kavramını, nasıl etkinleştirildiğini, bellek üzerindeki etkisini, kullanıcı transaction'larıyla birlikte doğru kullanımını ve commit sırasında bağlantı koptuğunda ortaya çıkan idempotency sorununu belgelerdeki dört çözüm yoluyla anlatıyoruz. Kendi sunucunuzda çalışan bir uygulamada ağ kesintileri veya veritabanı yeniden başlatmaları sırasında ne olacağını önceden düşünmek için iyi bir başlangıç noktasıdır. Serinin genel haritası için EF Core öğrenme rehberi sayfasına bakın.

Execution strategy nedir?

Execution strategy nedir?

Connection resiliency özelliği herhangi bir veritabanıyla kullanılabilir; bunun için bir yürütme stratejisi verirsiniz. Strateji, hataları tespit edip komutları yeniden denemek için gereken mantığı kapsüller. EF Core sağlayıcıları kendi veritabanlarının hata koşullarına ve uygun yeniden deneme politikalarına göre uyarlanmış stratejiler sunabilir.

Örneğin SQL Server sağlayıcısı, SQL Server ve Azure SQL için özel olarak uyarlanmış bir strateji içerir. Bu strateji hangi istisna türlerinin yeniden denenebileceğini bilir ve en fazla deneme sayısı, denemeler arasındaki bekleme gibi değerler için mantıklı varsayılanlara sahiptir. Yani EnableRetryOnFailure() çağrısını parametresiz yazdığınızda bu varsayılanlarla çalışan bir strateji elde edersiniz. Hangi hataların geçici sayılacağı da zamanla güncellenir; örneğin EF Core 6 sürüm notları SQL Server için daha fazla hatanın geçici kabul edilmeye başlandığını belirtir.

EnableRetryOnFailure ile etkinleştirme

Yürütme stratejisi bağlamın seçenekleri yapılandırılırken belirtilir. Bu genellikle türetilmiş bağlamınızın OnConfiguring metodunda yapılır:

csharp
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(
            @"Server=(localdb)\mssqllocaldb;Database=EFMiscellanous.ConnectionResiliency;Trusted_Connection=True;ConnectRetryCount=0",
            options => options.EnableRetryOnFailure());
}

DI kullanan bir uygulamada aynı ayar AddDbContext içinde verilir. Belgelerdeki örnek eski Startup.cs biçimindedir; aynı çağrı Program.cs içinde şöyle yazılabilir:

csharp
builder.Services.AddDbContext<PicnicContext>(
    options => options.UseSqlServer(
        "<connection string>",
        providerOptions => providerOptions.EnableRetryOnFailure()));

Azure SQL ve kontrol etmediğiniz kod

SQL Server sağlayıcısının belgelerine göre UseAzureSql ve UseAzureSynapse kullanıldığında connection resiliency, bu veritabanlarına özgü uygun ayarlarla otomatik olarak kurulur. UseSqlServer kullanıyorsanız ise sağlayıcıyı EnableRetryOnFailure ile sizin yapılandırmanız gerekir. SQL Server ve Azure SQL için aynı sağlayıcı kullanılır, ancak belgeler Azure SQL'e bağlanırken connection resiliency kullanılmasını önerir.

Bazen UseSqlServer çağrısı sizin kontrol etmediğiniz bir kodda, örneğin bir kütüphanede yapılır. EF 9'dan itibaren bu durumda resiliency'yi açmak için önceden ConfigureSqlEngine(c => c.EnableRetryOnFailureByDefault()) çağırabilirsiniz; UseAzureSql ve UseAzureSynapse ile buna gerek yoktur.

Gizli maliyet: sonuçların belleğe alınması

Yeniden denemeyi etkinleştirmek EF'nin sonuç kümesini kendi içinde belleğe almasına (buffering) neden olur. Bunun amacı, sorgu daha sonra yeniden denenirse aynı sonuçların döndürülmesini sağlamaktır. Büyük sonuç kümeleri döndüren sorgularda bellek gereksinimini önemli ölçüde artırabilir. Performans rehberi bu iç buffering'in sizin LINQ operatörlerinizle yaptığınız buffering'e ek olarak gerçekleştiğini vurgular: yeniden deneme stratejisi etkinken ToList kullanırsanız sonuç kümesi belleğe iki kez yüklenir, bir kez EF tarafından içeride, bir kez de ToList tarafından.

Bu, resiliency'den vazgeçmeniz gerektiği anlamına gelmez; ama çok büyük raporlama sorgularında sonuç kümesini sınırlamanın, yalnızca gereken kolonları seçmenin ve sayfalamanın önemi artar. Bu teknikler için performans optimizasyonu ve sayfalama yazılarına bakın. Streaming ile buffering farkı async programlama yazısında da anlatılıyor.

Özel yürütme stratejisi

Özel yürütme stratejisi

Varsayılanlardan herhangi birini değiştirmek isterseniz kendi yürütme stratejinizi kaydetme mekanizması vardır. Belgelerdeki şablon, sağlayıcı seçeneklerinde ExecutionStrategy(...) çağrısını gösterir; buradaki UseMyProvider kullandığınız sağlayıcının yer tutucusudur:

csharp
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseMyProvider(
            "<connection string>",
            options => options.ExecutionStrategy(...));
}

Özel strateji yazacaksanız EF Core 6 ile gelen ve bugün de geçerli olan bir davranışı bilmelisiniz: IExecutionStrategy singleton bir servistir. Özel uygulamalarınıza eklediğiniz durum çalıştırmalar arasında korunur ve ExecutionStrategy çağrısına verdiğiniz delegate yalnızca bir kez çalıştırılır. ExecutionStrategy sınıfından türeyen uygulamalar durumu OnFirstExecution içinde temizlemelidir; delegate içindeki koşullu mantık ise özel bir IExecutionStrategy uygulamasına taşınmalıdır. Hangi hataların geçici sayılacağını değiştirmek için SQL Server'da SqlServerRetryingExecutionStrategy sınıfından türetilmiş bir strateji kullanılabilir.

Execution strategy ve kullanıcı transaction'ları

Hatalarda otomatik yeniden deneyen bir stratejinin, başarısız olan her işlemi bir yeniden deneme bloğu içinde baştan oynatabilmesi gerekir. Yeniden deneme etkinken EF Core üzerinden yaptığınız her işlem kendi başına yeniden denenebilir bir işleme dönüşür: her sorgu ve her SaveChangesAsync() çağrısı, geçici bir hata olduğunda bir birim olarak yeniden denenir.

Ancak kodunuz BeginTransactionAsync() ile bir transaction başlatırsa, birlikte ele alınması gereken kendi işlem grubunuzu tanımlamış olursunuz; hata olduğunda transaction içindeki her şeyin yeniden oynatılması gerekir. Strateji bunu kendiliğinden yapamaz. Belgelerde gösterildiği gibi şuna benzer bir istisna alırsınız:

Mesaj çözümü de söyler: yapılandırılmış strateji kullanıcı tarafından başlatılan transaction'ları desteklemez; transaction içindeki tüm işlemleri tek bir yeniden denenebilir birim olarak çalıştırmak için CreateExecutionStrategy() ile dönen stratejiyi kullanın. Yani yürütülmesi gereken her şeyi temsil eden bir delegate ile stratejiyi elle çağırırsınız; geçici bir hata olursa strateji delegate'i yeniden çağırır:

csharp
using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();

await strategy.ExecuteAsync(
    async () =>
    {
        using var context = new BloggingContext();
        await using var transaction = await context.Database.BeginTransactionAsync();

        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        await context.SaveChangesAsync();

        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
        await context.SaveChangesAsync();

        await transaction.CommitAsync();
    });

Bu örnekte dikkat çekici bir ayrıntı var: transaction'ı kullanan bağlam delegate'in içinde oluşturulur. Delegate yeniden çalıştırıldığında, önceki denemede izlemeye alınmış varlıklarla dolu bir bağlam yerine her seferinde temiz bir bağlamla baştan başlanır. Delegate'in içindeki her şeyin yeniden oynatılabilir olması gerekir; delegate içinde örneğin e-posta göndermek gibi veritabanı dışı bir yan etki varsa, o yan etki de tekrarlanabilir. Aynı yaklaşım ortam transaction'ları için de çalışır: delegate içinde bir TransactionScope açıp iki farklı bağlamın kayıtlarını onun içinde yapabilir, sonunda Complete() çağırabilirsiniz. Transaction API'lerinin tamamı transaction yönetimi yazısında.

Migration uygularken

EF Core 9'dan itibaren Migrate ve MigrateAsync çağrıları kendileri bir transaction başlatır ve komutları bir ExecutionStrategy ile yürütür. Eskiden yaygın olan, migration'ı elle açılmış bir transaction ve strateji içine sarma deseni bu yüzden artık MigrationsUserTransactionWarning uyarısından üretilen bir hataya yol açar. Transaction içinde tek bir veritabanı çağrısı varsa dış transaction'ı ve stratejiyi kaldırıp doğrudan MigrateAsync çağırmanız önerilir. Migration'ları üretimde uygulamanın önerilen yolları için migration production rehberi yazısına bakın.

Commit sırasında bağlantı koparsa: idempotency sorunu

Commit sırasında bağlantı koparsa: idempotency sorunu: 1. Neredeyse hiçbir şey yapmamak, 2. Uygulama durumunu yeniden kurmak, 3. Durum doğrulaması eklemek, 4. Transaction'ı elle...

Genel olarak bağlantı hatası olduğunda o anki transaction geri alınır. Ancak bağlantı tam transaction commit edilirken koparsa transaction'ın sonucu bilinmez: commit veritabanına ulaşmış da olabilir, ulaşmamış da. Varsayılan olarak yürütme stratejisi işlemi transaction geri alınmış gibi yeniden dener. Gerçekte commit başarılı olmuşsa bu yeniden deneme iki sonuçtan birine yol açar: yeni veritabanı durumu işlemle uyumsuzsa bir istisna, işlem belirli bir duruma dayanmıyorsa veri bozulması. Belgelerin verdiği örnek, otomatik üretilen anahtarla yeni bir satır eklemektir; yeniden deneme aynı veriyi ikinci kez ekler.

Belgeler bu duruma karşı dört yol sunar. Hangisinin uygun olduğu, işleminizin doğasına ve tekrarlanan bir işlemin ne kadar zarar vereceğine bağlıdır.

1. Neredeyse hiçbir şey yapmamak

Commit sırasında bağlantı hatası olasılığı düşüktür; bu durum gerçekleştiğinde uygulamanızın yalnızca hata vermesi kabul edilebilir olabilir. Ancak bu yolda veritabanının ürettiği anahtarlardan kaçınmanız gerekir; böylece yinelenen satır eklemek yerine istisna fırlatılması sağlanır. Belgeler istemci tarafında üretilen bir GUID değeri veya istemci tarafı değer üreticisi kullanmayı önerir. Değer üretimi seçenekleri için değer üretimi yazısına bakabilirsiniz.

2. Uygulama durumunu yeniden kurmak

Mevcut DbContext'i atarsınız, yeni bir bağlam oluşturup uygulamanızın durumunu veritabanından yeniden yüklersiniz ve kullanıcıya son işlemin başarıyla tamamlanmamış olabileceğini bildirirsiniz. Bu yol, kullanıcının sonucu görüp kararı kendisinin verebildiği etkileşimli uygulamalar için uygundur.

3. Durum doğrulaması eklemek

Veritabanı durumunu değiştiren işlemlerin çoğunda, işlemin başarılı olup olmadığını kontrol eden kod yazmak mümkündür. EF bunu kolaylaştırmak için IExecutionStrategy.ExecuteInTransaction genişletme metodunu sağlar. Bu metot transaction'ı başlatır ve commit eder; ayrıca commit sırasında geçici bir hata olduğunda çağrılan bir fonksiyonu verifySucceeded parametresinde alır:

csharp
using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();

var blogToAdd = new Blog { Url = "http://blogs.msdn.com/dotnet" };
db.Blogs.Add(blogToAdd);

await strategy.ExecuteInTransactionAsync(
    db,
    operation: (context, cancellationToken) => context.SaveChangesAsync(acceptAllChangesOnSuccess: false, cancellationToken),
    verifySucceeded: (context, cancellationToken) => context.Blogs.AsNoTracking().AnyAsync(b => b.BlogId == blogToAdd.BlogId, cancellationToken));

db.ChangeTracker.AcceptAllChanges();

Buradaki ince nokta acceptAllChangesOnSuccess: false argümanıdır. Normalde başarılı bir SaveChanges, izlenen varlıkların durumunu Unchanged yapar. Bu argüman o geçişi engeller; böylece commit başarısız olup transaction geri alındığında aynı işlem, Blog hâlâ eklenmeyi bekleyen durumdayken yeniden denenebilir. Her şey bittikten sonra ChangeTracker.AcceptAllChanges() çağrısı durumları elle kabul eder. Varlık durumlarının ayrıntısı için change tracker ve entity state yazısına bakın.

4. Transaction'ı elle izlemek

Veritabanının ürettiği anahtarları kullanmanız gerekiyorsa veya yapılan işleme bağlı olmayan genel bir çözüm istiyorsanız, her transaction'a bir kimlik atayıp commit başarısız olduğunda bu kimliği kontrol edebilirsiniz. Adımlar şöyledir: veritabanına transaction durumunu izleyen bir tablo eklersiniz; her transaction'ın başında bu tabloya bir satır yazarsınız; commit sırasında bağlantı koparsa ilgili satırın varlığını kontrol edersiniz; commit başarılıysa tablonun büyümemesi için satırı silersiniz.

csharp
using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();

db.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });

var transaction = new TransactionRow { Id = Guid.NewGuid() };
db.Transactions.Add(transaction);

await strategy.ExecuteInTransactionAsync(
    db,
    operation: (context, cancellationToken) => context.SaveChangesAsync(acceptAllChangesOnSuccess: false, cancellationToken),
    verifySucceeded: (context, cancellationToken) => context.Transactions.AsNoTracking().AnyAsync(t => t.Id == transaction.Id, cancellationToken));

db.ChangeTracker.AcceptAllChanges();
db.Transactions.Remove(transaction);
await db.SaveChangesAsync();
YolNe yaparUygun olduğu durum
1. Neredeyse hiçbir şeyHata vermeyi kabul eder, istemci tarafı anahtar kullanırNadir hatanın kabul edilebildiği işlemler
2. Durumu yeniden kurmakBağlamı atar, veriyi yeniden yükler, kullanıcıyı uyarırEtkileşimli uygulamalar
3. Durum doğrulamasıExecuteInTransactionAsync ve verifySucceededBaşarının sorguyla doğrulanabildiği işlemler
4. Transaction izlemeHer transaction için bir izleme satırıVeritabanı üretimli anahtarlar, genel çözüm

Yeniden denemeleri izlemek

EF Core'un metrikleri arasında yürütme stratejisi işlem hatalarını sayan bir sayaç bulunur. Belgelere göre yeniden deneyen bir strateji etkinse bu sayı, aynı işlem üzerindeki birden fazla denemenin her bir hatasını içerir ve altyapınızdaki geçici sorunları tespit etmek için kullanılabilir. Yeniden denemeler hatayı kullanıcıdan gizlediği için, bu sayaç olmadan ağınızda veya veritabanı sunucunuzda süregelen bir sorunu fark etmeyebilirsiniz. Metrikler ve loglama için SQL loglama ve interceptor yazısına bakın.

EF Core 10 sürüm notlarında Azure Cosmos DB sağlayıcısı için de bir iyileştirme yer alıyor: sorgu yürütmede yeniden deneme amacıyla ExecutionStrategy kullanılıyor. Diğer sağlayıcılar için hangi hataların geçici sayıldığı ve varsayılan değerler sağlayıcının kendi belgelerinde açıklanır; sağlayıcı listesi için veritabanı sağlayıcıları yazısına bakın.

Sık yapılan hatalar

  • Retry açıkken stratejisiz BeginTransaction: Kullanıcı transaction'ı doğrudan açılırsa yukarıdaki InvalidOperationException alınır; transaction bloğunu CreateExecutionStrategy().ExecuteAsync içine alın.
  • Delegate dışında oluşturulan bağlamı delegate içinde kullanmak: Yeniden denemede önceki denemeden kalan izleme durumu sorun çıkarabilir; belgelerdeki örnek bağlamı delegate içinde oluşturur.
  • Delegate içinde tekrarlanmaması gereken yan etkiler: Strateji delegate'i baştan çalıştırır; tekrarlanınca zarar verecek işleri dışarı alın.
  • Commit belirsizliğini yok saymak: Veritabanı üretimli anahtarlarla satır ekleyen işlemlerde yeniden deneme yinelenen satırlara yol açabilir.
  • Büyük sonuç kümelerinde belleği hesaba katmamak: Retry, sonuçların EF içinde buffer'lanmasına neden olur; ToList ile birlikte veri iki kez belleğe alınır.
  • Migration'ı dış transaction ile sarmak: EF 9 ve sonrasında MigrateAsync kendi transaction'ını ve stratejisini yönetir.
  • Özel stratejide durum tutmak: IExecutionStrategy singleton olduğundan durum çalıştırmalar arasında korunur.

Sık Sorulan Sorular

EnableRetryOnFailure kaç kez dener?

SQL Server stratejisi en fazla deneme sayısı ve denemeler arası bekleme için kendi varsayılanlarıyla gelir; belgeler bu değerleri mantıklı varsayılanlar olarak tanımlar ama bu sayfada sayı vermez. Değiştirmek isterseniz özel bir yürütme stratejisi kaydedebilirsiniz.

Kendi sunucumdaki SQL Server için de retry açmalı mıyım?

Belgeler özellikle Azure SQL için öneride bulunur ve UseAzureSql ile resiliency otomatik gelir. Kendi sunucunuzdaki veritabanı için karar, ağ kesintisi ve yeniden başlatma olasılığına ve yukarıdaki bellek ile transaction kısıtlarına göre verilmelidir.

Retry açıkken neden transaction hatası alıyorum?

Çünkü strateji, sizin başlattığınız transaction içindeki işlemleri tek tek yeniden deneyemez. Transaction'ı içeren kodun tamamını CreateExecutionStrategy().ExecuteAsync ile çalıştırdığınızda strateji tüm bloğu birim olarak yeniden oynatır.

Commit sırasında kopma ne kadar olası?

Belgeler bu olasılığı düşük olarak nitelendirir; bu yüzden birinci seçenek olarak yalnızca hata vermeyi kabul edilebilir sayar. Yinelenen kaydın zararlı olduğu işlemlerde ise doğrulamalı ExecuteInTransactionAsync veya transaction izleme tablosu kullanılır.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: EF Core öğrenme rehberi · Transaction yönetimi · Migration production · Connection string yönetimi · SQL loglama · Change tracker