EF Core'da one-to-one (bire bir) ilişki, bir entity'nin en fazla bir başka entity ile ilişkili olduğu durumdur: belgelerdeki örnekte her Blog'un bir BlogHeader'ı vardır ve her BlogHeader tek bir bloga aittir. Entity Framework Core'da bu ilişki HasOne(...).WithOne(...) ile yapılandırılır ve one-to-many'den farklı olarak foreign key'in hangi tarafta olduğunu, yani dependent'ı, çoğu zaman HasForeignKey<T> ile açıkça belirtmeniz gerekir.
Bu yazıda dependent tarafın nasıl seçileceğini, required ve optional one-to-one ilişkiyi, birincil anahtarı foreign key olarak kullanan PK-to-PK ilişkiyi, shadow foreign key'i, tek yönlü varyasyonları, alternatif ve bileşik anahtarları ve self-referencing örneği anlatıyoruz. Temel kavramlar için EF Core ilişkiler yazısına, serinin tamamı için EF Core öğrenme rehberi sayfasına bakabilirsiniz.
Required one-to-one
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public BlogHeader? Header { get; set; } // dependent'a giden reference navigation
}
// Dependent (child)
public class BlogHeader
{
public int Id { get; set; }
public int BlogId { get; set; } // required foreign key
public Blog Blog { get; set; } = null!; // principal'a giden required reference navigation
}Bir one-to-one ilişki şu parçalardan oluşur: principal'daki birincil veya alternatif anahtar (Blog.Id), dependent'taki foreign key (BlogHeader.BlogId) ve isteğe bağlı iki reference navigation. Foreign key nullable olmadığı için ilişki required'dır: her başlık bir bloga bağlı olmak zorundadır. Buna karşılık bir blog başlığı olmadan var olabilir; required ilişki principal tarafa "mutlaka bir dependent'ı olacak" garantisi vermez.
Bu model konvansiyonla keşfedilir, çünkü iki reference navigation eşleşmiştir ve foreign key yalnızca BlogHeader tarafında bulunmuştur; EF dependent'ı buradan anlar. Açık yapılandırmada dikkat etmeniz gereken nokta HasForeignKey'in generic parametresidir: iki taraf da tek bir referans olduğu için EF'e foreign key'in hangi türde olduğunu söylemeniz gerekir.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasOne(e => e.Header)
.WithOne(e => e.Blog)
.HasForeignKey<BlogHeader>(e => e.BlogId)
.IsRequired();
}Aynı ilişkiyi BlogHeader tarafından başlatıp HasOne(e => e.Blog).WithOne(e => e.Header) diye de yazabilirsiniz; generic parametre yine HasForeignKey<BlogHeader> olur. Belgelere göre iki yazım aynı modeli üretir. Yine de ilişkiyi yalnızca bir uçtan, bir kez yapılandırın.
Hangi taraf dependent olmalı?

