EF Core'da (Entity Framework Core) DbContext pooling, dispose edilen bağlam örneklerini sıfırlayıp bir iç havuzda saklar ve bir sonraki istekte yenisini kurmak yerine bunu geri verir; etkinleştirmek için AddDbContext yerine AddDbContextPool yazmanız yeterlidir. Compiled query ise bir LINQ sorgusunu EF.CompileAsyncQuery ile önceden bir .NET delegate'ine derleyerek EF'nin her çalıştırmada yaptığı sorgu önbelleği aramasını atlar. İkisi de yüksek performans gereken senaryolar içindir ve çoğu uygulamada ilk yapılacak iş değildir.

Bu yazıda pooling'in nasıl çalıştığını, havuzlanmış bağlamlarda durum (state) yönetimini, veritabanı bağlantı havuzuyla farkını, compiled query'nin sınırlarını ve aynı aileden compiled model ile deneysel NativeAOT ve precompiled query desteğini resmî belgelere dayanarak anlatıyoruz. Sorgu verimliliği, index ve N+1 gibi daha temel konular EF Core performans optimizasyonu yazısında; serinin tamamı EF Core öğrenme rehberi sayfasında.

DbContext pooling ne işe yarar?

DbContext pooling ne işe yarar? DI ile: AddDbContextPool, DI olmadan: PooledDbContextFactory

DbContext genellikle hafif bir nesnedir: oluşturmak ve dispose etmek veritabanı işlemi gerektirmez ve çoğu uygulama bunu fark edilir bir performans etkisi olmadan yapabilir. Ancak her bağlam örneği görevini yerine getirmek için çeşitli iç servisler ve nesneler kurar. Bunu sürekli tekrarlamanın yükü, yüksek performanslı senaryolarda anlamlı hale gelebilir.

Pooling bu yükü ortadan kaldırır. Bağlamı dispose ettiğinizde EF Core onun durumunu sıfırlar ve iç havuza koyar; bir sonraki istekte yeni bir örnek kurmak yerine havuzdaki örneği verir. Böylece bağlam kurulum maliyetini sürekli değil, yalnızca program başlangıcında bir kez ödersiniz. Belgelerdeki örnek ölçüm, aynı makinede çalışan yerel SQL Server'dan tek satır okuyan tek thread'li bir testte pooling ile ortalama sürenin yaklaşık yarıya, ayrılan belleğin ise onda birin altına indiğini gösterir. Belgeler aynı yerde sonuçların satır sayısına, veritabanı gecikmesine ve gerçek dünyadaki rekabetli yüke göre değişeceğini ve karar vermeden önce kendi platformunuzda ölçmeniz gerektiğini vurgular.

DI ile: AddDbContextPool

ASP.NET Core uygulamasındaki tipik desen, bağlam türünü AddDbContext ile DI kapsayıcısına kaydetmek ve yapıcı parametresiyle almaktır. Pooling'i etkinleştirmek için bu çağrıyı AddDbContextPool ile değiştirirsiniz:

csharp
builder.Services.AddDbContextPool<WeatherForecastContext>(
    o => o.UseSqlServer(builder.Configuration.GetConnectionString("WeatherForecastContext")));

poolSize parametresi havuzda tutulacak en fazla örnek sayısını belirler ve varsayılanı 1024'tür. Bu sayı aşıldığında yeni örnekler önbelleğe alınmaz; EF havuzsuz davranışa döner ve gerektiğinde yeni örnek oluşturur. Performans rehberi bu değerin kullanım senaryonuza uyması gerektiğini söyler: çok düşükse bağlamlar sürekli oluşturulup dispose edilir, çok yüksekse kullanılmayan örnekler gereksiz bellek tüketir.

DI olmadan: PooledDbContextFactory

Konsol uygulaması veya arka plan işi gibi DI kullanmayan kodda PooledDbContextFactory oluşturup bağlamları ondan istersiniz:

csharp
var options = new DbContextOptionsBuilder<PooledBloggingContext>()
    .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Blogging;Trusted_Connection=True;ConnectRetryCount=0")
    .Options;

var factory = new PooledDbContextFactory<PooledBloggingContext>(options);

using (var context = factory.CreateDbContext())
{
    var allPosts = await context.Posts.ToListAsync();
}

Bu fabrikanın yapıcısındaki poolSize parametresi de aynı anlama gelir ve varsayılanı yine 1024'tür. Belgeler, çok küçük bir ek kazanç için DI'ın bağlamı doğrudan enjekte etmesi yerine PooledDbContextFactory kullanmayı düşünebileceğinizi, çünkü DI'ın pooling yönetiminin küçük bir ek yükü olduğunu not eder. DI içinde fabrika istiyorsanız AddPooledDbContextFactory kaydını kullanırsınız.

