Chapter 05 · Relationships, Navigations, Cascade Behavior, and Many-to-Many Modeling

One-to-Many Relationships: Principal/Dependent Roles, Requiredness, and Foreign Keys

Model one-to-many relationships as synchronized foreign-key and navigation semantics, then prove requiredness, fixup, and SQLite constraints with ServiceHub technician assignments.

Intermediate95–120 minutesone-to-many mapping + relationship-fixup labEF Core 10.0.11 · .NET 10.0.11SQLite provider 10.0.11 baselineSDK checkpoint: 10.0.400Last reviewed: August 2026

Learning outcomes

ServiceHub now has stable key, index, generated-value, and concurrency semantics. The next step is to connect work orders to the people and records around them without treating object references as if they automatically guaranteed relational integrity. In EF Core a relationship is ultimately defined by a foreign key; navigation properties are the object-oriented view layered over that key. This lesson builds that mental model with a real assignment relationship and makes every change visible in the model, tracker, generated SQL, and SQLite constraints.

01

Distinguish principal and dependent entities, principal keys, foreign keys, reference navigations, and collection navigations.

02

Configure optional and required one-to-many relationships by convention and Fluent API.

03

Connect nullable reference types and nullable foreign-key properties to relationship requiredness without confusing C# annotations with database constraints.

04

Compare explicit and shadow foreign keys, including their consequences for disconnected workflows.

05

Observe relationship fixup when an FK or navigation changes while entities are tracked.

06

Verify the resulting SQLite foreign-key constraints and update behavior rather than relying only on CLR object state.

1. Relationships have two synchronized representations

Consider a dispatch screen: one Technician can be assigned to many work orders, while each work order has zero or one current technician. In the database the relationship is represented by a foreign-key value on work_orders. In the object graph the same relationship can be represented by WorkOrder.AssignedTechnician and Technician.WorkOrders.

EF Core tracks both representations. When both related entities are tracked, changing the navigation can update the FK, and changing the FK can cause navigation fixup. That synchronization is useful, but it does not abolish relational semantics: the database constraint, nullability, indexes, delete behavior, and transaction boundaries still decide what is valid when SQL reaches the store.

Term ServiceHub meaning
Principal entity Technician; its key is referenced by work orders.
Dependent entity WorkOrder; it stores the FK.
Principal key Normally Technician.Id.
Foreign key WorkOrder.AssignedTechnicianId.
Reference navigation WorkOrder.AssignedTechnician.
Collection navigation Technician.WorkOrders.

2. Add an optional technician assignment

A new work order may be unassigned, so the FK is nullable and the navigation is nullable. The collection is initialized because an empty collection is a valid state; a reference navigation should not be initialized to a made-up entity merely to silence nullable-reference warnings.

csharp · Technician and WorkOrder navigation members
public sealed class Technician{    private readonly List<WorkOrder> _workOrders = new();    private Technician() { }    public Technician(string employeeCode, string displayName)    {        EmployeeCode = employeeCode.Trim();        DisplayName = displayName.Trim();    }    public int Id { get; private set; }    public string EmployeeCode { get; private set; } = string.Empty;    public string DisplayName { get; private set; } = string.Empty;    public IReadOnlyCollection<WorkOrder> WorkOrders => _workOrders;}public sealed partial class WorkOrder{    public int? AssignedTechnicianId { get; private set; }    public Technician? AssignedTechnician { get; private set; }    public void AssignTo(Technician? technician)    {        AssignedTechnician = technician;        AssignedTechnicianId = technician?.Id;    }}

The example uses private setters and an encapsulated collection, continuing Chapter 03's domain-friendly persistence style. The relationship can still be mapped explicitly even if EF's conventions could discover it.

csharp · explicit optional one-to-many mapping
modelBuilder.Entity<Technician>(b =>{    b.ToTable("technicians");    b.HasKey(x => x.Id);    b.Property(x => x.EmployeeCode)        .HasColumnName("employee_code")        .HasMaxLength(30);    b.HasIndex(x => x.EmployeeCode).IsUnique();    b.HasMany(x => x.WorkOrders)        .WithOne(x => x.AssignedTechnician)        .HasForeignKey(x => x.AssignedTechnicianId)        .IsRequired(false)        .OnDelete(DeleteBehavior.SetNull);});

