EF Core'da ilişkili verileri ana sorguyla birlikte yüklemenin yolu Include metodudur: context.Blogs.Include(blog => blog.Posts) yazdığınızda dönen her blogun Posts koleksiyonu aynı sorguda doldurulur. Bu yönteme eager loading denir. Daha derin seviyeler için ThenInclude, ilişkili veriyi sonradan ve bilinçli olarak yüklemek için ise Entry(...).Collection(...).LoadAsync() biçimindeki explicit loading kullanılır.

Entity Framework Core belgeleri ilişkili veri için üç yaygın ORM kalıbı tanımlar: eager loading (ilk sorguyla birlikte), explicit loading (sonradan, açıkça) ve lazy loading (navigation özelliğine erişildiğinde, kendiliğinden). Bu yazı ilk ikisini ayrıntılı anlatıyor: Include ve ThenInclude zincirleri, filtrelenmiş include, türetilmiş türlerde include, model düzeyinde AutoInclude ve Load/Query ile açık yükleme. Lazy loading EF Core lazy loading yazısında, ilişkilerin kendisi ise ilişkiler, navigation ve foreign key yazısında. Serinin tamamı için EF Core öğrenme rehberi.

Örnek model

Örnek model

Belgelerdeki örnekler bir blog modeli kullanır: bir Blog çok sayıda Post içerir; her blogun bir sahibi (Owner) ve bir teması (Theme), her yazının bir yazarı (Author) ve etiketleri (Tags) vardır. Bu yazıdaki tüm örnekler aynı model üzerinden ilerler.

csharp
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
    public int? Rating { get; set; }

    public List<Post> Posts { get; set; }

    public int OwnerId { get; set; }
    public Person Owner { get; set; }

    public int ThemeId { get; set; }
    public Theme Theme { get; set; }
}

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

    public int BlogId { get; set; }
    public Blog Blog { get; set; }

    public int AuthorId { get; set; }
    public Person Author { get; set; }

    public List<PostTag> Tags { get; set; }
}

Posts ve Tags koleksiyon navigation'larıdır; Blog, Owner, Theme ve Author ise referans navigation'larıdır. Bu ayrım önemlidir, çünkü tek bir sorguda yüklenen koleksiyonlar performans açısından referanslardan farklı davranır.

Include ile eager loading

Include, sorgu sonucuna hangi ilişkili verinin dahil edileceğini belirtir. Aşağıdaki sorgu tüm blogları ve her blogun yazılarını getirir; birden fazla ilişkiyi aynı sorguda yüklemek için Include çağrılarını art arda ekleyebilirsiniz.

csharp
using (var context = new BloggingContext())
{
    var blogs = await context.Blogs
        .Include(blog => blog.Posts)
        .Include(blog => blog.Owner)
        .ToListAsync();
}

Belgelerdeki önemli bir ipucu: EF Core, context örneğine daha önce yüklenmiş varlıklarla navigation özelliklerini otomatik olarak birbirine bağlar (navigation fixup). Bu nedenle bir navigation için Include yazmasanız bile, ilişkili varlıkların bir kısmı ya da tamamı aynı context'e başka bir sorguyla daha önce yüklendiyse o özellik dolu gelebilir. Bu davranış takip eden sorgulara özgüdür; ayrıntısı AsNoTracking ve tracking sorguları yazısında.

ThenInclude ile birden fazla seviye

İlişkiler boyunca daha derine inmek için ThenInclude kullanılır. Aşağıdaki sorgu blogları, yazılarını ve her yazının yazarını yükler; zinciri uzatarak yazarın fotoğrafına kadar inebilir, aynı sorguda başka bir kökten (Owner) yeni bir zincir başlatabilirsiniz.

csharp
using (var context = new BloggingContext())
{
    var blogs = await context.Blogs
        .Include(blog => blog.Posts)
        .ThenInclude(post => post.Author)
        .ThenInclude(author => author.Photo)
        .Include(blog => blog.Owner)
        .ThenInclude(owner => owner.Photo)
        .ToListAsync();
}

