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.

Intermediate90–115 minutesone-to-one uniqueness + ambiguity labEF Core 10.0.11 · .NET 10.0.11SQLite provider 10.0.11 baselineSDK checkpoint: 10.0.400Last reviewed: August 2026

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.

01

Explain one-to-one and one-to-zero-or-one relationships in terms of unique foreign keys.

02

Choose principal and dependent sides explicitly when conventions cannot infer them.

03

Configure optional and required dependent relationships with HasOne/WithOne/HasForeignKey.

04

Model a shared-primary-key one-to-one and explain its identity/lifecycle implications.

05

Observe the model-validation failure produced by an ambiguous one-to-one.

06

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.

Mental model

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

csharp · one-to-one domain types
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.

csharp · explicit one-to-one mapping
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

csharp · one-to-one metadata probe
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.

sql · SQLite schema evidence
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.

csharp · ambiguous one-to-one
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.

csharp · repair the ambiguity explicitly
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:

csharp · nullable dependent FK
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.

csharp · shared primary key dependent
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

sql · database shape that is actually one-to-many
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

  1. Add WorkOrderSlaSnapshot and the nullable principal navigation to WorkOrder.
  2. Configure the dependent explicitly with HasForeignKey<WorkOrderSlaSnapshot>.
  3. Generate/review a migration and locate both the FK and unique index/constraint.
  4. Apply to the disposable SQLite lab.
  5. Insert one snapshot for a known work order and save.
  6. Attempt to insert a second snapshot for the same work order in a clean context and capture the database uniqueness failure.
  7. Run the metadata probe and confirm fk.IsUnique.
  8. Create the ambiguous warranty types in a temporary branch/test project, observe model validation failure, then repair with explicit dependent configuration.
  9. Compare the normal separate-key model with the shared-primary-key WorkOrderPrivateDetail model.

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

  1. What relational feature turns a foreign key into a one-to-one relationship?
  2. Which side normally contains the FK?
  3. Does a required one-to-one guarantee every principal has a dependent?
  4. What API makes dependent selection explicit?
  5. Why can shared-primary-key mapping be useful?
  6. 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

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