HasMany/WithOne describe cardinality and navigations; HasForeignKey identifies the dependent property. IsRequired(false) is consistent with the nullable FK. SetNull is an explicit lifecycle decision for this example: deleting a technician should not delete historical work orders.

3. Nullable reference types help the compiler, but the FK determines database requiredness

C# nullable reference types (NRTs) express whether a reference may be null in C# code. EF conventions can use NRT annotations as model hints, but the relational constraint is driven by the resulting EF metadata and migration. An optional FK such as int? naturally maps to a nullable column. A required FK such as int maps to a non-nullable relationship by convention.

Do not confuse layers

A non-null C# navigation at runtime does not prove the database row is valid, and a nullable C# annotation does not by itself alter an already-deployed schema. Review the EF model and migration DDL.

4. Contrast with a required child record

Chapter 04 introduced work-order notes as a key-related example. Here, make the relationship explicit and required: every WorkOrderNote must reference a work order, while a work order may have any number of notes.

csharp · required WorkOrderNote relationship
public sealed class WorkOrderNote{    public long Id { get; private set; }    public int WorkOrderId { get; private set; }    public WorkOrder WorkOrder { get; private set; } = null!;    public string Text { get; private set; } = string.Empty;}modelBuilder.Entity<WorkOrderNote>(b =>{    b.ToTable("work_order_notes");    b.HasKey(x => x.Id);    b.HasOne(x => x.WorkOrder)        .WithMany()        .HasForeignKey(x => x.WorkOrderId)        .IsRequired();});

Because WorkOrderId is non-nullable, this is required. By convention, required relationships use cascade delete unless overridden. Lesson 4 will treat that default as a lifecycle choice to review, not an instruction to accept automatically.

5. Unidirectional relationships are still relationships

You do not need navigations on both sides. If the application never asks a technician for its work-order collection, the principal-side collection can be omitted:

csharp · unidirectional relationship
modelBuilder.Entity<WorkOrder>()    .HasOne(x => x.AssignedTechnician)    .WithMany()    .HasForeignKey(x => x.AssignedTechnicianId);

The relational relationship remains real because the FK is real. Navigations are application conveniences, not the source of database referential integrity.

6. Explicit versus shadow foreign keys

If a dependent has a navigation but no matching CLR FK property, EF can create a shadow FK. That can reduce domain noise, but the key value then lives in ChangeTracker state rather than in the disconnected object.

csharp · shadow FK variant
public sealed class WorkOrderAttachment{    public long Id { get; private set; }    public WorkOrder WorkOrder { get; private set; } = null!;    public string FileName { get; private set; } = string.Empty;}modelBuilder.Entity<WorkOrderAttachment>(b =>{    b.HasOne(x => x.WorkOrder)        .WithMany()        .HasForeignKey("WorkOrderId")        .IsRequired();});

For server-rendered or disconnected APIs, an explicit FK is often easier to validate, serialize, compare, and authorize. Shadow FKs are appropriate when the identifier is genuinely persistence infrastructure. The choice is architectural, not aesthetic.

7. Make relationship metadata observable

csharp · inspect foreign-key metadata
var workOrderType = db.Model.FindEntityType(typeof(WorkOrder))!;foreach (var fk in workOrderType.GetForeignKeys()){    Console.WriteLine($"Principal: {fk.PrincipalEntityType.DisplayName()}");    Console.WriteLine($"Dependent: {fk.DeclaringEntityType.DisplayName()}");    Console.WriteLine($"FK: {string.Join(", ", fk.Properties.Select(p => p.Name))}");    Console.WriteLine($"Required: {fk.IsRequired}");    Console.WriteLine($"Delete: {fk.DeleteBehavior}");}

This proves the runtime model's relationship contract. It does not prove the migration was applied to the target database.

sql · inspect SQLite foreign keys
PRAGMA foreign_key_list('work_orders');PRAGMA foreign_key_list('work_order_notes');

