EF Core'da complex type, kendi kimliği (anahtarı) olmayan ve bir entity'nin içinde yaşayan bir değer nesnesini, örneğin bir Address ya da Money türünü eşlemenin yoludur. Entity Framework Core 10 ile complex type'lar hem ana tablonun kolonlarına (table splitting) hem de tek bir JSON kolonuna eşlenebiliyor; belgeler değer nesneleri için daha önce önerilen owned entity türleri yerine artık complex type'ları öneriyor.

Bu yazıda complex type ile owned entity arasındaki farkı, ComplexProperty ve [ComplexType] yapılandırmasını, EF Core 10'un getirdiği optional complex type, struct, koleksiyon ve JSON eşlemesini, ilgili breaking change'leri, owned entity türlerinin hâlâ geçerli kullanımını ve normal entity'lerde table ile entity splitting'i anlatıyoruz. Serinin tamamı için EF Core öğrenme rehberi sayfasına bakabilirsiniz.

Üç tür nesne: primitive, entity, değer nesnesi

Belgeler veritabanına kaydedilen nesneleri üç gruba ayırır. Tek bir değer tutan yapısız nesneler (int, Guid, string) gevşek bir ifadeyle primitive türlerdir. Birden çok değer tutan ve kimliği bir anahtar değeriyle belirlenen nesneler (Blog, Customer) entity türleridir. Birden çok değer tutan ama kimliğini belirleyen bir anahtarı olmayan nesneler (Address, Coordinate) ise değer nesneleridir ve EF Core bunları complex type olarak eşler. Complex type kendi başına takip edilemez ve sorgulanamaz; yalnızca içinde bulunduğu entity'nin parçası olarak vardır.

Complex type ile owned entity arasındaki fark

Complex type ile owned entity arasındaki fark: Kimlik, Örnek paylaşımı, Atama, .NET tipi

Complex type'lar EF Core 8 ile geldi ve sonraki sürümlerde önemli ölçüde genişletildi. Daha önce anahtarı olmayan nesneler için owned entity türleri öneriliyordu. Sorun şu ki owned türler perde arkasında hâlâ entity'dir: gizli bir anahtarları ve kimlikleri vardır, dolayısıyla referans semantiğiyle çalışırlar. Belgelerdeki karşılaştırma şöyledir:

KonuOwned entityComplex type
KimlikGizli anahtar ve kimlik varKimlik yok; değerle karşılaştırılır
Örnek paylaşımıAynı örnek iki kez kullanılamazAynı örnek birden çok özelliğe atanabilir
AtamaReferans semantiğiDeğer semantiği; özellikler kopyalanır
.NET tipiYalnızca referans tipiReferans veya değer tipi (struct)
Tablo eşlemeAyrı tablo, table splitting veya JSONAna tablo (table splitting) veya JSON
NavigationBaşka entity lere navigation içerebilirNavigation içeremez
ExecuteUpdate ile toplu güncellemeDesteklenmezDesteklenir

Farkın en somut görüldüğü yer şu kod parçasıdır: bir müşterinin fatura adresini teslimat adresiyle aynı yapmak, owned entity kullanırken SaveChanges sırasında hata verir, çünkü aynı entity örneği birden fazla yerden gösterilemez. Complex type'ta ise atama özellikleri kopyalar ve beklendiği gibi çalışır. Benzer şekilde LINQ sorgusunda iki owned değeri karşılaştırmak kimliklerini, iki complex değeri karşılaştırmak içeriklerini karşılaştırır.

csharp
var customer = await context.Customers.SingleAsync(c => c.Id == someId);
customer.BillingAddress = customer.ShippingAddress;
await context.SaveChangesAsync(); // owned entity ile hata verir, complex type ile çalışır

Complex type yapılandırma

Çoğu entity türünden farklı olarak complex type'lar konvansiyonla keşfedilmez. Türü [ComplexType] attribute'u ile işaretlemeniz ya da her özellik için OnModelCreating içinde ComplexProperty çağırmanız gerekir. Belgelerdeki örnek değişmez bir record kullanır:

csharp
[ComplexType]
public record Address
{
    public required string Line1 { get; init; }
    public string? Line2 { get; init; }
    public required string City { get; init; }
    public required string Country { get; init; }
    public required string PostCode { get; init; }
}

public class Customer
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public required Address Address { get; set; }      // required complex property
    public Address? SecondaryAddress { get; set; }     // optional complex property (EF Core 10)
}

