Chapter 05 · Relationships, Navigations, Cascade Behavior, and Many-to-Many Modeling
One-to-One Relationships: Uniqueness, Shared Keys, and Ambiguous Relationship Resolution
Build one-to-one relationships from unique foreign keys and explicit dependent selection, including optional dependents, shared keys, and ambiguous-model failure analysis.
Learning outcomes
ServiceHub wants one optional SLA snapshot per work order. The phrase “one-to-one” sounds simple in objects—two reference properties—but a relational database needs something concrete: one row stores a foreign key and that FK must be unique if at most one dependent may point to each principal. EF must also know which type is dependent. This lesson makes those hidden decisions explicit.
Explain one-to-one and one-to-zero-or-one relationships in terms of unique foreign keys.
Choose principal and dependent sides explicitly when conventions cannot infer them.
Configure optional and required dependent relationships with HasOne/WithOne/HasForeignKey.
Model a shared-primary-key one-to-one and explain its identity/lifecycle implications.
Observe the model-validation failure produced by an ambiguous one-to-one.
Verify uniqueness and FK constraints in SQLite DDL/catalog evidence.
1. “Required” does not mean every principal has a dependent
In EF relationship terminology, a required one-to-one means that every dependent row must reference a principal. It does not generally enforce that every principal row has a dependent. Standard relational foreign keys point from dependent to principal; they do not express “every principal must have exactly one child row.” That stronger invariant usually needs application workflow, a different schema pattern, or database-specific enforcement.
One-to-one = a foreign key plus uniqueness. The dependent holds the foreign key. Optional versus required describes whether the dependent FK may be null, not whether a principal row must have a dependent row.
2. Add an optional WorkOrderSlaSnapshot dependent
public sealed partial class WorkOrder{ public WorkOrderSlaSnapshot? SlaSnapshot { get; private set; }}public sealed class WorkOrderSlaSnapshot{ private WorkOrderSlaSnapshot() { } public int Id { get; private set; } public int WorkOrderId { get; private set; } public WorkOrder WorkOrder { get; private set; } = null!; public DateTime DueUtc { get; private set; } public string PolicyCode { get; private set; } = string.Empty;}The dependent FK WorkOrderId is non-nullable, so an SLA snapshot cannot exist without a work order. The work order can still exist with no snapshot, so WorkOrder.SlaSnapshot is nullable.
modelBuilder.Entity<WorkOrderSlaSnapshot>(b =>{ b.ToTable("work_order_sla_snapshots"); b.HasKey(x => x.Id); b.HasOne(x => x.WorkOrder) .WithOne(x => x.SlaSnapshot) .HasForeignKey<WorkOrderSlaSnapshot>(x => x.WorkOrderId) .IsRequired();});EF creates a uniqueness requirement for the dependent FK so two SLA snapshot rows cannot reference the same work order.
3. Inspect the metadata: unique FK and dependent selection
var type = db.Model.FindEntityType(typeof(WorkOrderSlaSnapshot))!;var fk = type.GetForeignKeys().Single();Console.WriteLine($"Dependent: {fk.DeclaringEntityType.DisplayName()}");Console.WriteLine($"Principal: {fk.PrincipalEntityType.DisplayName()}");Console.WriteLine($"Required: {fk.IsRequired}");Console.WriteLine($"Unique: {fk.IsUnique}");Console.WriteLine($"FK: {string.Join(", ", fk.Properties.Select(p => p.Name))}");IsUnique is what distinguishes the one-to-one FK metadata from a normal one-to-many FK. The database must also have the corresponding uniqueness enforcement.
SELECT type, name, sqlFROM sqlite_schemaWHERE tbl_name = 'work_order_sla_snapshots' AND type IN ('table', 'index')ORDER BY type, name;4. Deliberately ambiguous model: two references, no dependent clue
If two entity types reference one another and neither side has a discoverable FK, EF may be unable to determine the dependent end.
public sealed class WorkOrderWarranty{ public int Id { get; set; } public WorkOrderWarrantyTerms? Terms { get; set; }}public sealed class WorkOrderWarrantyTerms{ public int Id { get; set; } public WorkOrderWarranty? Warranty { get; set; }}// No FK property and no explicit HasForeignKey<TDependent>().Model validation reports an InvalidOperationException indicating that EF cannot determine the relationship/dependent side (exact wording can vary across versions). The failure is useful: guessing the dependent could produce the wrong FK ownership.
modelBuilder.Entity<WorkOrderWarranty>() .HasOne(x => x.Terms) .WithOne(x => x.Warranty) .HasForeignKey<WorkOrderWarrantyTerms>("WarrantyId");The generic type passed to HasForeignKey<TDependent> states the dependent explicitly.
5. Optional dependent FK: one-to-zero-or-one from the dependent side
If the dependent itself may exist before it is linked, make its FK nullable:
public int? WorkOrderId { get; private set; }public WorkOrder? WorkOrder { get; private set; }b.HasOne(x => x.WorkOrder) .WithOne(x => x.SlaSnapshot) .HasForeignKey<WorkOrderSlaSnapshot>(x => x.WorkOrderId) .IsRequired(false);Whether this intermediate “orphan snapshot” state makes business sense is a domain question. Nullability should model a real lifecycle, not merely avoid a migration error.
6. Shared-primary-key one-to-one
For tightly coupled dependents, the dependent primary key can also be its FK to the principal. This eliminates a separate surrogate key and guarantees at most one dependent row per principal key.
public sealed class WorkOrderPrivateDetail{ public int WorkOrderId { get; private set; } public WorkOrder WorkOrder { get; private set; } = null!; public string InternalRoutingNote { get; private set; } = string.Empty;}modelBuilder.Entity<WorkOrderPrivateDetail>(b =>{ b.ToTable("work_order_private_details"); b.HasKey(x => x.WorkOrderId); b.HasOne(x => x.WorkOrder) .WithOne() .HasForeignKey<WorkOrderPrivateDetail>(x => x.WorkOrderId);});This pattern strongly couples identity to the principal. It is useful when the dependent has no independent identity, but it can make re-parenting impossible or conceptually wrong—which may be exactly the invariant you want.
7. Unique indexes and one-to-one semantics
A one-to-one mapping typically results in a unique index/constraint on the FK. Do not manually add a second redundant unique index without inspecting generated migrations. EF metadata and provider DDL can already create the uniqueness needed by the relationship.
8. Broken design: use a non-unique FK and call it one-to-one
CREATE TABLE work_order_sla_snapshots ( id INTEGER PRIMARY KEY, work_order_id INTEGER NOT NULL, due_utc TEXT NOT NULL, FOREIGN KEY (work_order_id) REFERENCES work_orders(work_order_id));-- No UNIQUE(work_order_id).This schema allows multiple snapshot rows for the same work order. An object model with one SlaSnapshot property cannot make that database state impossible. Repair by enforcing relational uniqueness and keeping EF mapping aligned.
9. Hands-on lab: prove dependent selection and uniqueness
- Add
WorkOrderSlaSnapshotand the nullable principal navigation toWorkOrder. - Configure the dependent explicitly with
HasForeignKey<WorkOrderSlaSnapshot>. - Generate/review a migration and locate both the FK and unique index/constraint.
- Apply to the disposable SQLite lab.
- Insert one snapshot for a known work order and save.
- Attempt to insert a second snapshot for the same work order in a clean context and capture the database uniqueness failure.
- Run the metadata probe and confirm
fk.IsUnique. - Create the ambiguous warranty types in a temporary branch/test project, observe model validation failure, then repair with explicit dependent configuration.
- Compare the normal separate-key model with the shared-primary-key
WorkOrderPrivateDetailmodel.
Verification checklist
- EF and the database agree on the dependent side.
- The live schema prevents two SLA snapshots for one work order.
- The principal can exist without a dependent snapshot.
- No migration is applied to production from a model-validation experiment.
- Shared-primary-key semantics are documented before adopting them.
Check your understanding
- What relational feature turns a foreign key into a one-to-one relationship?
- Which side normally contains the FK?
- Does a required one-to-one guarantee every principal has a dependent?
- What API makes dependent selection explicit?
- Why can shared-primary-key mapping be useful?
- What should you do when EF cannot determine the dependent?
Review the answers
Uniqueness on the foreign-key value(s).
The dependent entity.
No. It guarantees a dependent must reference a principal; it does not force every principal to have a dependent row.
HasForeignKey
It makes the dependent identity the same as the principal identity and inherently allows at most one dependent per principal.
Configure the relationship explicitly rather than trying to satisfy the convention accidentally.
10. Production judgment and bridge
Use one-to-one only when the relationship is truly at-most-one in the business model and the database enforces it. Avoid splitting every group of columns into a separate one-to-one table merely for object neatness; every table boundary affects joins, migrations, locking, and operational diagnostics.
Lesson 3 moves to many-to-many relationships, where an association cannot be represented by a single FK and the join row itself may become important domain data.
Authoritative references
- One-to-one relationships - EF Core — required/optional, shared-key, shadow-FK, alternate-key, and explicit dependent examples
- Relationships glossary - EF Core — principal/dependent/foreign-key terminology
- Indexes - EF Core — relational uniqueness/index background
- Relationship conventions - EF Core — how EF determines cardinality and dependent ends by convention
- SQLite UNIQUE constraint — database-level uniqueness behavior used by the lab