EF Core'da lazy loading, ilişkili verinin navigation özelliğine ilk erişildiği anda veritabanından kendiliğinden yüklenmesidir. En basit kurulumu Microsoft.EntityFrameworkCore.Proxies paketini ekleyip UseLazyLoadingProxies() çağırmak ve navigation özelliklerini virtual yapmaktır. Proxy kullanmak istemiyorsanız varlığa ILazyLoader servisini enjekte eden ikinci bir yol vardır.

Entity Framework Core belgeleri lazy loading'i kurmayı kolay, doğru kullanmayı zor bir özellik olarak anlatır: kod sade görünür, ama döngü içinde her navigation erişimi ayrı bir veritabanı gidişine dönüşebilir. Bu yazıda iki kurulum yolunu, EF Core 8 ile gelen ayarları, N+1 sorununun loglarda nasıl göründüğünü, context kapandıktan sonra ne olduğunu ve yüklenen nesne grafiğini JSON'a çevirirken çıkan döngü hatasını ele alıyoruz. Eager ve explicit loading Include ile eager loading yazısında; serinin geri kalanı için EF Core öğrenme rehberi.

Proxy ile lazy loading kurulumu

Proxy ile lazy loading kurulumu

Proxy tabanlı lazy loading için önce Microsoft.EntityFrameworkCore.Proxies NuGet paketini projeye eklersiniz. Belgelerdeki NuGet paketleri tablosuna göre bu paket hem lazy loading hem de change tracking proxy'lerini sağlar ve ek bağımlılık olarak Castle.Core paketini getirir. Paket kurulumunun genel akışı EF Core kurulumu ve NuGet paketleri yazısında. Ardından seçenek oluşturucusunda UseLazyLoadingProxies çağrılır; bu, OnConfiguring içinde ya da AddDbContext kaydında yapılabilir.

csharp
// OnConfiguring içinde
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseLazyLoadingProxies()
        .UseSqlServer(myConnectionString);

// ya da AddDbContext ile
.AddDbContext<BloggingContext>(
    b => b.UseLazyLoadingProxies()
          .UseSqlServer(myConnectionString));

Belgeler UseLazyLoadingProxies gibi ".UseSomething()" biçimindeki çağrıların, ayrı paketlerde gelen EF Core uzantılarını yapılandırmanın önerilen yolu olduğunu da belirtir. Bu ayardan sonra EF Core, üzerine yazılabilen (override edilebilen) her navigation özelliği için lazy loading'i etkinleştirir. Bunun iki koşulu vardır: özellik virtual olmalı ve sınıf kalıtıma açık olmalıdır (yani sealed olmamalıdır). Aşağıdaki modelde hem Post.Blog hem de Blog.Posts lazy yüklenir.

csharp
public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public virtual Blog Blog { get; set; }
}

Proxy'ler, çalışma anında varlık sınıfınızdan türeyen bir sınıf üretir ve sanal navigation özelliklerini bu türemiş sınıfta ezer. Bu yüzden kurucu metot konusunda da bir kısıt vardır: varlık türü kurucuları sayfasına göre kurucu metot herhangi bir erişim düzeyinde olabilir, ancak lazy loading proxy'leri kurucunun türeyen proxy sınıfından erişilebilir olmasını gerektirir; bu genellikle kurucuyu public ya da protected yapmak demektir. Navigation belgeleri de navigation'ları yalnızca lazy loading ya da change tracking proxy'leri kullanılıyorsa virtual yapmanın gerektiğini hatırlatır.

virtual unutulursa ne olur?

EF Core 8 sürüm notları burada EF6'dan gelen bir alışkanlığa değinir. Klasik EF6 uygulamalarında bir navigation'ı virtual yapmayı unutmak sık görülen bir hata kaynağıydı, çünkü o navigation sessizce lazy yüklenmezdi. EF Core proxy'leri bu yüzden varsayılan olarak navigation virtual değilse hata fırlatır. EF Core 8 ile eski EF6 davranışını tercih edebilirsiniz: bu durumda virtual olmayan navigation'lar hata vermeden lazy loading dışında kalır.

