[{"data":1,"prerenderedAt":1412},["ShallowReactive",2],{"blog-\u002Fblog\u002Fdotnet-ef-relationships":3},{"id":4,"title":5,"body":6,"description":1397,"difficulty":1398,"extension":1399,"framework":1400,"frameworkSlug":1401,"meta":1402,"navigation":78,"order":57,"path":1403,"qaPath":1404,"seo":1405,"stem":1406,"subtopic":1407,"topic":1408,"topicSlug":1409,"updated":1410,"__hash__":1411},"blog\u002Fblog\u002Fdotnet-ef-relationships.md","Configuring Relationships in EF Core",{"type":7,"value":8,"toc":1378},"minimark",[9,14,18,22,25,119,130,188,198,202,205,267,270,356,359,363,366,450,453,457,460,523,571,575,578,632,720,729,733,736,807,820,824,961,965,968,973,976,1006,1010,1013,1038,1042,1045,1065,1068,1072,1075,1140,1143,1147,1162,1268,1274,1278,1281,1320,1327,1331,1334,1374],[10,11,13],"h2",{"id":12},"why-relationships-matter-in-interviews","Why relationships matter in interviews",[15,16,17],"p",{},"EF Core relationship configuration is one of the most frequently tested topics because\ngetting it wrong — wrong cascade behaviour, missing unique indexes, incorrect FK placement —\ncauses data integrity bugs that are hard to detect without integration tests.",[10,19,21],{"id":20},"one-to-many-the-most-common-relationship","One-to-many — the most common relationship",[15,23,24],{},"A Customer has many Orders; each Order belongs to one Customer.",[26,27,32],"pre",{"className":28,"code":29,"language":30,"meta":31,"style":31},"language-csharp shiki shiki-themes github-light github-dark","public class Customer\n{\n    public int Id { get; set; }\n    public string Name { get; set; } = \"\";\n    public ICollection\u003COrder> Orders { get; set; } = new List\u003COrder>();\n}\n\npublic class Order\n{\n    public int Id { get; set; }\n    public int CustomerId { get; set; }          \u002F\u002F FK property\n    public Customer Customer { get; set; } = null!; \u002F\u002F reference navigation\n    public decimal Total { get; set; }\n}\n","csharp","",[33,34,35,43,49,55,61,67,73,80,86,91,96,102,108,114],"code",{"__ignoreMap":31},[36,37,40],"span",{"class":38,"line":39},"line",1,[36,41,42],{},"public class Customer\n",[36,44,46],{"class":38,"line":45},2,[36,47,48],{},"{\n",[36,50,52],{"class":38,"line":51},3,[36,53,54],{},"    public int Id { get; set; }\n",[36,56,58],{"class":38,"line":57},4,[36,59,60],{},"    public string Name { get; set; } = \"\";\n",[36,62,64],{"class":38,"line":63},5,[36,65,66],{},"    public ICollection\u003COrder> Orders { get; set; } = new List\u003COrder>();\n",[36,68,70],{"class":38,"line":69},6,[36,71,72],{},"}\n",[36,74,76],{"class":38,"line":75},7,[36,77,79],{"emptyLinePlaceholder":78},true,"\n",[36,81,83],{"class":38,"line":82},8,[36,84,85],{},"public class Order\n",[36,87,89],{"class":38,"line":88},9,[36,90,48],{},[36,92,94],{"class":38,"line":93},10,[36,95,54],{},[36,97,99],{"class":38,"line":98},11,[36,100,101],{},"    public int CustomerId { get; set; }          \u002F\u002F FK property\n",[36,103,105],{"class":38,"line":104},12,[36,106,107],{},"    public Customer Customer { get; set; } = null!; \u002F\u002F reference navigation\n",[36,109,111],{"class":38,"line":110},13,[36,112,113],{},"    public decimal Total { get; set; }\n",[36,115,117],{"class":38,"line":116},14,[36,118,72],{},[15,120,121,122,125,126,129],{},"EF Core detects ",[33,123,124],{},"CustomerId"," by convention (entity name + ",[33,127,128],{},"Id","). Use Fluent API for\nnon-conventional names or to control cascade behaviour:",[26,131,133],{"className":28,"code":132,"language":30,"meta":31,"style":31},"public class OrderConfiguration : IEntityTypeConfiguration\u003COrder>\n{\n    public void Configure(EntityTypeBuilder\u003COrder> b)\n    {\n        b.HasOne(o => o.Customer)\n         .WithMany(c => c.Orders)\n         .HasForeignKey(o => o.CustomerId)\n         .IsRequired()\n         .OnDelete(DeleteBehavior.Restrict); \u002F\u002F error if customer deleted while orders exist\n    }\n}\n",[33,134,135,140,144,149,154,159,164,169,174,179,184],{"__ignoreMap":31},[36,136,137],{"class":38,"line":39},[36,138,139],{},"public class OrderConfiguration : IEntityTypeConfiguration\u003COrder>\n",[36,141,142],{"class":38,"line":45},[36,143,48],{},[36,145,146],{"class":38,"line":51},[36,147,148],{},"    public void Configure(EntityTypeBuilder\u003COrder> b)\n",[36,150,151],{"class":38,"line":57},[36,152,153],{},"    {\n",[36,155,156],{"class":38,"line":63},[36,157,158],{},"        b.HasOne(o => o.Customer)\n",[36,160,161],{"class":38,"line":69},[36,162,163],{},"         .WithMany(c => c.Orders)\n",[36,165,166],{"class":38,"line":75},[36,167,168],{},"         .HasForeignKey(o => o.CustomerId)\n",[36,170,171],{"class":38,"line":82},[36,172,173],{},"         .IsRequired()\n",[36,175,176],{"class":38,"line":88},[36,177,178],{},"         .OnDelete(DeleteBehavior.Restrict); \u002F\u002F error if customer deleted while orders exist\n",[36,180,181],{"class":38,"line":93},[36,182,183],{},"    }\n",[36,185,186],{"class":38,"line":98},[36,187,72],{},[15,189,190,191,193,194,197],{},"Always declare both the FK property (",[33,192,124],{},") and the navigation (",[33,195,196],{},"Customer",") on the\ndependent entity. EF Core generates a better schema and produces clearer error messages.",[10,199,201],{"id":200},"many-to-many-implicit-vs-explicit-join-entity","Many-to-many — implicit vs explicit join entity",[15,203,204],{},"EF Core 5+ supports implicit many-to-many with no join class required:",[26,206,208],{"className":28,"code":207,"language":30,"meta":31,"style":31},"public class Post\n{\n    public int Id { get; set; }\n    public ICollection\u003CTag> Tags { get; set; } = new List\u003CTag>();\n}\n\npublic class Tag\n{\n    public int Id { get; set; }\n    public string Name { get; set; } = \"\";\n    public ICollection\u003CPost> Posts { get; set; } = new List\u003CPost>();\n}\n\u002F\u002F EF infers a join table \"PostTag\" — no Fluent API needed for simple cases.\n",[33,209,210,215,219,223,228,232,236,241,245,249,253,258,262],{"__ignoreMap":31},[36,211,212],{"class":38,"line":39},[36,213,214],{},"public class Post\n",[36,216,217],{"class":38,"line":45},[36,218,48],{},[36,220,221],{"class":38,"line":51},[36,222,54],{},[36,224,225],{"class":38,"line":57},[36,226,227],{},"    public ICollection\u003CTag> Tags { get; set; } = new List\u003CTag>();\n",[36,229,230],{"class":38,"line":63},[36,231,72],{},[36,233,234],{"class":38,"line":69},[36,235,79],{"emptyLinePlaceholder":78},[36,237,238],{"class":38,"line":75},[36,239,240],{},"public class Tag\n",[36,242,243],{"class":38,"line":82},[36,244,48],{},[36,246,247],{"class":38,"line":88},[36,248,54],{},[36,250,251],{"class":38,"line":93},[36,252,60],{},[36,254,255],{"class":38,"line":98},[36,256,257],{},"    public ICollection\u003CPost> Posts { get; set; } = new List\u003CPost>();\n",[36,259,260],{"class":38,"line":104},[36,261,72],{},[36,263,264],{"class":38,"line":110},[36,265,266],{},"\u002F\u002F EF infers a join table \"PostTag\" — no Fluent API needed for simple cases.\n",[15,268,269],{},"When the join has extra columns (a timestamp, ordering flag, etc.), use an explicit join entity:",[26,271,273],{"className":28,"code":272,"language":30,"meta":31,"style":31},"public class PostTag\n{\n    public int PostId { get; set; }\n    public int TagId  { get; set; }\n    public DateTime TaggedAt { get; set; }\n    public Post Post { get; set; } = null!;\n    public Tag  Tag  { get; set; } = null!;\n}\n\nmodelBuilder.Entity\u003CPostTag>()\n    .HasKey(pt => new { pt.PostId, pt.TagId });\n\nmodelBuilder.Entity\u003CPostTag>()\n    .HasOne(pt => pt.Post).WithMany(p => p.PostTags).HasForeignKey(pt => pt.PostId);\n\nmodelBuilder.Entity\u003CPostTag>()\n    .HasOne(pt => pt.Tag).WithMany(t => t.PostTags).HasForeignKey(pt => pt.TagId);\n",[33,274,275,280,284,289,294,299,304,309,313,317,322,327,331,335,340,345,350],{"__ignoreMap":31},[36,276,277],{"class":38,"line":39},[36,278,279],{},"public class PostTag\n",[36,281,282],{"class":38,"line":45},[36,283,48],{},[36,285,286],{"class":38,"line":51},[36,287,288],{},"    public int PostId { get; set; }\n",[36,290,291],{"class":38,"line":57},[36,292,293],{},"    public int TagId  { get; set; }\n",[36,295,296],{"class":38,"line":63},[36,297,298],{},"    public DateTime TaggedAt { get; set; }\n",[36,300,301],{"class":38,"line":69},[36,302,303],{},"    public Post Post { get; set; } = null!;\n",[36,305,306],{"class":38,"line":75},[36,307,308],{},"    public Tag  Tag  { get; set; } = null!;\n",[36,310,311],{"class":38,"line":82},[36,312,72],{},[36,314,315],{"class":38,"line":88},[36,316,79],{"emptyLinePlaceholder":78},[36,318,319],{"class":38,"line":93},[36,320,321],{},"modelBuilder.Entity\u003CPostTag>()\n",[36,323,324],{"class":38,"line":98},[36,325,326],{},"    .HasKey(pt => new { pt.PostId, pt.TagId });\n",[36,328,329],{"class":38,"line":104},[36,330,79],{"emptyLinePlaceholder":78},[36,332,333],{"class":38,"line":110},[36,334,321],{},[36,336,337],{"class":38,"line":116},[36,338,339],{},"    .HasOne(pt => pt.Post).WithMany(p => p.PostTags).HasForeignKey(pt => pt.PostId);\n",[36,341,343],{"class":38,"line":342},15,[36,344,79],{"emptyLinePlaceholder":78},[36,346,348],{"class":38,"line":347},16,[36,349,321],{},[36,351,353],{"class":38,"line":352},17,[36,354,355],{},"    .HasOne(pt => pt.Tag).WithMany(t => t.PostTags).HasForeignKey(pt => pt.TagId);\n",[15,357,358],{},"Use implicit many-to-many when the join has no payload. Add an explicit join entity as soon\nas you need any extra column.",[10,360,362],{"id":361},"one-to-one","One-to-one",[15,364,365],{},"One-to-one links exactly one principal to at most one dependent. Always specify which end holds\nthe FK (EF Core can't infer it when both navigations are present):",[26,367,369],{"className":28,"code":368,"language":30,"meta":31,"style":31},"public class User    { public int Id { get; set; }  public UserProfile? Profile { get; set; } }\npublic class UserProfile\n{\n    public int Id { get; set; }\n    public int UserId { get; set; }   \u002F\u002F FK lives on the dependent\n    public User User { get; set; } = null!;\n}\n\nmodelBuilder.Entity\u003CUserProfile>()\n    .HasOne(p => p.User)\n    .WithOne(u => u.Profile)\n    .HasForeignKey\u003CUserProfile>(p => p.UserId)\n    .OnDelete(DeleteBehavior.Cascade);\n\n\u002F\u002F EF does NOT add a unique index automatically for one-to-one:\nmodelBuilder.Entity\u003CUserProfile>()\n    .HasIndex(p => p.UserId).IsUnique();\n",[33,370,371,376,381,385,389,394,399,403,407,412,417,422,427,432,436,441,445],{"__ignoreMap":31},[36,372,373],{"class":38,"line":39},[36,374,375],{},"public class User    { public int Id { get; set; }  public UserProfile? Profile { get; set; } }\n",[36,377,378],{"class":38,"line":45},[36,379,380],{},"public class UserProfile\n",[36,382,383],{"class":38,"line":51},[36,384,48],{},[36,386,387],{"class":38,"line":57},[36,388,54],{},[36,390,391],{"class":38,"line":63},[36,392,393],{},"    public int UserId { get; set; }   \u002F\u002F FK lives on the dependent\n",[36,395,396],{"class":38,"line":69},[36,397,398],{},"    public User User { get; set; } = null!;\n",[36,400,401],{"class":38,"line":75},[36,402,72],{},[36,404,405],{"class":38,"line":82},[36,406,79],{"emptyLinePlaceholder":78},[36,408,409],{"class":38,"line":88},[36,410,411],{},"modelBuilder.Entity\u003CUserProfile>()\n",[36,413,414],{"class":38,"line":93},[36,415,416],{},"    .HasOne(p => p.User)\n",[36,418,419],{"class":38,"line":98},[36,420,421],{},"    .WithOne(u => u.Profile)\n",[36,423,424],{"class":38,"line":104},[36,425,426],{},"    .HasForeignKey\u003CUserProfile>(p => p.UserId)\n",[36,428,429],{"class":38,"line":110},[36,430,431],{},"    .OnDelete(DeleteBehavior.Cascade);\n",[36,433,434],{"class":38,"line":116},[36,435,79],{"emptyLinePlaceholder":78},[36,437,438],{"class":38,"line":342},[36,439,440],{},"\u002F\u002F EF does NOT add a unique index automatically for one-to-one:\n",[36,442,443],{"class":38,"line":347},[36,444,411],{},[36,446,447],{"class":38,"line":352},[36,448,449],{},"    .HasIndex(p => p.UserId).IsUnique();\n",[15,451,452],{},"Always add a unique index on the FK column — without it the database doesn't enforce the\none-to-one constraint.",[10,454,456],{"id":455},"navigation-properties","Navigation properties",[15,458,459],{},"Navigation properties are how EF resolves the object graph. Three types:",[461,462,463,479],"table",{},[464,465,466],"thead",{},[467,468,469,473,476],"tr",{},[470,471,472],"th",{},"Type",[470,474,475],{},"Example",[470,477,478],{},"Purpose",[480,481,482,496,509],"tbody",{},[467,483,484,488,493],{},[485,486,487],"td",{},"Reference navigation",[485,489,490],{},[33,491,492],{},"Customer Customer",[485,494,495],{},"Points to one related entity",[467,497,498,501,506],{},[485,499,500],{},"Collection navigation",[485,502,503],{},[33,504,505],{},"ICollection\u003COrder> Orders",[485,507,508],{},"Points to many related entities",[467,510,511,514,520],{},[485,512,513],{},"Inverse navigation",[485,515,516,519],{},[33,517,518],{},"Order Order"," on OrderItem",[485,521,522],{},"Points back to the principal",[26,524,526],{"className":28,"code":525,"language":30,"meta":31,"style":31},"\u002F\u002F Initialise collections to avoid NullReferenceException:\npublic ICollection\u003COrderItem> Items { get; set; } = new List\u003COrderItem>();\n\n\u002F\u002F Make reference navigations nullable — they may not be loaded:\npublic Customer? Customer { get; set; }\n\n\u002F\u002F Check and load navigations on demand:\nif (!_db.Entry(order).Reference(o => o.Customer).IsLoaded)\n    await _db.Entry(order).Reference(o => o.Customer).LoadAsync();\n",[33,527,528,533,538,542,547,552,556,561,566],{"__ignoreMap":31},[36,529,530],{"class":38,"line":39},[36,531,532],{},"\u002F\u002F Initialise collections to avoid NullReferenceException:\n",[36,534,535],{"class":38,"line":45},[36,536,537],{},"public ICollection\u003COrderItem> Items { get; set; } = new List\u003COrderItem>();\n",[36,539,540],{"class":38,"line":51},[36,541,79],{"emptyLinePlaceholder":78},[36,543,544],{"class":38,"line":57},[36,545,546],{},"\u002F\u002F Make reference navigations nullable — they may not be loaded:\n",[36,548,549],{"class":38,"line":63},[36,550,551],{},"public Customer? Customer { get; set; }\n",[36,553,554],{"class":38,"line":69},[36,555,79],{"emptyLinePlaceholder":78},[36,557,558],{"class":38,"line":75},[36,559,560],{},"\u002F\u002F Check and load navigations on demand:\n",[36,562,563],{"class":38,"line":82},[36,564,565],{},"if (!_db.Entry(order).Reference(o => o.Customer).IsLoaded)\n",[36,567,568],{"class":38,"line":88},[36,569,570],{},"    await _db.Entry(order).Reference(o => o.Customer).LoadAsync();\n",[10,572,574],{"id":573},"cascade-delete","Cascade delete",[15,576,577],{},"EF Core supports four delete behaviours:",[461,579,580,590],{},[464,581,582],{},[467,583,584,587],{},[470,585,586],{},"Option",[470,588,589],{},"What happens to dependents",[480,591,592,602,612,622],{},[467,593,594,599],{},[485,595,596],{},[33,597,598],{},"Cascade",[485,600,601],{},"Deleted automatically with the principal",[467,603,604,609],{},[485,605,606],{},[33,607,608],{},"Restrict",[485,610,611],{},"Error thrown if any dependents exist",[467,613,614,619],{},[485,615,616],{},[33,617,618],{},"SetNull",[485,620,621],{},"FK set to NULL (FK must be nullable)",[467,623,624,629],{},[485,625,626],{},[33,627,628],{},"NoAction",[485,630,631],{},"Application must handle orphans manually",[26,633,635],{"className":28,"code":634,"language":30,"meta":31,"style":31},"\u002F\u002F Cascade — deleting Order also deletes its OrderItems:\nb.HasOne(i => i.Order)\n .WithMany(o => o.Items)\n .HasForeignKey(i => i.OrderId)\n .OnDelete(DeleteBehavior.Cascade);\n\n\u002F\u002F Restrict — can't delete a Customer with existing Orders:\nb.HasOne(o => o.Customer)\n .WithMany(c => c.Orders)\n .HasForeignKey(o => o.CustomerId)\n .OnDelete(DeleteBehavior.Restrict);\n\n\u002F\u002F SetNull — deleting Category sets Product.CategoryId to NULL:\nb.HasOne(p => p.Category)\n .WithMany(c => c.Products)\n .HasForeignKey(p => p.CategoryId)   \u002F\u002F int? (nullable)\n .OnDelete(DeleteBehavior.SetNull);\n",[33,636,637,642,647,652,657,662,666,671,676,681,686,691,695,700,705,710,715],{"__ignoreMap":31},[36,638,639],{"class":38,"line":39},[36,640,641],{},"\u002F\u002F Cascade — deleting Order also deletes its OrderItems:\n",[36,643,644],{"class":38,"line":45},[36,645,646],{},"b.HasOne(i => i.Order)\n",[36,648,649],{"class":38,"line":51},[36,650,651],{}," .WithMany(o => o.Items)\n",[36,653,654],{"class":38,"line":57},[36,655,656],{}," .HasForeignKey(i => i.OrderId)\n",[36,658,659],{"class":38,"line":63},[36,660,661],{}," .OnDelete(DeleteBehavior.Cascade);\n",[36,663,664],{"class":38,"line":69},[36,665,79],{"emptyLinePlaceholder":78},[36,667,668],{"class":38,"line":75},[36,669,670],{},"\u002F\u002F Restrict — can't delete a Customer with existing Orders:\n",[36,672,673],{"class":38,"line":82},[36,674,675],{},"b.HasOne(o => o.Customer)\n",[36,677,678],{"class":38,"line":88},[36,679,680],{}," .WithMany(c => c.Orders)\n",[36,682,683],{"class":38,"line":93},[36,684,685],{}," .HasForeignKey(o => o.CustomerId)\n",[36,687,688],{"class":38,"line":98},[36,689,690],{}," .OnDelete(DeleteBehavior.Restrict);\n",[36,692,693],{"class":38,"line":104},[36,694,79],{"emptyLinePlaceholder":78},[36,696,697],{"class":38,"line":110},[36,698,699],{},"\u002F\u002F SetNull — deleting Category sets Product.CategoryId to NULL:\n",[36,701,702],{"class":38,"line":116},[36,703,704],{},"b.HasOne(p => p.Category)\n",[36,706,707],{"class":38,"line":342},[36,708,709],{}," .WithMany(c => c.Products)\n",[36,711,712],{"class":38,"line":347},[36,713,714],{}," .HasForeignKey(p => p.CategoryId)   \u002F\u002F int? (nullable)\n",[36,716,717],{"class":38,"line":352},[36,718,719],{}," .OnDelete(DeleteBehavior.SetNull);\n",[15,721,722,723,725,726,728],{},"Use ",[33,724,608],{}," for business-critical data (orders, invoices). Use ",[33,727,598],{}," for true\nparent-child where the child has no independent existence (order items, line items).",[10,730,732],{"id":731},"owned-entities","Owned entities",[15,734,735],{},"Owned entities are value objects that share the owner's table and have no independent lifecycle:",[26,737,739],{"className":28,"code":738,"language":30,"meta":31,"style":31},"public class Address { public string Street { get; set; } = \"\"; public string City { get; set; } = \"\"; }\npublic class Customer { public int Id { get; set; } public Address ShippingAddress { get; set; } = new(); }\n\n\u002F\u002F Stored as columns in the Customers table:\nmodelBuilder.Entity\u003CCustomer>()\n    .OwnsOne(c => c.ShippingAddress, a =>\n    {\n        a.Property(x => x.Street).HasColumnName(\"Ship_Street\").HasMaxLength(100);\n        a.Property(x => x.City).HasColumnName(\"Ship_City\");\n    });\n\n\u002F\u002F Or as a JSON column (EF Core 7+):\nmodelBuilder.Entity\u003CCustomer>()\n    .OwnsOne(c => c.ShippingAddress, a => a.ToJson());\n",[33,740,741,746,751,755,760,765,770,774,779,784,789,793,798,802],{"__ignoreMap":31},[36,742,743],{"class":38,"line":39},[36,744,745],{},"public class Address { public string Street { get; set; } = \"\"; public string City { get; set; } = \"\"; }\n",[36,747,748],{"class":38,"line":45},[36,749,750],{},"public class Customer { public int Id { get; set; } public Address ShippingAddress { get; set; } = new(); }\n",[36,752,753],{"class":38,"line":51},[36,754,79],{"emptyLinePlaceholder":78},[36,756,757],{"class":38,"line":57},[36,758,759],{},"\u002F\u002F Stored as columns in the Customers table:\n",[36,761,762],{"class":38,"line":63},[36,763,764],{},"modelBuilder.Entity\u003CCustomer>()\n",[36,766,767],{"class":38,"line":69},[36,768,769],{},"    .OwnsOne(c => c.ShippingAddress, a =>\n",[36,771,772],{"class":38,"line":75},[36,773,153],{},[36,775,776],{"class":38,"line":82},[36,777,778],{},"        a.Property(x => x.Street).HasColumnName(\"Ship_Street\").HasMaxLength(100);\n",[36,780,781],{"class":38,"line":88},[36,782,783],{},"        a.Property(x => x.City).HasColumnName(\"Ship_City\");\n",[36,785,786],{"class":38,"line":93},[36,787,788],{},"    });\n",[36,790,791],{"class":38,"line":98},[36,792,79],{"emptyLinePlaceholder":78},[36,794,795],{"class":38,"line":104},[36,796,797],{},"\u002F\u002F Or as a JSON column (EF Core 7+):\n",[36,799,800],{"class":38,"line":110},[36,801,764],{},[36,803,804],{"class":38,"line":116},[36,805,806],{},"    .OwnsOne(c => c.ShippingAddress, a => a.ToJson());\n",[15,808,722,809,812,813,816,817,819],{},[33,810,811],{},"OwnsOne"," \u002F ",[33,814,815],{},"OwnsMany"," for DDD value objects — keeps the schema tidy and the domain\nmodel free of ",[33,818,128],{}," properties that don't carry business meaning.",[10,821,823],{"id":822},"self-referencing-relationships-hierarchies","Self-referencing relationships (hierarchies)",[26,825,827],{"className":28,"code":826,"language":30,"meta":31,"style":31},"public class Category\n{\n    public int Id { get; set; }\n    public string Name { get; set; } = \"\";\n    public int? ParentId { get; set; }\n    public Category? Parent { get; set; }\n    public ICollection\u003CCategory> Children { get; set; } = new List\u003CCategory>();\n}\n\nmodelBuilder.Entity\u003CCategory>()\n    .HasOne(c => c.Parent)\n    .WithMany(c => c.Children)\n    .HasForeignKey(c => c.ParentId)\n    .IsRequired(false)\n    .OnDelete(DeleteBehavior.Restrict);\n\n\u002F\u002F Load shallow trees with Include:\nvar categories = await _db.Categories\n    .Include(c => c.Children)\n        .ThenInclude(c => c.Children)\n    .Where(c => c.ParentId == null)\n    .ToListAsync();\n\n\u002F\u002F For deep trees, load all and build hierarchy in memory:\nvar all = await _db.Categories.AsNoTracking().ToListAsync();\nvar roots = all.Where(c => c.ParentId == null).ToList();\n",[33,828,829,834,838,842,846,851,856,861,865,869,874,879,884,889,894,899,903,908,914,920,926,932,938,943,949,955],{"__ignoreMap":31},[36,830,831],{"class":38,"line":39},[36,832,833],{},"public class Category\n",[36,835,836],{"class":38,"line":45},[36,837,48],{},[36,839,840],{"class":38,"line":51},[36,841,54],{},[36,843,844],{"class":38,"line":57},[36,845,60],{},[36,847,848],{"class":38,"line":63},[36,849,850],{},"    public int? ParentId { get; set; }\n",[36,852,853],{"class":38,"line":69},[36,854,855],{},"    public Category? Parent { get; set; }\n",[36,857,858],{"class":38,"line":75},[36,859,860],{},"    public ICollection\u003CCategory> Children { get; set; } = new List\u003CCategory>();\n",[36,862,863],{"class":38,"line":82},[36,864,72],{},[36,866,867],{"class":38,"line":88},[36,868,79],{"emptyLinePlaceholder":78},[36,870,871],{"class":38,"line":93},[36,872,873],{},"modelBuilder.Entity\u003CCategory>()\n",[36,875,876],{"class":38,"line":98},[36,877,878],{},"    .HasOne(c => c.Parent)\n",[36,880,881],{"class":38,"line":104},[36,882,883],{},"    .WithMany(c => c.Children)\n",[36,885,886],{"class":38,"line":110},[36,887,888],{},"    .HasForeignKey(c => c.ParentId)\n",[36,890,891],{"class":38,"line":116},[36,892,893],{},"    .IsRequired(false)\n",[36,895,896],{"class":38,"line":342},[36,897,898],{},"    .OnDelete(DeleteBehavior.Restrict);\n",[36,900,901],{"class":38,"line":347},[36,902,79],{"emptyLinePlaceholder":78},[36,904,905],{"class":38,"line":352},[36,906,907],{},"\u002F\u002F Load shallow trees with Include:\n",[36,909,911],{"class":38,"line":910},18,[36,912,913],{},"var categories = await _db.Categories\n",[36,915,917],{"class":38,"line":916},19,[36,918,919],{},"    .Include(c => c.Children)\n",[36,921,923],{"class":38,"line":922},20,[36,924,925],{},"        .ThenInclude(c => c.Children)\n",[36,927,929],{"class":38,"line":928},21,[36,930,931],{},"    .Where(c => c.ParentId == null)\n",[36,933,935],{"class":38,"line":934},22,[36,936,937],{},"    .ToListAsync();\n",[36,939,941],{"class":38,"line":940},23,[36,942,79],{"emptyLinePlaceholder":78},[36,944,946],{"class":38,"line":945},24,[36,947,948],{},"\u002F\u002F For deep trees, load all and build hierarchy in memory:\n",[36,950,952],{"class":38,"line":951},25,[36,953,954],{},"var all = await _db.Categories.AsNoTracking().ToListAsync();\n",[36,956,958],{"class":38,"line":957},26,[36,959,960],{},"var roots = all.Where(c => c.ParentId == null).ToList();\n",[10,962,964],{"id":963},"inheritance-mapping-strategies","Inheritance mapping strategies",[15,966,967],{},"EF Core supports three strategies for mapping class hierarchies:",[969,970,972],"h3",{"id":971},"table-per-hierarchy-tph-default","Table-Per-Hierarchy (TPH) — default",[15,974,975],{},"One table with a Discriminator column. No joins required:",[26,977,979],{"className":28,"code":978,"language":30,"meta":31,"style":31},"modelBuilder.Entity\u003CPayment>()\n    .HasDiscriminator\u003Cstring>(\"Discriminator\")\n    .HasValue\u003CCardPayment>(\"Card\")\n    .HasValue\u003CBankPayment>(\"Bank\");\n\u002F\u002F Schema: Payments(Id, Amount, Discriminator, Last4, IBAN) — sparse nulls\n",[33,980,981,986,991,996,1001],{"__ignoreMap":31},[36,982,983],{"class":38,"line":39},[36,984,985],{},"modelBuilder.Entity\u003CPayment>()\n",[36,987,988],{"class":38,"line":45},[36,989,990],{},"    .HasDiscriminator\u003Cstring>(\"Discriminator\")\n",[36,992,993],{"class":38,"line":51},[36,994,995],{},"    .HasValue\u003CCardPayment>(\"Card\")\n",[36,997,998],{"class":38,"line":57},[36,999,1000],{},"    .HasValue\u003CBankPayment>(\"Bank\");\n",[36,1002,1003],{"class":38,"line":63},[36,1004,1005],{},"\u002F\u002F Schema: Payments(Id, Amount, Discriminator, Last4, IBAN) — sparse nulls\n",[969,1007,1009],{"id":1008},"table-per-type-tpt-normalised","Table-Per-Type (TPT) — normalised",[15,1011,1012],{},"Each type gets its own table, joined on PK:",[26,1014,1016],{"className":28,"code":1015,"language":30,"meta":31,"style":31},"modelBuilder.Entity\u003CCardPayment>().ToTable(\"CardPayments\");\nmodelBuilder.Entity\u003CBankPayment>().ToTable(\"BankPayments\");\n\u002F\u002F Schema: Payments(Id, Amount) + CardPayments(Id, Last4) + BankPayments(Id, IBAN)\n\u002F\u002F Pro: no null columns. Con: JOIN on every query.\n",[33,1017,1018,1023,1028,1033],{"__ignoreMap":31},[36,1019,1020],{"class":38,"line":39},[36,1021,1022],{},"modelBuilder.Entity\u003CCardPayment>().ToTable(\"CardPayments\");\n",[36,1024,1025],{"class":38,"line":45},[36,1026,1027],{},"modelBuilder.Entity\u003CBankPayment>().ToTable(\"BankPayments\");\n",[36,1029,1030],{"class":38,"line":51},[36,1031,1032],{},"\u002F\u002F Schema: Payments(Id, Amount) + CardPayments(Id, Last4) + BankPayments(Id, IBAN)\n",[36,1034,1035],{"class":38,"line":57},[36,1036,1037],{},"\u002F\u002F Pro: no null columns. Con: JOIN on every query.\n",[969,1039,1041],{"id":1040},"table-per-concrete-type-tpc-ef-core-7","Table-Per-Concrete-Type (TPC) — EF Core 7+",[15,1043,1044],{},"Each concrete type gets a full table with all columns:",[26,1046,1048],{"className":28,"code":1047,"language":30,"meta":31,"style":31},"modelBuilder.Entity\u003CCardPayment>().UseTpcMappingStrategy();\nmodelBuilder.Entity\u003CBankPayment>().UseTpcMappingStrategy();\n\u002F\u002F Pro: no joins. Con: UNION ALL for base-type queries.\n",[33,1049,1050,1055,1060],{"__ignoreMap":31},[36,1051,1052],{"class":38,"line":39},[36,1053,1054],{},"modelBuilder.Entity\u003CCardPayment>().UseTpcMappingStrategy();\n",[36,1056,1057],{"class":38,"line":45},[36,1058,1059],{},"modelBuilder.Entity\u003CBankPayment>().UseTpcMappingStrategy();\n",[36,1061,1062],{"class":38,"line":51},[36,1063,1064],{},"\u002F\u002F Pro: no joins. Con: UNION ALL for base-type queries.\n",[15,1066,1067],{},"Start with TPH (the default). Move to TPT when null columns are a schema concern.",[10,1069,1071],{"id":1070},"shadow-properties-for-auditing","Shadow properties for auditing",[15,1073,1074],{},"Shadow properties exist in the EF model and database but not in the entity class:",[26,1076,1078],{"className":28,"code":1077,"language":30,"meta":31,"style":31},"modelBuilder.Entity\u003COrder>()\n    .Property\u003CDateTime>(\"CreatedAt\").HasDefaultValueSql(\"GETUTCDATE()\");\n\nmodelBuilder.Entity\u003COrder>()\n    .Property\u003Cstring>(\"CreatedBy\").HasMaxLength(256);\n\n\u002F\u002F Write in SaveChangesInterceptor:\nentry.Property(\"CreatedBy\").CurrentValue = \"alice@example.com\";\n\n\u002F\u002F Query:\nvar orders = await _db.Orders\n    .Where(o => EF.Property\u003Cstring>(o, \"CreatedBy\") == \"alice@example.com\")\n    .ToListAsync();\n",[33,1079,1080,1085,1090,1094,1098,1103,1107,1112,1117,1121,1126,1131,1136],{"__ignoreMap":31},[36,1081,1082],{"class":38,"line":39},[36,1083,1084],{},"modelBuilder.Entity\u003COrder>()\n",[36,1086,1087],{"class":38,"line":45},[36,1088,1089],{},"    .Property\u003CDateTime>(\"CreatedAt\").HasDefaultValueSql(\"GETUTCDATE()\");\n",[36,1091,1092],{"class":38,"line":51},[36,1093,79],{"emptyLinePlaceholder":78},[36,1095,1096],{"class":38,"line":57},[36,1097,1084],{},[36,1099,1100],{"class":38,"line":63},[36,1101,1102],{},"    .Property\u003Cstring>(\"CreatedBy\").HasMaxLength(256);\n",[36,1104,1105],{"class":38,"line":69},[36,1106,79],{"emptyLinePlaceholder":78},[36,1108,1109],{"class":38,"line":75},[36,1110,1111],{},"\u002F\u002F Write in SaveChangesInterceptor:\n",[36,1113,1114],{"class":38,"line":82},[36,1115,1116],{},"entry.Property(\"CreatedBy\").CurrentValue = \"alice@example.com\";\n",[36,1118,1119],{"class":38,"line":88},[36,1120,79],{"emptyLinePlaceholder":78},[36,1122,1123],{"class":38,"line":93},[36,1124,1125],{},"\u002F\u002F Query:\n",[36,1127,1128],{"class":38,"line":98},[36,1129,1130],{},"var orders = await _db.Orders\n",[36,1132,1133],{"class":38,"line":104},[36,1134,1135],{},"    .Where(o => EF.Property\u003Cstring>(o, \"CreatedBy\") == \"alice@example.com\")\n",[36,1137,1138],{"class":38,"line":110},[36,1139,937],{},[15,1141,1142],{},"Use shadow properties for cross-cutting metadata (audit timestamps, tenant ID) that shouldn't\nappear in the domain model.",[10,1144,1146],{"id":1145},"optimistic-concurrency-with-rowversion","Optimistic concurrency with RowVersion",[15,1148,1149,1150,1153,1154,1157,1158,1161],{},"EF Core adds the concurrency token to ",[33,1151,1152],{},"WHERE"," clauses on ",[33,1155,1156],{},"UPDATE",". If zero rows are affected,\nanother writer modified the row — ",[33,1159,1160],{},"DbUpdateConcurrencyException"," is thrown:",[26,1163,1165],{"className":28,"code":1164,"language":30,"meta":31,"style":31},"public class Product\n{\n    public int Id { get; set; }\n    public int Stock { get; set; }\n\n    [Timestamp]                    \u002F\u002F SQL Server rowversion — auto-incremented by DB\n    public byte[] RowVersion { get; set; } = Array.Empty\u003Cbyte>();\n}\n\u002F\u002F Generated: UPDATE Products SET Stock=@s WHERE Id=@id AND RowVersion=@v\n\u002F\u002F 0 rows updated → DbUpdateConcurrencyException\n\ntry\n{\n    await _db.SaveChangesAsync();\n}\ncatch (DbUpdateConcurrencyException ex)\n{\n    var entry    = ex.Entries.Single();\n    var dbValues = await entry.GetDatabaseValuesAsync();\n    entry.OriginalValues.SetValues(dbValues!); \u002F\u002F refresh original values\n    await _db.SaveChangesAsync();              \u002F\u002F retry\n}\n",[33,1166,1167,1172,1176,1180,1185,1189,1194,1199,1203,1208,1213,1217,1222,1226,1231,1235,1240,1244,1249,1254,1259,1264],{"__ignoreMap":31},[36,1168,1169],{"class":38,"line":39},[36,1170,1171],{},"public class Product\n",[36,1173,1174],{"class":38,"line":45},[36,1175,48],{},[36,1177,1178],{"class":38,"line":51},[36,1179,54],{},[36,1181,1182],{"class":38,"line":57},[36,1183,1184],{},"    public int Stock { get; set; }\n",[36,1186,1187],{"class":38,"line":63},[36,1188,79],{"emptyLinePlaceholder":78},[36,1190,1191],{"class":38,"line":69},[36,1192,1193],{},"    [Timestamp]                    \u002F\u002F SQL Server rowversion — auto-incremented by DB\n",[36,1195,1196],{"class":38,"line":75},[36,1197,1198],{},"    public byte[] RowVersion { get; set; } = Array.Empty\u003Cbyte>();\n",[36,1200,1201],{"class":38,"line":82},[36,1202,72],{},[36,1204,1205],{"class":38,"line":88},[36,1206,1207],{},"\u002F\u002F Generated: UPDATE Products SET Stock=@s WHERE Id=@id AND RowVersion=@v\n",[36,1209,1210],{"class":38,"line":93},[36,1211,1212],{},"\u002F\u002F 0 rows updated → DbUpdateConcurrencyException\n",[36,1214,1215],{"class":38,"line":98},[36,1216,79],{"emptyLinePlaceholder":78},[36,1218,1219],{"class":38,"line":104},[36,1220,1221],{},"try\n",[36,1223,1224],{"class":38,"line":110},[36,1225,48],{},[36,1227,1228],{"class":38,"line":116},[36,1229,1230],{},"    await _db.SaveChangesAsync();\n",[36,1232,1233],{"class":38,"line":342},[36,1234,72],{},[36,1236,1237],{"class":38,"line":347},[36,1238,1239],{},"catch (DbUpdateConcurrencyException ex)\n",[36,1241,1242],{"class":38,"line":352},[36,1243,48],{},[36,1245,1246],{"class":38,"line":910},[36,1247,1248],{},"    var entry    = ex.Entries.Single();\n",[36,1250,1251],{"class":38,"line":916},[36,1252,1253],{},"    var dbValues = await entry.GetDatabaseValuesAsync();\n",[36,1255,1256],{"class":38,"line":922},[36,1257,1258],{},"    entry.OriginalValues.SetValues(dbValues!); \u002F\u002F refresh original values\n",[36,1260,1261],{"class":38,"line":928},[36,1262,1263],{},"    await _db.SaveChangesAsync();              \u002F\u002F retry\n",[36,1265,1266],{"class":38,"line":934},[36,1267,72],{},[15,1269,722,1270,1273],{},[33,1271,1272],{},"[Timestamp]"," on SQL Server for automatic, zero-application-code concurrency detection.",[10,1275,1277],{"id":1276},"alternate-keys","Alternate keys",[15,1279,1280],{},"Alternate keys are unique columns referenced by foreign keys from other tables:",[26,1282,1284],{"className":28,"code":1283,"language":30,"meta":31,"style":31},"modelBuilder.Entity\u003CProduct>().HasAlternateKey(p => p.Sku);\n\nmodelBuilder.Entity\u003COrderItem>()\n    .HasOne(i => i.Product)\n    .WithMany()\n    .HasForeignKey(i => i.ProductSku)\n    .HasPrincipalKey(p => p.Sku); \u002F\u002F FK targets alternate key, not PK\n",[33,1285,1286,1291,1295,1300,1305,1310,1315],{"__ignoreMap":31},[36,1287,1288],{"class":38,"line":39},[36,1289,1290],{},"modelBuilder.Entity\u003CProduct>().HasAlternateKey(p => p.Sku);\n",[36,1292,1293],{"class":38,"line":45},[36,1294,79],{"emptyLinePlaceholder":78},[36,1296,1297],{"class":38,"line":51},[36,1298,1299],{},"modelBuilder.Entity\u003COrderItem>()\n",[36,1301,1302],{"class":38,"line":57},[36,1303,1304],{},"    .HasOne(i => i.Product)\n",[36,1306,1307],{"class":38,"line":63},[36,1308,1309],{},"    .WithMany()\n",[36,1311,1312],{"class":38,"line":69},[36,1313,1314],{},"    .HasForeignKey(i => i.ProductSku)\n",[36,1316,1317],{"class":38,"line":75},[36,1318,1319],{},"    .HasPrincipalKey(p => p.Sku); \u002F\u002F FK targets alternate key, not PK\n",[15,1321,1322,1323,1326],{},"Use alternate keys when external systems reference your entity by a natural key.\nUse ",[33,1324,1325],{},"HasIndex(...).IsUnique()"," for uniqueness constraints that don't need to be FK targets.",[10,1328,1330],{"id":1329},"recap","Recap",[15,1332,1333],{},"EF Core relationship configuration rules for interviews:",[1335,1336,1337,1341,1344,1347,1350,1358,1365,1368],"ol",{},[1338,1339,1340],"li",{},"Declare both FK property and navigation on the dependent entity.",[1338,1342,1343],{},"Use explicit Fluent API — don't rely on conventions for anything non-trivial.",[1338,1345,1346],{},"Use implicit many-to-many (EF 5+) when no payload; explicit join entity when extra columns needed.",[1338,1348,1349],{},"Always add a unique index for one-to-one FK columns.",[1338,1351,722,1352,1354,1355,1357],{},[33,1353,608],{}," for business-critical relationships; ",[33,1356,598],{}," for true parent-child.",[1338,1359,722,1360,812,1362,1364],{},[33,1361,811],{},[33,1363,815],{}," for DDD value objects.",[1338,1366,1367],{},"Start with TPH inheritance; switch to TPT when null columns are a concern.",[1338,1369,1370,1371,1373],{},"Add ",[33,1372,1272],{}," for automatic optimistic concurrency on SQL Server.",[1375,1376,1377],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":31,"searchDepth":45,"depth":45,"links":1379},[1380,1381,1382,1383,1384,1385,1386,1387,1388,1393,1394,1395,1396],{"id":12,"depth":45,"text":13},{"id":20,"depth":45,"text":21},{"id":200,"depth":45,"text":201},{"id":361,"depth":45,"text":362},{"id":455,"depth":45,"text":456},{"id":573,"depth":45,"text":574},{"id":731,"depth":45,"text":732},{"id":822,"depth":45,"text":823},{"id":963,"depth":45,"text":964,"children":1389},[1390,1391,1392],{"id":971,"depth":51,"text":972},{"id":1008,"depth":51,"text":1009},{"id":1040,"depth":51,"text":1041},{"id":1070,"depth":45,"text":1071},{"id":1145,"depth":45,"text":1146},{"id":1276,"depth":45,"text":1277},{"id":1329,"depth":45,"text":1330},"How EF Core models relationships between entities — fluent configuration vs data annotations, cascade delete defaults, owned types, inheritance strategies, and optimistic concurrency with row versioning.","medium","md",".NET Core","dotnet",{},"\u002Fblog\u002Fdotnet-ef-relationships","\u002Fdotnet\u002Fentity-framework\u002Frelationships",{"title":5,"description":1397},"blog\u002Fdotnet-ef-relationships","Relationships","Entity Framework Core","entity-framework","2026-06-23","slttFJXxBL7p1knmh7_b-Hh341IioVDXNI06Kpu_Rag",1782244087124]