Havuzlanmış bağlamlarda durum yönetimi

Pooling aynı bağlam örneğini istekler arasında yeniden kullandığı için örnek fiilen singleton gibi davranır. Bu yüzden bağlamda istekten isteğe değişen bir durum varsa özel dikkat gerekir. En kritik ayrıntı şudur: bağlamın OnConfiguring metodu yalnızca örnek ilk oluşturulduğunda bir kez çağrılır. Dolayısıyla kiracı (tenant) kimliği gibi her istekte değişmesi gereken bir değeri OnConfiguring içinde ayarlayamazsınız.

Belgelerdeki tipik senaryo, sorgularda kiracı kimliğini dikkate alan çok kiracılı bir uygulamadır. Pooling ile birlikte çalışması için birkaç ek adım gerekir. Önce kiracı bilgisini taşıyan scoped bir ITenant servisi kaydedilir. Ardından havuzlu fabrika her zamanki gibi AddPooledDbContextFactory ile singleton olarak kaydedilir. Sonra havuzdan bağlam alıp ona kiracı kimliğini yerleştiren özel bir fabrika yazılır ve scoped olarak kaydedilir; son olarak bağlamın kendisi bu fabrikadan üretilecek şekilde kaydedilir:

csharp
builder.Services.AddPooledDbContextFactory<WeatherForecastContext>(
    o => o.UseSqlServer(builder.Configuration.GetConnectionString("WeatherForecastContext")));

public class WeatherForecastScopedFactory : IDbContextFactory<WeatherForecastContext>
{
    private const int DefaultTenantId = -1;

    private readonly IDbContextFactory<WeatherForecastContext> _pooledFactory;
    private readonly int _tenantId;

    public WeatherForecastScopedFactory(
        IDbContextFactory<WeatherForecastContext> pooledFactory,
        ITenant tenant)
    {
        _pooledFactory = pooledFactory;
        _tenantId = tenant?.TenantId ?? DefaultTenantId;
    }

    public WeatherForecastContext CreateDbContext()
    {
        var context = _pooledFactory.CreateDbContext();
        context.TenantId = _tenantId;
        return context;
    }
}

builder.Services.AddScoped<WeatherForecastScopedFactory>();
builder.Services.AddScoped(
    sp => sp.GetRequiredService<WeatherForecastScopedFactory>().CreateDbContext());

Bu kurulumla bağlamı enjekte eden servisler, kiracı kimliği hakkında hiçbir şey bilmeden doğru kimliği taşıyan bir örnek alır. Belgeler önemli bir güvenlik notu ekler: örnekteki minimal çözüm kiracı kimliğini sorgu dizesinden okur, bu da istemcinin herhangi bir kiracıyı taklit edebilmesi demektir. Gerçek bir uygulamada kiracı kimliği güvenli kimlik doğrulama verisinden türetilmelidir. Kiracı filtresinin kendisi ve çok kiracılı tasarım global query filter ve soft delete yazısında ele alınıyor.

Bağlantı havuzu ile farkı

Çoğu veritabanında işlem yapmak için uzun ömürlü bir bağlantı gerekir ve bu bağlantıları açıp kapatmak pahalıdır. EF bağlantı havuzlamasını kendisi uygulamaz; bu iş ADO.NET sürücüsü gibi alttaki veritabanı sürücüsüne aittir. Bağlantı havuzu istemci tarafında var olan bağlantıları yeniden kullanarak tekrar tekrar açıp kapatma yükünü azaltır. Genellikle varsayılan olarak açıktır ve yapılandırması o sürücünün belgelediği şekilde, ADO.NET'te çoğunlukla bağlantı dizesindeki en küçük ve en büyük havuz boyutu gibi parametrelerle yapılır. Bağlantı dizesi yönetimi için connection string yönetimi yazısına bakın.

Bağlamın havuzlanıp havuzlanmamasından bağımsız olarak EF, genellikle her işlemden, örneğin her sorgudan hemen önce bağlantıyı açar ve hemen ardından kapatır; kapatılan bağlantı sürücünün havuzuna döner. Böylece bağlantılar gereğinden uzun süre havuz dışında tutulmaz. Özetle iki mekanizma birbirini tamamlar:

ÖzellikDbContext poolingBağlantı havuzu
Neyi yeniden kullanırDbContext örneklerini ve iç servisleriniVeritabanı bağlantılarını
Hangi katman yönetirEF CoreVeritabanı sürücüsü (ör. ADO.NET)
Nasıl etkinleşirAddDbContextPool veya PooledDbContextFactoryGenellikle varsayılan olarak açık
Nasıl yapılandırılırpoolSize parametresiSürücüye özgü, çoğunlukla bağlantı dizesi
Önlediği maliyetBağlam bellek ayırma ve başlatmaBağlantı açma ve kapatma

Tanılama için EF Core'un metrikleri de yardımcı olur. "Active DbContexts" sayacı uygulamadaki dispose edilmemiş bağlam sayısını gösterir; bu sayı sürekli artıyorsa bağlamlar düzgün dispose edilmiyor olabilir. Belgelere göre pooling etkinse bu sayıya havuzda bekleyen kullanılmayan örnekler de dahildir, bu yüzden yüksek bir değeri tek başına sızıntı belirtisi saymamalısınız. Metrikler ve loglama için SQL loglama ve interceptor yazısına bakın.

Compiled query: sorgu önbelleği aramasını atlamak

Compiled query: sorgu önbelleği aramasını atlamak: Compiled query'ler yalnızca tek bir EF Core modeline karşı kullanılabilir. Aynı..., Parametreler basit, skaler değerler...

EF bir LINQ sorgu ağacını çalıştırmadan önce onu derlemek, yani ondan SQL üretmek zorundadır. Bu ağır bir iş olduğundan EF sorguları ağaç şekline göre önbelleğe alır; aynı yapıdaki sorgular, parametre değerleri farklı olsa bile önbellekteki derleme çıktısını yeniden kullanır. Ancak önbelleği kullanabilmek için bile EF'nin bazı işler yapması gerekir: sorgunuzun ifade ağacı, önbellekteki sorguların ağaçlarıyla özyinelemeli olarak karşılaştırılır. Bu yük çoğu uygulamada ağ G/Ç'si ve veritabanındaki sorgu işleme maliyetinin yanında önemsizdir; yine de bazı yüksek performanslı senaryolarda ortadan kaldırmak istenebilir.

Compiled query, bir LINQ sorgusunu açıkça bir .NET delegate'ine derlemenizi sağlar. Delegate'i bir kez aldıktan sonra ifade ağacı vermeden doğrudan çağırabilirsiniz; önbellek araması atlanır. Async sorgular için EF.CompileAsyncQuery, senkron sorgular için EF.CompileQuery kullanılır:

csharp
private static readonly Func<BloggingContext, int, IAsyncEnumerable<Blog>> _compiledQuery
    = EF.CompileAsyncQuery(
        (BloggingContext context, int length) => context.Blogs.Where(b => b.Url.StartsWith("http://") && b.Url.Length == length));

// Kullanim
await foreach (var blog in _compiledQuery(context, 8))
{
    // Sonuclarla calisin
}

Lambda, bir bağlam örneği ve sorguya geçirilecek bir parametre alır. Delegate'i genellikle static readonly bir alanda bir kez oluşturursunuz. Belgelere göre bu delegate thread güvenlidir ve farklı bağlam örnekleri üzerinde eşzamanlı çağrılabilir; bağlamın kendisi ise yine thread güvenli değildir. Belgelerdeki örnek ölçümde compiled query, basit bir sorgunun ortalama süresini yaklaşık yüzde 9 ile 16 arasında kısaltır; rehber, sorgu ne kadar karmaşıksa, yani ne kadar çok operatör ve büyük ifade ağacı içeriyorsa, kazancın o kadar büyük olmasının beklendiğini söyler.

Compiled query sınırlamaları

  • Compiled query'ler yalnızca tek bir EF Core modeline karşı kullanılabilir. Aynı türdeki farklı bağlam örnekleri bazen farklı modeller kullanacak şekilde yapılandırılabilir; bu senaryoda compiled query desteklenmez.
  • Parametreler basit, skaler değerler olmalıdır. Örnek üyelerine veya metotlara erişim gibi daha karmaşık parametre ifadeleri desteklenmez.
  • Dinamik olarak kurulan sorgularla compiled query optimizasyonu kullanılamaz.

Compiled query'ye geçmeden önce sorgularınızın önbelleği zaten verimli kullandığından emin olun. Sabit değerler yerine değişken kullanan sorgular aynı ağaç şeklini paylaşır ve bir kez derlenir; ifade API'siyle her seferinde farklı sabitler gömen dinamik sorgular ise önbelleği sürekli kirletir. EF metriklerindeki sorgu önbelleği isabet oranı, normal bir uygulamada başlangıçtan kısa süre sonra yüzde yüze ulaşır; sürekli bunun altında kalıyorsa önbelleği bozan bir şey olduğunu gösterir. Parametreleştirme ayrıntısı performans optimizasyonu yazısının konusudur.