Attribute yerine Fluent API tercih ederseniz modelBuilder.Entity<Customer>(b => { b.ComplexProperty(c => c.Address); b.ComplexProperty(c => c.SecondaryAddress); }); yazarsınız. İlişkisel veritabanında complex type kendi tablosunu almaz; özellikleri içinde bulunduğu entity'nin tablosuna ek kolonlar olarak yazılır. Belgelerdeki örnek INSERT çıktısında kolon adlarının Address_City, Address_Line1 biçiminde, yani özellik adı önekiyle oluştuğu görülür:

sql
INSERT INTO [Customers] ([Name], [Address_City], [Address_Country], [Address_Line1], [Address_Line2], [Address_PostCode])
OUTPUT INSERTED.[Id]
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);

Complex type içindeki skaler özellikleri, entity özellikleri gibi yapılandırabilirsiniz; iç içe Property builder'ı kolon adı ve uzunluk gibi ayarları kabul eder. Kolon yapılandırmasının genel kuralları property ve kolon yapılandırma yazısında.

csharp
modelBuilder.Entity<Order>()
    .ComplexProperty(
        o => o.ShippingAddress,
        b =>
        {
            b.Property(a => a.Line1).HasColumnName("ShipsToStreet").HasMaxLength(100);
            b.Property(a => a.City).HasColumnName("ShipsToCity");
        });

Referans tipi, struct ve değişmezlik

Referans tipi, struct ve değişmezlik

Complex type bir referans tipi (class veya record) ya da EF Core 10 ile birlikte bir değer tipi (struct veya record struct) olabilir. Referans tipinde dikkat edilmesi gereken nokta paylaşımdır: aynı örnek birden çok özelliğe atanabildiği için örneğin bir özelliğini değiştirmek, değeri kullanıldığı her yerde değiştirir. Bu genellikle istenen bir şey değildir. Belgelerin önerdiği çözüm complex type'ı değişmez yapmaktır; değeri değiştirmek için yeni bir örnek oluşturursunuz:

csharp
// Address değişmez bir record olduğu için yeni örnek oluşturulur
customer.Address = customer.Address with { Line1 = "Peacock Lodge" };

await context.SaveChangesAsync();

Bütün bir Address örneği yeniden atanmış olsa da EF değişiklikleri özellik düzeyinde takip eder; belgelerdeki örnek çıktıda yalnızca değişen Address_Line1 kolonu güncellenir. Değişmezlik init-only veya salt okunur özellikli bir class, bir record, bir readonly struct ya da readonly record struct ile sağlanabilir. Struct'lar kopyalama semantiğine sahip olduğu için kazara paylaşım sorununu zaten önler, ama C#'ta değiştirilebilir struct'lar genellikle önerilmez. Birkaç entity'nin gerçekten aynı adresi görmesi ve adres değişince birlikte güncellenmesi gerekiyorsa, belgelerin önerisi adresi kendi kimliği olan bir entity olarak modellemek ve navigation ile göstermektir; bunun için one-to-many veya one-to-one ilişki kullanılır.

İç içe ve optional complex type

Complex type başka complex type'lar içerebilir; örneğin bir Contact türü bir Address ve iki PhoneNumber taşıyabilir. Table splitting ile eşlendiğinde iç içe türlerin kolonları özelliğin tam yoluyla öneklenir, örneğin Contact_HomePhone_Number. Bu davranış EF Core 10 ile değişti: breaking changes belgesine göre önceden iç içe özellikler yalnızca bildiren türün adıyla (NestedComplex_Property) eşleniyordu, şimdi tam yol (Complex_NestedComplex_Property) kullanılıyor. Eski kolon adlarını korumanız gerekiyorsa HasColumnName ile açıkça verin; aksi hâlde yükseltmeden sonra oluşturulan migration kolonları yeniden adlandırmak isteyebilir.

Varsayılan olarak complex property required'dır: CLR özelliği her zaman bir değer taşımalıdır ve nullable olmayan kolonlara eşlenir. EF Core 10 ile complex property'yi nullable tanımlayarak optional yapabilirsiniz; değeri null olan optional bir complex property bütün kolonlarında NULL üretir. Burada bir koşul var: optional complex type'ın en az bir required özelliği olmalıdır. EF, değeri null olan bir complex type ile bütün özellikleri tesadüfen null olan bir complex type'ı ayırt etmek için en az bir nullable olmayan kolona ihtiyaç duyar. Türün kendi required özelliği yoksa bir discriminator yapılandırabilirsiniz; EF bunu varsayılan olarak required bir shadow property olarak oluşturur ve koşul sağlanmış olur:

csharp
modelBuilder.Entity<Place>()
    .ComplexProperty(p => p.Location, b => b.HasDiscriminator());

Breaking changes belgesindeki bir değişiklik daha bu bölümle ilgilidir: EF Core 10'dan önce farklı complex type'lardaki aynı adlı özellikler sessizce aynı kolonu paylaşabiliyordu; artık kolon adlarının sonuna bir sayı eklenerek benzersiz hâle getiriliyor. Amaç, yanlışlıkla aynı kolona eşlenen özelliklerin veri bozmasını önlemek. Paylaşım gerçekten istiyorsanız her iki tarafta HasColumnName ile aynı adı açıkça vermelisiniz.

JSON kolonuna eşleme ve koleksiyonlar

Table splitting'e ek olarak EF Core 10, koleksiyon olmayan bir complex property'yi ToJson ile tek bir JSON kolonuna eşlemeye izin verir. Bu durumda değer birden çok kolona dağıtılmaz, tek bir kolonda serileştirilmiş olarak durur:

csharp
modelBuilder.Entity<Customer>(b =>
{
    b.ComplexProperty(c => c.Address, c => c.ToJson());
    b.ComplexProperty(c => c.SecondaryAddress, c => c.ToJson());
});

Belgelerin SQL Server örnek çıktısında [Address] json NOT NULL ve [SecondaryAddress] json NULL kolonları görülür. Kolon tipi veritabanına bağlıdır: EF 10, UseAzureSql ile ya da 170 veya üzeri uyumluluk düzeyiyle (SQL Server 2025) yapılandırıldığında yeni yerel json veri tipini varsayılan olarak kullanır; diğer veritabanlarında ve eski SQL Server sürümlerinde JSON bir metin kolonunda saklanır. HasColumnType ile kolon tipini kendiniz belirleyebilirsiniz. Belgelere göre UseSqlServer ile varsayılan uyumluluk düzeyi şu an 150 (SQL Server 2019) olduğu için yerel SQL Server kurulumlarında json tipi varsayılan olarak kullanılmaz.

EF Core 10 ile bir özellik complex type koleksiyonu da tutabilir. İlişkisel veritabanlarında complex koleksiyonlar mutlaka tek bir JSON kolonuna eşlenir; ayrı bir tabloya eşlenemez. Koleksiyonun her elemanı JSON dizisi içinde bir nesne olarak saklanır. Struct koleksiyonları şu an desteklenmez; eleman tipi class veya record olmalıdır.

csharp
public class Distributor
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public List<Address> ShippingCenters { get; set; } = new();
}

modelBuilder.Entity<Distributor>()
    .ComplexCollection(d => d.ShippingCenters, b => b.ToJson());

JSON eşlemesi, table splitting'ten farklı olarak eşlenen tür içinde koleksiyonlara izin verir ve doküman içindeki tek tek özellikleri diğer özellikler gibi sorgulamanıza ve güncellemenize olanak tanır. JSON kolonları içindeki değerler ExecuteUpdateAsync ile toplu olarak da güncellenebilir; bu konu ExecuteUpdate ve ExecuteDelete yazısında anlatılıyor. EF 10 sürüm notları complex type'ların doküman modelleme tekniklerini getirdiğini ve geleneksel JOIN'lerden kaçınıldığı için önemli performans kazanımları sağlayabileceğini belirtir.

Sorgulama ve change tracking

Complex type üyeleri LINQ sorgularında entity'nin kendi özellikleri gibi kullanılabilir: filtreleyebilir, projekte edebilir ve sıralayabilirsiniz. Değer semantiği sayesinde iki complex değeri bütün olarak da karşılaştırabilirsiniz; EF bütün özelliklerini karşılaştırır:

csharp
var ukCities = await context.Customers
    .Where(c => c.Address.Country == "UK")
    .Select(c => c.Address.City)
    .ToListAsync();

var ordersToHomeAddress = await context.Orders
    .Where(o => o.ShippingAddress == o.BillingAddress)
    .ToListAsync();