Sık sorulan bir durum, dahil edilen bir varlığın birden fazla ilişkisini yüklemektir: blogların yazılarını, yazıların da hem yazarını hem etiketlerini istiyorsanız her yolu kökten başlayarak ayrı ayrı belirtmeniz gerekir, yani Blog ile başlayıp Posts ve Author üzerinden giden bir yol ve yine Blog ile başlayıp Posts ve Tags üzerinden giden bir yol. Belgeler bunun gereksiz join'ler üretmediğini, EF Core'un çoğu durumda join'leri SQL üretirken birleştirdiğini belirtir.

csharp
var blogs = await context.Blogs
    .Include(blog => blog.Posts)
    .ThenInclude(post => post.Author)
    .Include(blog => blog.Posts)
    .ThenInclude(post => post.Tags)
    .ToListAsync();

Tek bir Include içinde nokta ile zincir kurmak da mümkündür; ancak bu yalnızca zincirin tamamı referans navigation'lardan oluşuyorsa ya da zincir tek bir koleksiyonla bitiyorsa çalışır. Belgelerdeki örnek Include(blog => blog.Owner.AuthoredPosts) ifadesinden sonra ThenInclude(post => post.Blog.Owner.Photo) ile devam eder.

Filtrelenmiş include

Filtrelenmiş include

Dahil edilen bir koleksiyonun tamamını değil yalnızca bir kısmını istiyorsanız, Include lambdasının içinde koleksiyon üzerine bazı operatörler uygulayabilirsiniz. Belgelere göre desteklenen operatörler Where, OrderBy, OrderByDescending, ThenBy, ThenByDescending, Skip ve Take'tir. Aşağıdaki sorgu her blog için başlığa göre tersten sıralanmış en fazla beş yazı yükler:

csharp
using (var context = new BloggingContext())
{
    var filteredBlogs = await context.Blogs
        .Include(
            blog => blog.Posts
                .Where(post => post.BlogId == 1)
                .OrderByDescending(post => post.Title)
                .Take(5))
        .ToListAsync();
}

Bir kural var: dahil edilen her navigation için yalnızca tek bir benzersiz filtre kümesi tanımlanabilir. Aynı koleksiyon navigation'ı (örneğin blog.Posts) birden fazla Include çağrısında geçiyorsa filtreyi bunlardan yalnızca birine yazabilir ya da her birine birebir aynı işlemleri uygulayabilirsiniz. Farklı filtreler yazmak geçerli değildir.

csharp
// Geçerli: filtre yalnızca bir Include içinde
var filteredBlogs = await context.Blogs
    .Include(blog => blog.Posts.Where(post => post.BlogId == 1))
    .ThenInclude(post => post.Author)
    .Include(blog => blog.Posts)
    .ThenInclude(post => post.Tags.OrderBy(postTag => postTag.TagId).Skip(3))
    .ToListAsync();

Filtrelenmiş include ve takip edilen varlıklar

Filtrelenmiş include, takip eden sorgularda beklenmedik sonuç verebilir. Navigation fixup nedeniyle, daha önce sorgulanıp change tracker'da tutulan tüm ilgili varlıklar filtreyi karşılamasalar bile sonuçtaki koleksiyonda görünür. Belgelerdeki örnekte önce Id değeri 1000'den büyük siparişler yüklenir, ardından müşteriler Id > 5000 filtresiyle siparişleriyle birlikte çekilir; müşterilerin sipariş koleksiyonlarında 1000'den büyük tüm siparişler yer alır.

csharp
var orders = await context.Orders.Where(o => o.Id > 1000).ToListAsync();

// Müşterilerin Orders koleksiyonunda Id > 1000 olan tüm siparişler bulunur, yalnızca > 5000 olanlar değil
var filtered = await context.Customers.Include(c => c.Orders.Where(o => o.Id > 5000)).ToListAsync();