csharp
optionsBuilder.UseLazyLoadingProxies(b => b.IgnoreNonVirtualNavigations());

Tek bir navigation'ı, geri kalan her şey lazy loading için ayarlanmışken bilinçli olarak dışarıda bırakmak da EF Core 8 ile mümkün oldu. Bu ayar hem proxy'li hem proxy'siz lazy loading için çalışır:

csharp
modelBuilder
    .Entity<Post>()
    .Navigation(p => p.Author)
    .EnableLazyLoading(false);

Proxy olmadan lazy loading: ILazyLoader

İkinci yol, varlığa ILazyLoader servisini kurucu metot üzerinden enjekte etmektir. EF Core'un kurucu bağlama özelliği, eşlenmiş özelliklerin yanında bazı servisleri de kurucu parametresi olarak verebilir; bunlar arasında DbContext, ILazyLoader, Action<object, string> türünde bir lazy loading temsilcisi ve IEntityType bulunur. Belgelerdeki örnekte navigation'ın getter'ı yükleyiciyi çağırır, setter ise yalnızca alanı atar:

csharp
public class Blog
{
    private ICollection<Post> _posts;

    public Blog()
    {
    }

    private Blog(ILazyLoader lazyLoader)
    {
        LazyLoader = lazyLoader;
    }

    private ILazyLoader LazyLoader { get; set; }

    public int Id { get; set; }
    public string Name { get; set; }

    public ICollection<Post> Posts
    {
        get => LazyLoader.Load(this, ref _posts);
        set => _posts = value;
    }
}

Bu yöntem varlık türlerinin kalıtıma açık olmasını ya da navigation'ların virtual olmasını gerektirmez ve new ile oluşturulan varlıkların bir context'e bağlandıktan sonra lazy yüklenmesine izin verir. Bedeli, varlık sınıfınızın Microsoft.EntityFrameworkCore.Abstractions paketindeki ILazyLoader türüne bağımlı olmasıdır. Belgeler bu paketin bilinçli olarak az sayıda tür içerdiğini, bu yüzden bağımlılığın etkisinin küçük olduğunu belirtir.

Varlık türlerinde hiçbir EF Core paketine bağımlılık istemiyorsanız ILazyLoader.Load metodunu bir Action<object, string> temsilcisi olarak enjekte edebilirsiniz. Belgelerdeki örnek, temsilcinin kullanımını sadeleştirmek için küçük bir uzantı metodu tanımlar; [CallerMemberName] özniteliği navigation adını otomatik olarak doldurur:

csharp
public static class PocoLoadingExtensions
{
    public static TRelated Load<TRelated>(
        this Action<object, string> loader,
        object entity,
        ref TRelated navigationField,
        [CallerMemberName] string navigationName = null)
        where TRelated : class
    {
        loader?.Invoke(entity, navigationName);

        return navigationField;
    }
}

Neden getter içinde yükleyici çağrılması sonsuz döngüye yol açmıyor diye merak edebilirsiniz. Change tracking belgeleri, EF Core'un varlık özelliklerine varsayılan olarak backing field üzerinden eriştiğini, bunun getter ve setter yan etkilerini tetiklemediğini ve lazy loading'in sonsuz döngüleri bu sayede önlediğini açıklar. Backing field yapılandırması property ve kolon yapılandırma yazısında.

N+1 sorunu: loglarda nasıl görünür?

Lazy loading'in asıl riski, belgelerin uyardığı gibi gereksiz ek veritabanı gidişleridir. Performans sayfasındaki örnek masum görünen iki iç içe döngüdür:

csharp
foreach (var blog in await context.Blogs.ToListAsync())
{
    foreach (var post in blog.Posts)
    {
        Console.WriteLine($"Blog {blog.Url}, Post: {post.Title}");
    }
}

SQL loglaması açıldığında belgelerin gösterdiği örnek çıktı, önce blogların tek sorguyla, ardından her blogun yazılarının ayrı bir sorguyla çekildiğini ortaya koyar. Aşağıdaki satırlar belgelerdeki örnek logdan kısaltılmıştır:

bash
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [b].[BlogId], [b].[Rating], [b].[Url]
      FROM [Blogs] AS [b]
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (5ms) [Parameters=[@__p_0='1'], CommandType='Text', CommandTimeout='30']
      SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Title]
      FROM [Post] AS [p]
      WHERE [p].[BlogId] = @__p_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[@__p_0='2'], CommandType='Text', CommandTimeout='30']
      SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Title]
      FROM [Post] AS [p]
      WHERE [p].[BlogId] = @__p_0

... and so on

Neden böyle olduğu açık: bir blogun Posts özelliği yalnızca erişildiğinde yüklenir, bu yüzden iç döngünün her başlangıcı kendi gidiş-dönüşünde ek bir sorgu tetikler. Blogları getiren ilk sorgunun ardından her blog için bir sorgu daha gelir; buna N+1 sorunu denir ve belgeler bunun çok ciddi performans sorunlarına yol açabileceğini vurgular. Geliştirme ortamında üç blog varken fark edilmeyen bu durum, gerçek veride yüzlerce ayrı sorguya dönüşebilir. Logları kendi uygulamanızda açmak için SQL loglama: LogTo ve interceptor yazısına bakın.

Tüm yazılara ihtiyaç olduğu biliniyorsa çözüm eager loading'dir. Belgeler, örnekte yalnızca blog adresleri gerektiği için Include yerine bir projeksiyon önerir; bu, blogları yazılarıyla birlikte tek sorguda getirir:

csharp
await foreach (var blog in context.Blogs.Select(b => new { b.Url, b.Posts }).AsAsyncEnumerable())
{
    foreach (var post in blog.Posts)
    {
        Console.WriteLine($"Blog {blog.Url}, Post: {post.Title}");
    }
}

Aynı seviyede birden fazla koleksiyon yükleyecekseniz tek sorgunun kartezyen patlamaya yol açabileceğini unutmayın; bu durumda split query seçeneği değerlendirilebilir. Belgelerin bu bölümdeki son uyarısı açıktır: lazy loading N+1 sorununu yanlışlıkla tetiklemeyi son derece kolaylaştırdığı için ondan kaçınılması önerilir; eager ya da explicit loading, veritabanı gidişinin ne zaman olduğunu kaynak kodunda açıkça gösterir.

Context kapandıktan sonra lazy loading

Context kapandıktan sonra lazy loading: Lazy loading yalnızca varlığı sorgulayan DbContext dispose edilene kadar..., Bu şekilde sorgulanan varlıklar takip edilmeseler bile...

Lazy loading bir context'e ihtiyaç duyar ve bu, web uygulamalarında sık görülen bir hataya kapı açar: varlığı bir istek içinde yükleyip context dispose edildikten sonra navigation'a erişmek. EF Core 3.0 değişiklik notları bugünkü davranışın temelini açıklar. EF Core 3.0'dan beri proxy'ler bir navigation'ın yüklenip yüklenmediğini takip eder. Context dispose edildikten sonra daha önce yüklenmiş bir navigation'a erişmek her zaman etkisizdir (no-op), navigation boş ya da null olsa bile. Yüklenmemiş bir navigation'a erişmek ise, koleksiyon boş olmasa bile, istisna fırlatır. Belgeler bu durumun uygulamanın lazy loading'i geçersiz bir anda kullanmaya çalıştığı anlamına geldiğini ve kodun düzeltilmesi gerektiğini söyler; alternatif olarak bu durumu istisna mesajında anlatıldığı şekilde etkisiz olacak biçimde yapılandırmak mümkündür.

Basit loglama belgeleri, gözden geçirilip anlaşılmış bir uyarının loglanmasını bastırmaya örnek olarak tam da bu konuyla ilgili CoreEventId.DetachedLazyLoadingWarning olayını kullanır:

csharp
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .ConfigureWarnings(b => b.Ignore(CoreEventId.DetachedLazyLoadingWarning))
        .LogTo(Console.WriteLine);