Change tracker complex type'ın tek tek özelliklerini takip eder; SaveChanges yalnızca etkilenen kolonları günceller. Bu durumu incelemek için context.Entry(customer).ComplexProperty(c => c.Address) ile bir ComplexPropertyEntry alır, oradan Property(a => a.City) ile skaler özelliğe inersiniz; CurrentValue ve IsModified değerlerini okuyup ayarlayabilirsiniz. Bu API entity'ler için kullanılan EntityEntry API'sini yansıtır; ayrıntılar change tracker yazısında. Belgelerde JSON yolları için anlatılan EF.Functions.JsonPathExists ise EF Core 11 ile geliyor ve geliştirme aşamasında.

Complex type sınırlamaları

Complex type sınırlamaları: Kendi kimliği ve takibi yoktur, Entity türlerine navigation içeremez., İlişkisel veritabanında hiçbir zaman kendi tablosunda saklanmaz; ya ana...
  • Kendi kimliği ve takibi yoktur: DbSet<T> açılamaz, tek başına takip edilemez veya sorgulanamaz.
  • Entity türlerine navigation içeremez.
  • İlişkisel veritabanında hiçbir zaman kendi tablosunda saklanmaz; ya ana tabloda ya da bir JSON kolonunda durur.
  • Koleksiyonlar ilişkisel sağlayıcılarda JSON gerektirir; table splitting ile eşlenemez.
  • Değer tipi (struct) koleksiyonları desteklenmez.
  • Optional complex type en az bir required özellik içermelidir.
  • Belgelere göre EF Core henüz complex type'lar için kalıtımı desteklemez.

Owned entity türleri ne zaman hâlâ anlamlı?

Owned entity türleri yalnızca başka entity'lerin navigation özelliklerinde görünebilen entity'lerdir; içinde bulundukları entity onların sahibidir (owner). Kavramsal olarak aggregate'lere benzerler ve tanım gereği ilişkinin dependent tarafındadırlar. Belgeler değer nesneleri için complex type'ı öneriyor, ama tablodaki farklara bakınca owned türlerin karşılığı olmayan iki yeteneği öne çıkıyor: ayrı bir tabloya eşlenebilmeleri ve başka entity'lere navigation içerebilmeleri. Ayrıca mevcut projelerde yaygın olarak kullanıldıkları için nasıl çalıştıklarını bilmek gerekir.

Çoğu sağlayıcıda bir tür konvansiyonla owned yapılmaz; OwnsOne çağrısı ya da [Owned] attribute'u gerekir. Azure Cosmos DB sağlayıcısı bir istisnadır: doküman veritabanı olduğu için ilişkili bütün entity türlerini varsayılan olarak owned yapar. Owned türün kendi anahtar özelliği tanımlaması gerekmez; EF bir shadow birincil anahtar oluşturur ve değeri sahibin anahtarıyla aynıdır. İlişkisel veritabanında referans owned türler varsayılan olarak sahibin tablosuna eşlenir ve kolonlar Navigation_Özellik kalıbıyla adlandırılır (ShippingAddress_Street gibi); HasColumnName ile değiştirebilir, ToTable ile ayrı bir tabloya taşıyabilirsiniz.

csharp
modelBuilder.Entity<Order>().OwnsOne(
    o => o.ShippingAddress,
    sa =>
    {
        sa.Property(p => p.Street).HasColumnName("ShipsToStreet");
        sa.Property(p => p.City).HasColumnName("ShipsToCity");
    });

// Ayrı tabloya taşımak:
modelBuilder.Entity<DetailedOrder>().OwnsOne(p => p.OrderDetails, od => { od.ToTable("OrderDetails"); });

Owned koleksiyonlar OwnsMany ile yapılandırılır. Bu durumda sahibin anahtarı tek başına yetmez, çünkü bir sahibin birden çok owned örneği olabilir; EF varsayılan olarak foreign key ile ek bir int özellikten oluşan bileşik anahtar kullanır, örneğin ("DistributorId", "Id"). Owned türler sahip sorgulandığında kendiliğinden yüklenir; ayrı tabloda olsalar bile Include gerekmez. Belgelerin listelediği sınırlamalar şunlardır: owned tür için DbSet<T> açılamaz, ModelBuilder üzerinde Entity<T>() çağrılamaz, bir owned örnek birden çok sahip tarafından paylaşılamaz ve owned türler kalıtım hiyerarşisine sahip olamaz.

Normal entity'lerde table splitting ve entity splitting

