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 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.
// 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.
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.
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:
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:
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:
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:
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:
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 onNeden 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:
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

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:
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
DbContextdispose 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.Detachedyaparak 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:
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:
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?

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.
| Konu | Proxy ile | ILazyLoader ile |
|---|---|---|
| Gereken paket | Microsoft.EntityFrameworkCore.Proxies (Castle.Core ile) | Microsoft.EntityFrameworkCore.Abstractions |
| virtual navigation | Gerekli (yoksa varsayılan olarak hata) | Gerekli değil |
| Sınıf kalıtıma açık olmalı mı | Evet | Hayı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'ı kapatma | EnableLazyLoading(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
Includeya 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:
DetachedLazyLoadingWarningbastı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ı
lazyLoaderolmalı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ü