Compiled model: büyük modellerde başlangıç süresi

Compiled model, yüzlerce veya binlerce varlık türü ve ilişki içeren büyük modellerde EF Core'un başlangıç süresini kısaltabilir. Burada başlangıç süresi, bir DbContext türü uygulamada ilk kez kullanıldığında ilk işlemin süresidir. Yalnızca bağlam örneği oluşturmak modeli başlatmaz; modeli başlatan tipik ilk işlemler DbContext.Add çağrısı veya ilk sorgunun yürütülmesidir.

Compiled model, dotnet ef komut satırı aracındaki dbcontext optimize komutuyla üretilir. --output-dir ve --namespace seçenekleri üretilen kodun klasörünü ve ad alanını belirler. Araçların kurulumu ve diğer komutlar için dotnet ef komutları yazısına bakın.

bash
dotnet ef dbcontext optimize --output-dir MyCompiledModels --namespace MyCompiledModels

Komutun çıktısı, bağlam yapılandırmanıza yapıştırmanız için bir kod parçası içerir. Belgelerdeki örnek çıktıda araç, modeli kullanmak için options.UseModel(MyCompiledModels.BlogsContextModel.Instance) çağrısını önerir ve model değiştiğinde komutun yeniden çalıştırılması gerektiğini hatırlatır. Yapılandırma şöyle görünür:

csharp
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseModel(MyCompiledModels.BlogsContextModel.Instance)
        .UseSqlite(@"Data Source=test.db");

Compiled model sınırlamaları

  • Global query filter'lar desteklenmez.
  • Lazy loading ve change-tracking proxy'leri desteklenmez.
  • Private metotlara başvuran value converter'lar desteklenmez; başvurulan metotları public veya internal yapmanız gerekir.
  • Model tanımı veya yapılandırması her değiştiğinde model yeniden üretilerek elle eşitlenmelidir.
  • Özel IModelCacheKeyFactory uygulamaları desteklenmez; bunun yerine birden fazla model derleyip uygun olanı çalışma zamanında yükleyebilirsiniz.

Bu sınırlamalar nedeniyle belgeler compiled model'i yalnızca EF Core başlangıç süreniz gerçekten yavaşsa kullanmanızı önerir; küçük modelleri derlemek genellikle buna değmez.

NativeAOT ve precompiled query (deneysel)

.NET NativeAOT, önceden derlenmiş, kendi kendine yeten uygulamalar yayımlamanıza izin verir; daha hızlı başlangıç, daha küçük ikili dosyalar ve JIT desteklenmeyen ortamlarda çalışabilme gibi avantajlar sunar. EF bu modda LINQ sorgularını yayımlama sırasında önceden derler: kaynak kodunuzdaki EF sorgularını statik olarak tespit eder ve her sorgu için nihai SQL'i ve sonuçları nesnelere dönüştüren optimize kodu içeren C# interceptor'ları üretir. Bunun için proje dosyasında NativeAOT yayımlamasını açar, interceptor'lar için gereken ad alanını etkinleştirir ve Microsoft.EntityFrameworkCore.Tasks paketini eklersiniz:

xml
<PropertyGroup>
  <PublishAot>true</PublishAot>
  <InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.EntityFrameworkCore.GeneratedInterceptors</InterceptorsNamespaces>
</PropertyGroup>

Kaynak koddaki herhangi bir değişiklik interceptor'ları geçersiz kıldığı için belgeler bu üretimin geliştirme döngüsünde değil, CI/CD gibi bir yayımlama akışında yapılmasını önerir. Bilinen sınırlamalar arasında dinamik olarak birleştirilen sorguların ön derlenememesi, LINQ sorgu ifadesi söz diziminin (query syntax) desteklenmemesi, yakalanmış durum kullanan value converter'ların desteklenmemesi ve sağlayıcıların bu özelliğe ayrıca destek eklemesi gerekebilmesi var. NativeAOT kullanmadan da yalnızca ön derlenmiş sorgulardan yararlanmak mümkündür; bu, başlangıç süresi kazancını alırken dinamik sorgular gibi NativeAOT'nin şu an desteklemediği özellikleri kullanmaya devam etmenizi sağlar:

bash
dotnet ef dbcontext optimize --precompile-queries

EF'nin iç bellek önbelleği