Bir uyarıyı bastırmak sorunu çözmez, yalnızca görünmez kılar. Context ömrünü doğru yönetmek, yani her iş birimi için kısa ömürlü bir context kullanmak, DbContext yapılandırma ve yaşam döngüsü yazısının konusudur.

No-tracking sorgulardan sonra lazy loading

EF Core 8 ile lazy loading, context tarafından takip edilmeyen varlıklarda da çalışır hâle geldi; yani bir AsNoTracking() sorgusunun döndürdüğü varlıkların navigation'ları da lazy yüklenebilir. EF Core 8 ayrıca takip edilmeyen varlıklar için bir navigation'ın yüklenip yüklenmediğini context.Entry(blog).Collection(e => e.Posts).IsLoaded ile bildirir. Sürüm notları bu kullanım için dört önemli noktayı sıralar:

  • Lazy loading yalnızca varlığı sorgulayan DbContext dispose edilene kadar başarılı olur.
  • Bu şekilde sorgulanan varlıklar takip edilmeseler bile context'lerine bir referans tutar. Varlık nesneleri uzun yaşayacaksa bellek sızıntısına karşı dikkatli olunmalıdır.
  • Varlığın durumunu EntityState.Detached yaparak açıkça ayırmak context bağlantısını keser ve lazy loading artık çalışmaz.
  • Tüm lazy loading senkron G/Ç kullanır, çünkü bir özelliğe asenkron erişmenin yolu yoktur.

Son madde sunucu uygulamaları için ayrıca önemlidir. Belgelerin genel önerisi ölçeklenebilirlik için asenkron API'leri kullanmak ve senkron ile asenkron kodu aynı uygulamada karıştırmamaktır; lazy loading ise her erişimde senkron bir veritabanı çağrısı yapar. Asenkron yükleme gerekiyorsa explicit loading'in LoadAsync metodu kullanılabilir. Async kullanımının ayrıntıları async ve second operation hatası yazısında. No-tracking sorguların genel davranışı ise AsNoTracking ve tracking sorguları yazısında.

Serileştirme ve döngüsel referanslar

EF Core navigation'ları otomatik olarak birbirine bağladığı için nesne grafiğinizde döngüler oluşabilir: bir blogu yazılarıyla yüklediğinizde blog yazı koleksiyonunu, her yazı da tekrar blogu gösterir. Bazı serileştiriciler döngülere izin vermez. Belgelere göre Json.NET bu durumda şu istisnayı fırlatır:

bash
Newtonsoft.Json.JsonSerializationException: Self referencing loop detected for property 'Blog' with type 'MyApplication.Models.Blog'.

System.Text.Json da benzer bir istisna verir; belgelerdeki mesaj, döngü olasılığını ya da 32'lik izin verilen azami nesne derinliğinin aşıldığını belirtir ve ReferenceHandler.Preserve kullanmayı önerir. Belgeler iki çözüm yolu gösterir. Birincisi serileştiriciyi döngüleri yok sayacak şekilde ayarlamaktır; System.Text.Json için bu ayar şöyledir:

csharp
services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
    });

Json.NET kullanıyorsanız karşılığı ReferenceLoopHandling.Ignore ayarıdır. İkinci yol, döngüye neden olan navigation özelliklerinden birini [JsonIgnore] özniteliğiyle işaretlemektir; System.Text.Json için bu özniteliğin System.Text.Json.Serialization ad alanındaki sürümü kullanılır. Hangi yolu seçerseniz seçin, serileştirilen grafiğin context açıkken ve gereken ilişkiler yüklenmiş hâlde hazırlanması gerekir; aksi hâlde bir önceki bölümde anlatılan dispose sonrası erişim durumuyla karşılaşırsınız. Uygulamanın web API katmanının ayrıntıları ise ASP.NET Core tarafında ayrı bir konu.

Lazy loading ne zaman kullanılabilir?

Lazy loading ne zaman kullanılabilir? Gereken paket, virtual navigation, Sınıf kalıtıma açık olmalı mı, Varlık sınıfında EF bağımlılığı