Belgeler bu durumda NoTracking sorguları kullanmayı ya da context'i yeniden oluşturmayı önerir. Bir ayrıntı daha var: takip eden sorgularda filtrelenmiş include uygulanan navigation "yüklendi" kabul edilir. Bu yüzden EF Core, bazı öğeler eksik olsa bile o navigation'ı explicit ya da lazy loading ile yeniden yüklemeye çalışmaz.

Türetilmiş türlerde include

Kalıtım kullanan bir modelde yalnızca türetilmiş türde tanımlı bir navigation'ı da yükleyebilirsiniz. Belgelerdeki örnekte Person temel tür, Student ondan türeyen ve School navigation'ı taşıyan türdür. People sorgusunda öğrencilerin okullarını yüklemenin üç yolu vardır: tür dönüşümü, as operatörü ya da string alan Include aşırı yüklemesi.

csharp
context.People.Include(person => ((Student)person).School).ToList();
context.People.Include(person => (person as Student).School).ToList();
context.People.Include("School").ToList();

String sürümü derleme zamanında denetlenmediği için yazım hatalarını ancak çalışma anında fark edersiniz; lambda sürümleri yeniden adlandırmalarda derleyicinin sizi uyarmasını sağlar. Kalıtım eşleme stratejileri (TPH, TPT, TPC) inheritance: TPH, TPT ve TPC yazısında.

AutoInclude: navigation'ı her zaman yüklemek

Bir navigation'ın varlık her yüklendiğinde getirilmesini istiyorsanız bunu modelde AutoInclude ile yapılandırabilirsiniz. Etkisi, o varlığın döndüğü her sorguya ilgili Include'u yazmakla aynıdır. Belgeler bu ayarın varlık sonuca nasıl girerse girsin uygulandığını vurgular: varlık başka bir navigation üzerinden, başka bir türün Include'u ile ya da başka bir auto-include sayesinde gelmiş olsa da otomatik dahil edilen navigation'ları yüklenir. Kural türetilmiş türlerde tanımlı auto-include'lar için de geçerlidir.

csharp
modelBuilder.Entity<Theme>().Navigation(e => e.ColorScheme).AutoInclude();

// ColorScheme her temada yüklenir
var themes = await context.Themes.ToListAsync();

// Bu sorguda kullanıcı tanımlı auto-include'lar yüklenmez
var plainThemes = await context.Themes.IgnoreAutoIncludes().ToListAsync();

Explicit loading: Load ve Query

Bazen ilişkili veriye ihtiyacınız olup olmadığını ana varlığı yükledikten sonra öğrenirsiniz. Bu durumda DbContext.Entry(...) API'si ile bir navigation'ı açıkça yükleyebilirsiniz: koleksiyonlar için Collection, referanslar için Reference ve ardından LoadAsync.

csharp
using (var context = new BloggingContext())
{
    var blog = await context.Blogs
        .SingleAsync(b => b.BlogId == 1);

    await context.Entry(blog)
        .Collection(b => b.Posts)
        .LoadAsync();

    await context.Entry(blog)
        .Reference(b => b.Owner)
        .LoadAsync();
}

İlişkili varlıkları döndüren ayrı bir sorgu çalıştırmak da explicit loading'in bir biçimidir: change tracking açıksa EF Core yeni yüklenen varlıkların navigation'larını zaten yüklü varlıklara, zaten yüklü olanlarınkini de yenilere bağlar. Daha da kullanışlı olanı Query() metodudur; navigation'ın içeriğini temsil eden bir LINQ sorgusu verir. Böylece ilişkili varlıkları belleğe almadan bir toplama işlemi yapabilir ya da yalnızca bir kısmını yükleyebilirsiniz.

csharp
// Yazıları yüklemeden say
var postCount = await context.Entry(blog)
    .Collection(b => b.Posts)
    .Query()
    .CountAsync();

// Yalnızca puanı 3'ten büyük yazıları yükle
var goodPosts = await context.Entry(blog)
    .Collection(b => b.Posts)
    .Query()
    .Where(p => p.Rating > 3)
    .ToListAsync();