Aynı tabloyu paylaşma fikri yalnızca değer nesnelerine özgü değildir. Table splitting'de iki ya da daha fazla normal entity türü aynı satıra eşlenir. Bunun için türlerin aynı tabloya eşlenmesi, birincil anahtarlarının aynı kolonlara gitmesi ve aynı tablodaki birincil anahtarlar arasında en az bir ilişki tanımlanması gerekir. Tipik kullanım, performans veya kapsülleme için tablonun yalnızca bir kolon alt kümesini kullanan hafif bir entity tanımlamaktır; belgelerdeki örnekte Order, DetailedOrder'ın bir alt kümesidir ve ilişki HasOne(o => o.DetailedOrder).WithOne().HasForeignKey<DetailedOrder>(o => o.Id) ile kurulur.

Table splitting'de dependent'ın bütün kolonları veritabanında NULL ise sorguda onun için örnek oluşturulmaz; bu, optional dependent modellemeye izin verir ama bütün özellikleri optional olan bir dependent'ın beklenmedik biçimde kaybolmasına da yol açabilir. Ek kontrol sorgu performansını etkileyebileceği için belgeler dependent'ı Navigation(...).IsRequired() ile zorunlu işaretlemeyi önerir. Paylaşılan tablodaki türlerden birinde concurrency token varsa diğerlerinde de bulunmalıdır; aksi hâlde yalnızca bir entity güncellendiğinde token bayatlar. Concurrency token'lar concurrency yazısında anlatılıyor.

Entity splitting ise tersidir: tek bir entity iki ya da daha fazla tablodaki satırlara eşlenir. Örneğin her zaman birlikte kullanılan Customers, PhoneNumbers ve Addresses tablolarını tek bir Customer türünde toplamak için her bölünme için SplitToTable çağrılır. Bu özellik belgelere göre EF7 ile geldi.

Sık yapılan hatalar

  • Complex type'ın konvansiyonla bulunacağını sanmak. [ComplexType] ya da ComplexProperty olmadan EF bu türü bir entity gibi ele almaya çalışır; anahtarı olmadığı için model kurulamaz.
  • Değiştirilebilir referans tipi kullanıp örneği paylaşmak. Bir adres örneğinin bir özelliğini değiştirmek, o örneğin atandığı her yeri değiştirir; record ve with ifadesi kullanın.
  • Required özelliği olmayan optional complex type. Koşul sağlanmazsa model kurulmaz; bir özelliği required yapın ya da discriminator ekleyin.
  • EF 10 yükseltmesinde migration'ı incelememek. İç içe complex type kolon adları ve SQL Server json tipi değişiklikleri yeni migration'da kolon yeniden adlandırma ya da tip değişikliği olarak görünebilir.
  • Owned entity ile adres kopyalamak. BillingAddress = ShippingAddress owned türlerde hata verir; complex type'a geçmek ya da yeni bir örnek oluşturmak gerekir.
  • EF Core 11 özelliklerini EF Core 10'da beklemek. Zincirleme Property(c => c.Address.Line1), complex type içinde indeks ve JsonPathExists geliştirme aşamasındaki EF Core 11'e aittir.

Sık Sorulan Sorular

Adres gibi bir değer için owned entity mi, complex type mı?

Belgelerin önerisi complex type'tır: kimliği yoktur, değerle karşılaştırılır, aynı örnek birden çok özelliğe atanabilir ve ExecuteUpdate ile toplu güncelleme desteklenir. Owned entity'yi değer nesnesinin ayrı tabloda durması ya da başka entity'lere navigation içermesi gerektiğinde düşünün.

Table splitting mi, JSON mu?

Table splitting her özelliği ayrı bir kolona yazar ve klasik ilişkisel şemaya en yakın seçenektir. JSON tek kolonda tutar ve içinde koleksiyonlara izin verir; complex koleksiyonlar ilişkisel sağlayıcılarda zaten yalnızca JSON ile eşlenebilir. Kolon tipi sağlayıcıya ve SQL Server uyumluluk düzeyine bağlıdır.

Complex type'ı struct olarak tanımlayabilir miyim?

Evet, EF Core 10 ile struct ve record struct desteklenir. Ancak struct koleksiyonları şu an desteklenmez ve belgeler değiştirilebilir struct yerine değişmez bir biçim önerir.

Owned türleri sorgularken Include gerekir mi?

Hayır. Owned türler sahip sorgulandığında, ayrı bir tabloda dursalar bile kendiliğinden yüklenir. Normal ilişkiler için Include yazısına bakın.

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 · TPH, TPT ve TPC · One-to-one ilişki · ExecuteUpdate ve ExecuteDelete · Property ve kolon yapılandırma · EF Core 10 yenilikleri