One-to-one ilişkide principal ile dependent'ı ayırt etmek one-to-many'deki kadar belirgin değildir. Belgeler şu ölçütleri önerir:
- Tablolar veritabanında zaten varsa, foreign key kolon(lar)ını içeren tablo dependent türe eşlenmelidir.
- Bir tür diğeri olmadan mantıksal olarak var olamıyorsa genellikle dependent odur. Var olmayan bir blogun başlığı anlamsızdır; bu yüzden
BlogHeaderdoğal olarak dependent'tır. - Doğal bir ebeveyn ve çocuk ilişkisi varsa, çocuk genellikle dependent'tır.
Konvansiyon tarafında kural şudur: iki reference navigation eşleştiğinde foreign key yalnızca bir tarafta bulunursa o taraf dependent olur. Hiçbir tarafta ya da iki tarafta da uygun bir foreign key bulunursa EF dependent'ı belirleyemez ve dependent tarafın açıkça yapılandırılması gerektiğini bildiren bir hata fırlatır. Çözüm, HasForeignKey<T> ile foreign key'in bulunduğu türü göstermektir.
Optional one-to-one
Foreign key'i ve dependent'tan principal'a giden navigation'ı nullable yaptığınızda ilişki optional olur. Bu durumda bir BlogHeader, foreign key'i ve navigation'ı null yapılarak hiçbir bloga bağlı olmadan durabilir. Nullable reference types açıkken foreign key nullable ise dependent'taki navigation da nullable olmalıdır.
public class BlogHeader
{
public int Id { get; set; }
public int? BlogId { get; set; } // optional foreign key
public Blog? Blog { get; set; } // optional reference navigation
}
modelBuilder.Entity<Blog>()
.HasOne(e => e.Header)
.WithOne(e => e.Blog)
.HasForeignKey<BlogHeader>(e => e.BlogId)
.IsRequired(false);Konvansiyon, one-to-one ilişkilerde foreign key için benzersiz (unique) bir indeks oluşturur; ilişkinin "bire bir" kalmasını veritabanı tarafında sağlayan budur. Belgelerin gösterdiği örneğe göre SQLite'ta required ve optional ilişkide indeks aynıdır, SQL Server'da ise optional ilişkide null değerleri doğru ele almak için indekse bir IS NOT NULL filtresi eklenir. Belgelerdeki SQL Server örnek çıktısı şöyledir:
CREATE UNIQUE INDEX [IX_Author_BlogId] ON [Author] ([BlogId]) WHERE [BlogId] IS NOT NULL;PK-to-PK: birincil anahtarı foreign key olarak kullanmak
One-to-many'den farklı olarak one-to-one ilişkide dependent, kendi birincil anahtarını foreign key olarak kullanabilir. Buna PK-to-PK ilişki denir. Bunun için principal ve dependent'ın birincil anahtar tiplerinin aynı olması gerekir ve ortaya çıkan ilişki her zaman required'dır, çünkü dependent'ın birincil anahtarı nullable olamaz. Aşağıdaki modelde BlogHeader içinde ayrı bir BlogId yoktur:
public class BlogHeader
{
public int Id { get; set; }
public Blog Blog { get; set; } = null!;
}
modelBuilder.Entity<Blog>()
.HasOne(e => e.Header)
.WithOne(e => e.Blog)
.HasForeignKey<BlogHeader>();Foreign key konvansiyonla bulunamadığı için bu ilişkide principal ve dependent'ı göstermek zorundasınız; bunun olağan yolu HasForeignKey çağrısıdır (HasPrincipalKey de kullanılabilir ama belgelere göre bu daha az yaygındır). HasForeignKey<BlogHeader>() içine özellik vermediğinizde ve birincil anahtar uygunsa, birincil anahtar foreign key olarak kullanılır. Açıkça yazmak isterseniz HasForeignKey<BlogHeader>(e => e.Id) diyebilirsiniz.
Shadow foreign key ile one-to-one
PK-to-PK yerine ayrı ama sınıfta görünmeyen bir foreign key isterseniz, EF'e bir shadow foreign key oluşturmasını söyleyebilirsiniz. One-to-many'den farklı olarak burada konvansiyon dependent'ı kendisi bulamaz; ilişkiyi yapılandırmanız gerekir:
modelBuilder.Entity<Blog>()
.HasOne(e => e.Header)
.WithOne(e => e.Blog)
.HasForeignKey<BlogHeader>("BlogId")
.IsRequired();Nullable reference types açıksa shadow özelliğin nullable olup olmayacağı dependent'taki navigation'a göre belirlenir: Blog Blog required, Blog? Blog optional bir ilişki üretir. Nullable reference types kapalıysa shadow foreign key varsayılan olarak nullable olur; required istiyorsanız IsRequired() ekleyin, optional için IsRequired(false) yazabilirsiniz.
Tek yönlü one-to-one varyasyonları