EF Core 8 sürüm notlarına göre explicit loading, context tarafından takip edilmeyen varlıklarda da çalışır; varlık ya da navigation lazy loading için yapılandırılmamış olsa bile context.Entry(blog).Collection(e => e.Posts).LoadAsync() çağrısı yapılabilir. Lazy loading'in aksine bu yükleme asenkron yapılabilir.

Mini örnek: blog ayrıntı sayfası

Şimdi iki yöntemi aynı senaryoda birleştirelim. Bir blogun ayrıntı sayfasında blog sahibini, en yüksek puanlı beş yazıyı ve bu yazıların yazarlarını göstermek, ayrıca blogdaki toplam yazı sayısını yazmak istiyorsunuz. Sahip ve seçilmiş yazılar her zaman gerektiği için eager yüklenir; toplam sayı için ise tüm yazıları belleğe almak gereksizdir, bu yüzden Query().CountAsync() kullanılır. Kod belgelerdeki kalıplardan uyarlanmıştır ve çalıştırılmamıştır.

csharp
var blog = await context.Blogs
    .Include(b => b.Owner)
    .Include(b => b.Posts
        .OrderByDescending(p => p.Rating)
        .Take(5))
    .ThenInclude(p => p.Author)
    .SingleAsync(b => b.BlogId == blogId);

var toplamYazi = await context.Entry(blog)
    .Collection(b => b.Posts)
    .Query()
    .CountAsync();

Burada iki ayrıntıya dikkat edin. Birincisi, filtrelenmiş include kullanılan Posts navigation'ı takip eden sorguda "yüklendi" kabul edilir; yani blog.Posts yalnızca beş yazı içerir ve EF Core onu sonradan kendiliğinden tamamlamaz. Toplam sayıyı blog.Posts.Count ile değil veritabanına giden CountAsync ile almanın nedeni budur. İkincisi, aynı context'e daha önce bu bloga ait başka yazılar yüklendiyse fixup nedeniyle koleksiyonda beşten fazla yazı görebilirsiniz; ayrıntı sayfası gibi salt okunur senaryolarda sorguya AsNoTracking() eklemek bu sürprizi ortadan kaldırır. Koleksiyonda sıralı bir Take kullanıldığı için sıralamanın belirleyici olmasına da dikkat edin; aynı puana sahip yazılar varsa sıralamaya benzersiz bir anahtar eklemek sonucu öngörülebilir kılar.

Hangi yöntemi ne zaman seçmeli?

Hangi yöntemi ne zaman seçmeli? Include / ThenInclude, Filtrelenmiş include, Entry().Collection/Reference().LoadAsync, Entry().Collection().Query()

Belgelerin performans sayfası net bir öneri verir: ilişkili verilerden neye ihtiyaç duyacağınızı önceden biliyorsanız, örneğin belirli blogları tüm yazılarıyla birlikte yükleyecekseniz, eager loading her zaman daha iyidir, çünkü EF Core gereken her şeyi tek gidişte getirebilir. Filtrelenmiş include, yüklemeyi eager tutarken hangi ilişkili varlıkların geleceğini sınırlamanıza da izin verir. Ana varlığı yükledikten sonra başka bir kaynağa (örneğin bir web servisine) danışmadan ilişkili veriye ihtiyacınız olup olmadığını bilemiyorsanız explicit ya da lazy loading kullanılabilir; ancak bunlar ek gidiş-dönüş demektir ve belgeler, senaryoya göre tüm yazıları her seferinde yüklemenin seçici yüklemeden daha verimli olabileceğini hatırlatır.

YöntemNe zaman çalışırGidiş-dönüşUygun durum
Include / ThenIncludeAna sorguyla birlikteTek sorgu (split ile birden fazla)Gereken ilişki önceden biliniyor
Filtrelenmiş includeAna sorguyla birlikteTek sorguKoleksiyonun yalnızca bir kısmı gerekiyor
Entry().Collection/Reference().LoadAsyncSiz çağırdığınızdaHer yükleme için ek sorguİhtiyaç sonradan belli oluyor
Entry().Collection().Query()Siz çağırdığınızdaEk sorguSayma ya da filtreli kısmi yükleme
Projeksiyon (Select)Ana sorguyla birlikteTek sorguSalt okunur, yalnızca bazı alanlar
Lazy loadingNavigation'a erişildiğindeHer erişimde ek sorgu riskiBelgeler kaçınmayı önerir