Database metadata is the second half of the evidence. A migration file, an EF runtime model, and a live database can drift if deployment is incomplete.

8. Relationship fixup: change the navigation and watch the FK

csharp · tracked navigation change
var order = await db.WorkOrders    .Include(x => x.AssignedTechnician)    .SingleAsync(x => x.WorkOrderNumber == "WO-2026-000201", ct);var technician = await db.Technicians    .SingleAsync(x => x.EmployeeCode == "TECH-014", ct);Console.WriteLine($"Before: FK={order.AssignedTechnicianId}");order.AssignTo(technician);Console.WriteLine($"After:  FK={order.AssignedTechnicianId}");Console.WriteLine(db.ChangeTracker.DebugView.LongView);await db.SaveChangesAsync(ct);

Because both entities are tracked, EF can keep FK and navigation state consistent. If the domain method writes both members, the consistency is also explicit in CLR code. When EF performs fixup itself, inspect DebugView before save so you understand which property became modified.

9. Deliberately wrong approach: configure contradictory requiredness

csharp · contradictory model intent
public int? AssignedTechnicianId { get; private set; }// This says the relationship is required even though the FK is nullable.b.HasOne(x => x.AssignedTechnician) .WithMany(x => x.WorkOrders) .HasForeignKey(x => x.AssignedTechnicianId) .IsRequired();

Depending on the exact configuration/model, EF can produce a non-null relationship contract that conflicts with the domain's “unassigned” state. Even if the model builds, the generated schema and inserts can reject null assignment. The repair is not “make EF happy”; the repair is to decide the business rule, align CLR nullability, EF requiredness, and database nullability, then regenerate/review the migration.

10. Hands-on lab: assignment as a tracked relationship

  1. Add Technician and the optional assignment members to WorkOrder.
  2. Add DbSet<Technician> and explicit mapping.
  3. Keep the Chapter 04 WorkOrder mapping unchanged, including table work_orders, primary-key column work_order_id, indexes, backing-field mapping, and concurrency metadata.
  4. Create and review a migration that adds technicians plus assigned_technician_id.
  5. Apply it only to the disposable SQLite lab database.
  6. Seed two technicians deterministically.
  7. Load a work order and technician in one context, assign through the navigation/domain method, and print DebugView before SaveChangesAsync.
  8. Verify generated SQL updates the FK, then inspect PRAGMA foreign_key_list('work_orders').
  9. Clear the assignment and verify a nullable FK results instead of deleting the order.
  10. Create the required WorkOrderNote mapping and inspect its different requiredness/delete metadata.

Verification checklist

  • The work-order FK and navigation agree after fixup.
  • Deleting or unassigning a technician does not implicitly delete a work order in this design.
  • The required note FK cannot be null.
  • SQLite foreign-key metadata matches the reviewed migration.
  • All relationship changes happen in a short-lived context and are awaited before reuse.

Check your understanding

  1. Which entity is the dependent in Technician → WorkOrder assignment?
  2. Why is AssignedTechnicianId nullable?
  3. What is the difference between a navigation and a foreign key?
  4. Where does a shadow foreign-key value live?
  5. Does a principal-side collection navigation make the database constraint exist?
  6. What evidence proves the live SQLite database has the FK?
Review the answers

WorkOrder, because it stores the foreign key.

A work order may be unassigned, so the relationship is optional.

The FK is the relational key value defining the relationship; a navigation is the CLR object view over that relationship.

In ChangeTracker state, not in a CLR property/field.

No. The database constraint comes from schema/migration DDL, not from the existence of a CLR collection.

Inspect the live database, for example with PRAGMA foreign_key_list on the mapped table.

11. Production judgment and bridge

Model relationships from lifecycle and integrity rules first. Prefer explicit FKs where identifiers are important in API commands, authorization, logging, or disconnected updates. Keep optionality aligned across C#, EF metadata, and database nullability. Do not accept cascade defaults until you have decided what deleting a principal should mean for every dependent.

Lesson 2 narrows the cardinality from one-to-many to one-to-one, where EF must know which side is dependent and relational uniqueness becomes part of the mapping contract.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.

\n