Navigation'lardan biri kaldırıldığında yapılandırma farklılaşır ve bir noktada one-to-one ilişki farkında olmadan one-to-many'ye dönüşebilir. Bu bölüm o yüzden özellikle önemlidir.
Principal'a navigation yok
BlogHeader içinde BlogId var ama Blog navigation'ı yoksa ilişki yine konvansiyonla bulunur, çünkü foreign key dependent tarafı gösterir. Açık yazımda WithOne() argümansız çağrılır; navigation'ı olmayan taraftan başlarsanız HasOne<Blog>() ile tipi verirsiniz. Foreign key de navigation da kaldırılırsa HasForeignKey<BlogHeader>("BlogId") ile shadow foreign key ve dependent'ı birlikte belirtmeniz gerekir.
Dependent'a navigation yok: one-to-many tuzağı
Blog sınıfında Header navigation'ı yoksa ama BlogHeader.Blog varsa, EF konvansiyonla bu ilişkiyi one-to-many olarak ele alır. Bunun nedeni basittir: elinde tek, eşsiz bir reference navigation vardır ve konvansiyon bu durumda tek yönlü one-to-many kurar. Bire bir istiyorsanız en azından şu kadar yapılandırma yazmalısınız:
modelBuilder.Entity<BlogHeader>()
.HasOne(e => e.Blog)
.WithOne()
.HasForeignKey<BlogHeader>(e => e.BlogId)
.IsRequired();Bu fark yalnızca modelde kalmaz: one-to-many olarak bırakılan bir ilişkide foreign key indeksi benzersiz olmaz ve aynı bloga birden fazla başlık eklenmesini veritabanı engellemez. Migration oluşturduktan sonra indeks tanımında UNIQUE olup olmadığını kontrol etmek, bu hatayı erken yakalamanın kolay bir yoludur; migration iş akışı migration rehberinde anlatılıyor.
Hiç navigation yok
Navigation'ı hiç olmayan bir ilişki konvansiyonla keşfedilmez ve yalnızca foreign key değeri değiştirilerek yönetilir. En kısa yapılandırma modelBuilder.Entity<Blog>().HasOne<BlogHeader>().WithOne(); biçimindedir. Bu durumda da BlogHeader.BlogId konvansiyonla foreign key olarak bulunur; nullable değilse ilişki required, nullable ise optional olur.
Alternatif anahtar ve bileşik anahtar
Foreign key'i birincil anahtar yerine başka bir benzersiz özelliğe bağlamak için HasPrincipalKey kullanılır. One-to-one'da bu metot da generic parametre alır, çünkü principal'ın hangi tür olduğu belirtilmelidir:
modelBuilder.Entity<Blog>()
.HasOne(e => e.Header)
.WithOne(e => e.Blog)
.HasPrincipalKey<Blog>(e => e.AlternateId)
.HasForeignKey<BlogHeader>(e => e.BlogId)
.IsRequired();Principal'ın anahtarı bileşikse foreign key de aynı sayıda özellikten oluşmalıdır. Belgeler, bu ilişkinin ancak bileşik anahtar HasKey(e => new { e.Id1, e.Id2 }) ile açıkça tanımlandığında konvansiyonla bulunacağını vurgular; bileşik anahtarlar otomatik keşfedilmez. Açık yazımda HasPrincipalKey<Blog>(e => new { e.Id1, e.Id2 }) ve HasForeignKey<BlogHeader>(e => new { e.BlogId1, e.BlogId2 }) birlikte kullanılır. Parçalardan biri null olan bileşik foreign key'in tamamen null sayıldığını, dolayısıyla eşleşme üretmediğini unutmayın.
Cascade delete olmadan required one-to-one
Required ilişkiler konvansiyonla cascade delete kullanır, çünkü principal silindikten sonra dependent veritabanında var olamaz. Bunun yerine veritabanının hata vermesini (belgelerin ifadesiyle tipik olarak uygulamanın çökmesine yol açacak bir hata) tercih ediyorsanız silme davranışını OnDelete(DeleteBehavior.Restrict) ile değiştirebilirsiniz. Değerlerin tam karşılaştırması cascade delete yazısında.
Self-referencing one-to-one
Principal ve dependent aynı tür olabilir. Belgelerdeki örnekte her Person optional olarak başka bir Person'a bağlıdır:
public class Person
{
public int Id { get; set; }
public int? HusbandId { get; set; } // optional foreign key
public Person? Husband { get; set; } // principal'a giden navigation
public Person? Wife { get; set; } // dependent'a giden navigation
}
modelBuilder.Entity<Person>()
.HasOne(e => e.Husband)
.WithOne(e => e.Wife)
.HasForeignKey<Person>(e => e.HusbandId)
.IsRequired(false);Burada HasForeignKey<Person> dependent'ı ayırt etmez, çünkü iki taraf da Person'dır. Belgelere göre bu durumda yön navigation'larla belirlenir: HasOne içinde verilen navigation dependent'tan principal'a, WithOne içinde verilen ise principal'dan dependent'a gider.
Veritabanı tarafında ne oluşur?
Doğru yapılandırılmış bir one-to-one ilişkinin şemadaki en belirgin izi benzersiz indekstir. Belgelere göre required one-to-one ilişkide SQLite için oluşturulan indeks CREATE UNIQUE INDEX "IX_Author_BlogId" ON "Author" ("BlogId"); biçimindedir ve optional ilişkide SQLite'ta aynı kalır; SQL Server'da ise yukarıda gösterilen IS NOT NULL filtresi eklenir. Belgeler bu filtrenin amacını null foreign key değerlerini daha iyi ele almak olarak açıklar; optional ilişkide birçok dependent satırının foreign key'i boş olabilir. PK-to-PK ilişkide ise ayrı bir foreign key kolonu yoktur; dependent'ın birincil anahtarı aynı zamanda foreign key olduğu için benzersizlik birincil anahtar kısıtıyla zaten sağlanır ve EF, birincil anahtarla kapsanan kolonlar için ek indeks oluşturmaz.
Foreign key kısıtının adı one-to-many'deki gibi FK_<dependent tip>_<principal tip>_<foreign key özelliği> kalıbındadır ve HasConstraintName ile değiştirilebilir. Nullable bir foreign key özelliğini koruyup kolonu NOT NULL yapmak isterseniz ilişkiye IsRequired() eklemeniz yeterlidir; belgelere göre bu, kolonu foreign key özelliği nullable olsa bile non-nullable yapar.
Aynı tabloyu paylaşan one-to-one
Normal one-to-one ilişkide iki tür iki ayrı tabloya eşlenir ve principal'dan dependent'a giden navigation zorunlu yapılamaz. Tek istisna iki türün aynı tabloyu paylaştığı durumdur (table splitting veya owned türler). Orada Navigation(e => e.Header).IsRequired() ile dependent'ın her zaman var olacağını belirtebilirsiniz. Bu senaryo ve değer nesneleri için önerilen complex type yaklaşımı complex types ve owned entity yazısında anlatılıyor.
Sık yapılan hatalar