Projeksiyon satırı ayrıca vurgulanmalı. Yalnızca okuyup göstereceğiniz veriler için Select(b => new { b.Url, b.Posts }) gibi bir projeksiyon, belgelerin örneğinde olduğu gibi blogları yazılarıyla birlikte tek sorguda getirir ve gereksiz kolonları taşımaz. Bu teknik değiştirip kaydedeceğiniz varlıklar için uygun değildir, çünkü anonim tür context tarafından bir varlık olarak takip edilmez. Ham SQL ile başlayan sorgularda da Include kullanılabilir; bu konu FromSql ile ham SQL sorguları yazısında.

Sık yapılan hatalar

  • Include yazmadan navigation'ın dolmasını beklemek: Lazy loading kapalıysa ve ilişkili veri daha önce yüklenmediyse navigation boş ya da null kalır. Bazen dolu gelmesi fixup sayesindedir; buna güvenmeyin, ihtiyacınızı açıkça Include ile belirtin.
  • Aynı seviyede çok sayıda koleksiyon dahil etmek: Kartezyen patlamaya yol açar. Gerekirse split query kullanın ya da sorguyu bölün.
  • Aynı navigation'a farklı filtreler yazmak: Filtrelenmiş include her navigation için tek filtre kümesine izin verir.
  • Filtrelenmiş include sonucunu takip eden bir context'te yorumlamak: Daha önce yüklenmiş varlıklar sonuca karışabilir; no-tracking sorgu ya da yeni context kullanın.
  • Döngü içinde LoadAsync çağırmak: Her blog için ayrı yükleme, N+1 sorgu sorununun açık hâlidir. Tüm blogların yazıları gerekiyorsa tek bir Include çok daha az gidiş-dönüş demektir.
  • Döngüsel nesne grafiğini serileştirmek: Fixup sonucu blog ve yazılar birbirini gösterir; JSON serileştiricileri bu döngüde hata verebilir. Çözüm seçenekleri lazy loading yazısının serileştirme bölümünde.

Sık Sorulan Sorular

Include ile ThenInclude arasındaki fark nedir?

Include sorgunun kök varlığından başlayan ilk ilişkiyi belirtir; ThenInclude ise bir önceki Include ya da ThenInclude ile gelen varlığın ilişkisine iner. Yeni bir kökten başlamak için tekrar Include yazarsınız.

Aynı Include'u iki kez yazmak iki join mi üretir?

Belgelere göre hayır; aynı yolu farklı dallar için kökten tekrar yazmanız gerekir ama EF Core çoğu durumda join'leri SQL üretirken birleştirir.

AutoInclude edilen bir navigation'ı tek bir sorguda nasıl kapatırım?

Sorguya IgnoreAutoIncludes() ekleyin. Bu, kullanıcının auto-include olarak yapılandırdığı tüm navigation'ları o sorgu için devre dışı bırakır; owned türlere giden navigation'lar ise yine yüklenir.

Explicit loading ile lazy loading aynı şey mi?

İkisi de ilişkili veriyi sonradan ve ayrı bir sorguyla getirir, ama explicit loading'de yükleme kodda açıkça görünür ve LoadAsync ile asenkron yapılabilir. Lazy loading ise navigation özelliğine erişildiği anda kendiliğinden ve senkron olarak çalışır. Belgeler, eager ya da explicit loading'in veritabanı gidişlerini kaynak kodunda açıkça gösterdiği için tercih edilmesini ö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 · Split query ve AsSplitQuery · EF Core lazy loading · AsNoTracking ve tracking sorguları · One-to-many ilişki · EF Core performans optimizasyonu