EF Core, sorgu derleme ve model oluşturma gibi iç önbellekleme işleri için IMemoryCache kullanır. Varsayılan olarak kendi IMemoryCache örneğini 10240 boyut sınırıyla yapılandırır; belgelere göre derlenmiş bir sorgunun önbellek boyutu 10, oluşturulmuş modelinki 100'dür. Uygulamanız çok sayıda farklı sorgu şekli kullanıyorsa bu sınırı UseMemoryCache ile kendi önbellek örneğinizi vererek değiştirebilirsiniz. Uygulama genelindeki IMemoryCache'i paylaşırsanız boyut sınırının tüm tüketicileri hesaba katması ve sınır ayarlıysa her girişin bir boyut belirtmesi gerektiğini unutmayın.

Hangi tekniği ne zaman kullanmalı?

Hangi tekniği ne zaman kullanmalı? DbContext pooling, Compiled query, Compiled model, Precompiled query / NativeAOT
TeknikHedeflediği maliyetNe zaman
DbContext poolingBağlam kurulumuÇok yüksek istek hızı ve düşük gecikme gereken servisler
Compiled querySorgu önbelleği aramasıSık çalışan, karmaşık sıcak sorgular
Compiled modelModel oluşturma (başlangıç)Çok büyük modellerde yavaş ilk işlem
Precompiled query / NativeAOTBaşlangıçta sorgu derlemeDeneysel; üretim için önerilmez
EnableThreadSafetyChecks falseEşzamanlılık denetimiYalnızca kapsamlı testten sonra

Belgeler bu katmanın getirdiği çalışma zamanı yükünün gerçek dünyadaki uygulamaların çoğunu anlamlı biçimde etkilemesinin olası olmadığını, sorgu verimliliği, index kullanımı ve veritabanı gidiş-dönüşlerini azaltmanın çok daha önemli olduğunu hatırlatır. Ağ gecikmesi ve veritabanı G/Ç'si genellikle EF Core içinde geçen süreye baskın çıkar. Bu yüzden önce ölçün, sonra bu tekniklere başvurun. Thread güvenliği denetiminin ne yaptığı async ve second operation hatası yazısında anlatılıyor.

Sık yapılan hatalar

  • Kiracı veya kullanıcı bilgisini OnConfiguring içinde ayarlamak: Havuzlu bağlamda OnConfiguring yalnızca bir kez çalışır; değer istekler arasında yanlış taşınır.
  • Elle açılan bağlantıyı kapatmadan bağlamı havuza vermek: Sürücü durumu bir sonraki isteğe sızabilir.
  • Pooling'i bağlantı havuzu sanmak: Bağlantı havuzunun boyutu sürücü ayarlarından, bağlam havuzunun boyutu poolSize ile ayarlanır.
  • Compiled query'ye karmaşık parametre geçmek: Yalnızca skaler parametreler desteklenir.
  • Model değiştiği halde compiled model'i yeniden üretmemek: Model elle eşitlenmelidir; komutu her model değişikliğinden sonra yeniden çalıştırın.
  • Global query filter kullanan modelde compiled model denemek: Bu kombinasyon desteklenmez.
  • NativeAOT'yi üretime almak: Belgeler bu özelliği açıkça deneysel ve üretime uygun değil olarak işaretler.

Sık Sorulan Sorular

AddDbContextPool her uygulamada açılmalı mı?

Belgeler bağlam oluşturmanın çoğu uygulamada fark edilir bir maliyeti olmadığını söyler. Pooling, bağlam kurulumunun ölçülebilir bir yük olduğu yüksek performanslı senaryolar içindir. Açmadan önce bağlamınızda istekten isteğe değişen durum olup olmadığını kontrol edin.

poolSize dolarsa ne olur?

Havuz hata vermez. Sınır aşıldığında yeni örnekler önbelleğe alınmaz ve EF gerektiğinde normal şekilde yeni örnek oluşturur. Varsayılan sınır 1024'tür.

Compiled query ile normal sorgu arasında sonuç farkı olur mu?

Aynı LINQ sorgusunu çalıştırırlar; fark, EF'nin her çalıştırmada sorgu önbelleğinde arama yapıp yapmamasıdır. Kısıtlar tek model ve skaler parametrelerle ilgilidir.

Compiled model ile compiled query aynı şey mi?

Hayır. Compiled model, modelin kendisini önceden üreterek ilk işlemin başlangıç süresini kısaltır ve dotnet ef dbcontext optimize ile oluşturulur. Compiled query ise tek bir LINQ sorgusunu delegate'e derler ve her çalıştırmadaki önbellek aramasını atlar.

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 · EF Core performans optimizasyonu · DbContext yaşam döngüsü · Async ve thread güvenliği · Global query filter · dotnet ef komutları