- Dependent'ı belirtmemek. İki tarafta reference navigation olup foreign key bulunamadığında EF hata verir;
HasForeignKey<T>ile foreign key'in bulunduğu türü yazın. - Generic parametreyi unutmak. One-to-one'da
HasForeignKey(e => e.BlogId)değil,HasForeignKey<BlogHeader>(e => e.BlogId)yazılır; aynısıHasPrincipalKey<Blog>için geçerlidir. - Principal'daki navigation'ı silip ilişkinin bire bir kalacağını sanmak. Konvansiyon bunu one-to-many yapar; benzersiz indeks oluşmaz.
- PK-to-PK'yi optional yapmaya çalışmak. Dependent'ın birincil anahtarı nullable olamayacağı için PK-to-PK ilişki her zaman required'dır.
- Tip uyuşmazlığı olan PK-to-PK. Principal ve dependent'ın birincil anahtar tipleri aynı olmalıdır.
Sık Sorulan Sorular
One-to-one için ayrı tablo mu, aynı tablo mu kullanmalıyım?
İkisi de mümkündür. Ayrı tablolar için bu yazıdaki HasOne/WithOne yapılandırması kullanılır. Bilgi kimliği olmayan bir değer nesnesiyse (adres gibi) ve ana tabloda saklanması yeterliyse complex type daha uygun olabilir; seçim için complex types yazısına bakın.
Foreign key için benzersiz indeksi kendim mi eklemeliyim?
Hayır. One-to-one ilişki doğru yapılandırıldığında EF foreign key için konvansiyonla benzersiz indeks oluşturur. Kendiniz ek indeks tanımlamak yerine ilişkinin gerçekten bire bir olarak algılandığını kontrol edin.
Blog silinince başlık da silinir mi?
Required one-to-one ilişkide konvansiyonla evet, cascade delete uygulanır. Optional ilişkide cascade uygulanmaz. Bunu OnDelete ile değiştirebilirsiniz.
Kendini gösteren one-to-one ilişkide hangi taraf dependent?
Tip aynı olduğu için HasForeignKey<Person> yön bilgisi vermez. HasOne'daki navigation dependent'tan principal'a, WithOne'daki navigation principal'dan dependent'a giden olarak yorumlanır.
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 ilişkiler · One-to-many ilişki · Many-to-many ilişki · Complex types ve owned entity · Cascade delete