Belgeler lazy loading'i yasaklamaz, ama sınırlarını açıkça çizer. Ana varlığı yükledikten sonra başka bir kaynağa bakmadan ilişkili veriye ihtiyacınız olup olmadığını bilemediğiniz durumlarda explicit ve lazy loading ilişkili veriyi ayrı ayrı getirmeye yarar. Bu yöntemler eager olmadığı için ek gidiş-dönüş gerektirir ve senaryoya göre tüm ilişkili veriyi baştan yüklemek daha verimli olabilir. Bu maliyetin kabul edilebilir olup olmadığı, kaç varlık için kaç navigation'a erişildiğine bağlıdır: tek bir kaydın tek bir ilişkisine bakmak bir ek sorgudur, ama bir liste üzerinde dönen koddaki her erişim potansiyel olarak ayrı bir sorgudur. EF Core 10 sürüm notları, lazy loading performansı için AsyncLocal kullanımında iyileştirme yapıldığını listeler; bu tek tek erişimlerin maliyetini düşürebilir, ama N+1 sorununun yapısını değiştirmez.

KonuProxy ileILazyLoader ile
Gereken paketMicrosoft.EntityFrameworkCore.Proxies (Castle.Core ile)Microsoft.EntityFrameworkCore.Abstractions
virtual navigationGerekli (yoksa varsayılan olarak hata)Gerekli değil
Sınıf kalıtıma açık olmalı mıEvetHayır
Varlık sınıfında EF bağımlılığıYok (çalışma anında türetilir)ILazyLoader ya da temsilci gerekir
Tek navigation'ı kapatmaEnableLazyLoading(false)EnableLazyLoading(false)

Sık yapılan hatalar

  • Döngü içinde navigation'a erişmek: N+1 sorununun klasik kaynağıdır. Gerekli ilişkileri Include ya da projeksiyonla tek sorguda getirin.
  • Context dispose edildikten sonra erişmek: Yüklenmemiş navigation için istisna alırsınız. Gerekli veriyi context açıkken yükleyin.
  • Uyarıyı bastırıp sorunu bırakmak: DetachedLazyLoadingWarning bastırılabilir, ama asıl sorun context ömrünün yanlış yönetilmesidir.
  • sealed sınıf ya da virtual olmayan navigation: Proxy üretilemez ya da varsayılan ayarla hata alınır.
  • Temsilci parametresine farklı ad vermek: Parametre adı lazyLoader olmalıdır.
  • Varlıkları doğrudan JSON'a çevirmek: Döngüsel referans hatası ve beklenmedik ek sorgular doğurabilir.

Sık Sorulan Sorular

Lazy loading varsayılan olarak açık mı?

Hayır. Proxy paketini ekleyip UseLazyLoadingProxies() çağırmadan ya da ILazyLoader enjekte etmeden lazy loading çalışmaz; navigation'lar ancak Include, explicit loading ya da context'te zaten bulunan varlıklarla fixup sayesinde dolar.

Lazy loading asenkron yapılabilir mi?

Hayır. EF Core 8 sürüm notlarının da belirttiği gibi tüm lazy loading senkron G/Ç kullanır, çünkü bir özelliğe asenkron erişilemez. Asenkron yükleme için context.Entry(blog).Collection(b => b.Posts).LoadAsync() kullanın.

Tek bir navigation'da lazy loading'i nasıl kapatırım?

Model yapılandırmasında Navigation(p => p.Author).EnableLazyLoading(false) kullanın. Proxy kullanıyorsanız, IgnoreNonVirtualNavigations() seçeneğiyle virtual olmayan navigation'ları lazy loading dışında bırakmak da mümkündür.

Lazy loading ile explicit loading arasında nasıl seçim yapmalıyım?

İkisi de ilişkili veriyi sonradan getirir. Explicit loading'de yükleme kodda açıkça yazılır ve asenkron yapılabilir; lazy loading ise erişim anında, görünmez biçimde ve senkron çalışır. Belgeler, veritabanı gidişlerini kodda görünür kıldıkları için eager ya da explicit loading'i önerir.

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 · Include ile eager ve explicit loading · Split query · EF Core performans optimizasyonu · DbContext yaşam döngüsü