Chapter 05 · Relationships, Navigations, Cascade Behavior, and Many-to-Many Modeling
Cascade Delete, ClientCascade, Restrict/NoAction, Orphans, and Database Constraint Behavior
Separate EF client cascade/orphan behavior from database referential actions and design delete semantics from ownership, retention, and loaded-graph evidence.
Learning outcomes
In ServiceHub, deleting a technician, work order, note, SLA
snapshot, or tag can trigger very different outcomes depending
on requiredness and DeleteBehavior. A cascade can
be correct ownership semantics—or catastrophic accidental data
loss. This lesson separates EF-side state transitions from
database ON DELETE actions and makes the
timing/loaded-graph assumptions observable.
Explain Cascade, ClientCascade, ClientSetNull, SetNull, Restrict, NoAction, and ClientNoAction at a mechanism level.
Connect required/optional relationship defaults to cascade behavior without treating defaults as business rules.
Distinguish database cascades from EF client-side cascades and identify when loaded/tracked dependents are required.
Observe conceptual nulls and orphan deletion when required relationships are severed.
Control cascade/orphan timing for diagnostics and tests.
Repair an unsafe cascade design with explicit ownership and retention rules.
1. Start with lifecycle ownership, not an enum
Ask what deleting a principal means. If a work order owns a temporary SLA snapshot that has no meaning independently, cascading that dependent may be sensible. Historical technician assignment records or audit notes may need to survive a principal lifecycle transition. The correct delete behavior follows retention and ownership rules.
| Scenario | Likely design question |
|---|---|
| Delete technician | Should historical work orders survive and become unassigned? Usually yes. |
| Delete work order | Should transient child rows disappear? Maybe, depending on retention. |
| Delete tag | Should join rows disappear while work orders remain? Usually yes. |
| Delete audit/history row | Should application ever allow this? Often restricted/privileged. |
2. Required relationships default to cascade; optional relationships do not
EF conventions configure required relationships for cascading deletes and optional relationships for non-cascading nulling behavior. Those defaults make relational sense, but they are not domain analysis.
b.HasOne(x => x.WorkOrder) .WithMany(x => x.Notes) .HasForeignKey(x => x.WorkOrderId) .IsRequired() .OnDelete(DeleteBehavior.Restrict);
This says a work order cannot be deleted while notes reference it. The application must explicitly archive/delete/migrate notes first. That can be safer for retained records.
3. Database Cascade versus ClientCascade
| Behavior | Tracked EF action | Database FK action |
|---|---|---|
Cascade |
Marks tracked dependents deleted when appropriate. | Creates cascading FK action where provider supports it. |
ClientCascade |
Cascades for tracked dependents. | Creates non-cascading FK; database will not delete unloaded dependents. |
ClientSetNull |
Nulls tracked optional FKs. | Non-cascading FK. |
SetNull |
Nulling semantics plus database
ON DELETE SET NULL where supported.
|
Database nulls FK. |
Restrict/NoAction |
No database cascade; exact client fixup semantics differ by enum. | Database rejects invalid delete if dependent rows remain. |
ClientNoAction |
EF does not automatically delete/null tracked dependents. | Non-cascading FK. |
Provider DDL determines what the database can actually do. Always inspect the migration and live constraint.
4. ClientCascade has a loaded-graph requirement
modelBuilder.Entity<WorkOrder>() .HasMany(x => x.TransientChecks) .WithOne(x => x.WorkOrder) .HasForeignKey(x => x.WorkOrderId) .OnDelete(DeleteBehavior.ClientCascade);
If you load the work order with all transient checks,
EF can mark the tracked dependents deleted before deleting the
principal. If you load only the principal, the database FK is
non-cascading and the delete can fail because unseen dependent
rows still exist. This is why ClientCascade is not
equivalent to database cascade.
await using var db = factory.CreateDbContext();var order = await db.WorkOrders .SingleAsync(x => x.Id == id, ct); // dependents not loadeddb.Remove(order);await db.SaveChangesAsync(ct); // FK violation if dependents exist
Repair by choosing the correct lifecycle mechanism: load and explicitly process dependents if client cascade is intentional, or use a reviewed database cascade when ownership requires store-side deletion, or restrict the delete and implement an archival workflow.
5. Optional assignment: SetNull preserves the work order
b.HasOne(x => x.AssignedTechnician) .WithMany(x => x.WorkOrders) .HasForeignKey(x => x.AssignedTechnicianId) .OnDelete(DeleteBehavior.SetNull);
On technician deletion, the database can set
assigned_technician_id to null if the
provider/schema supports the action. This preserves the work
order. If operational history requires “who used to own this?”,
a nullable current-assignment FK alone is insufficient; model
assignment history separately.
6. Severing a required relationship creates an orphan problem
If a required dependent is removed from a principal collection, EF cannot set its non-nullable FK to a valid null database value. Internally, the tracker can represent a conceptual null while deciding whether the dependent should be deleted or re-parented.
var order = await db.WorkOrders .Include(x => x.Notes) .SingleAsync(x => x.Id == id, ct);var note = order.Notes.First();order.Notes.Remove(note);Console.WriteLine(db.ChangeTracker.DebugView.LongView);
Depending on delete behavior/timing, the note may be marked deleted. Do not infer database values from CLR non-nullability alone; DebugView reveals EF's conceptual state before SQL is emitted.
7. Control cascade and orphan timing for diagnostics
db.ChangeTracker.CascadeDeleteTiming = CascadeTiming.OnSaveChanges;db.ChangeTracker.DeleteOrphansTiming = CascadeTiming.OnSaveChanges;// Change relationships here.Console.WriteLine(db.ChangeTracker.DebugView.LongView);await db.SaveChangesAsync(ct);
Changing timing can make tests/diagnostics easier and can
support re-parenting workflows, but it is not a substitute for
correct relationship ownership.
CascadeTiming.Never can cause exceptions if
required orphans remain unresolved at save time.
8. Deliberately unsafe cascade: delete historical notes with a work order
// Notes are legally/audit-retained, but this mapping treats them as disposable.b.HasOne(x => x.WorkOrder) .WithMany(x => x.Notes) .HasForeignKey(x => x.WorkOrderId) .OnDelete(DeleteBehavior.Cascade);
Deleting a work order could erase retained notes in the database even if the application never loaded them. That is the power—and danger—of database cascade.
If notes must be retained, block hard deletion with Restrict/NoAction, use soft-delete/archive workflow for the principal, or move retained evidence into an independently retained model. Test deletion against realistic data and permissions before deployment.
9. Cascade cycles and provider differences
Complex models can create cycles or multiple cascade paths. SQL
Server, for example, can reject some schemas with multiple
cascade paths even when another engine accepts them. The repair
may involve ClientCascade, restricting one path, or
redesigning ownership. Do not “fix the migration” by randomly
changing enums until it applies; document which layer owns each
delete.
SQLite supports foreign-key actions, but foreign-key enforcement
is a database connection/runtime feature and the actual schema
is what matters. Use PRAGMA foreign_key_list and
destructive tests only against a disposable database.
10. Hands-on lab: prove delete semantics from three angles
-
Keep technician assignment as optional
SetNull. -
Configure work-order notes as
Restrictfor the retention exercise. -
Configure a disposable
WorkOrderTransientCheckrelationship once withClientCascade. - Generate/review the migration and classify each database FK action.
- Apply to disposable SQLite.
- Delete a technician and verify the work order survives with null assignment.
- Attempt to delete a work order that still has restricted notes; capture the FK failure and verify no partial deletion occurred.
-
Load a work order with transient checks and prove
ClientCascadedeletes tracked dependents. - Reset, then delete the same principal without loading dependents and observe the non-cascading database FK failure.
- Remove a required dependent from a loaded collection and inspect DebugView before/after cascade/orphan processing.
Verification checklist
- Every relationship has a documented retention/ownership reason for its delete behavior.
- Database and EF-side cascade behavior are not conflated.
-
ClientCascadetests cover both loaded and unloaded dependents. - Destructive tests use only disposable seeded databases.
- Generated migration DDL is reviewed before application.
Check your understanding
- What is the default delete behavior direction for a required relationship?
- Why can ClientCascade fail when dependents are not loaded?
- What does SetNull require of the FK column?
- What is a conceptual null?
- Why can a database cascade be more dangerous than a client cascade?
- What should you do when SQL Server reports multiple cascade paths?
Review the answers
Required relationships default to cascade by convention unless configured otherwise.
The database constraint is non-cascading, so EF cannot delete dependents it is not tracking and the database can reject the principal delete.
The dependent FK must be nullable and the provider/database must support the configured action.
Tracker state representing a severed required relationship before EF resolves it, even though the CLR FK may be non-nullable.
The database can delete unloaded rows directly, so a single principal delete can remove more data than the application graph shows.
Identify the ownership paths and deliberately move/restrict one cascade path or redesign the model; do not randomly change behavior just to make DDL apply.
11. Production judgment and bridge
Hard-delete cascades are schema-level data-loss policies. Treat them like migration and retention decisions, not convenience settings. Prefer explicit archival/retention designs for historical business records, and test both EF-tracked and direct/database-side behaviors because production data may be deleted by scripts, jobs, or other applications.
Lesson 5 completes the chapter by examining relationship fixup and disconnected graphs, where the database may be correct but the object graph can still be inconsistent, duplicated, or dangerously over-updated.
Authoritative references
- Cascade Delete - EF Core — cascade/delete-orphan behavior, cycles, client versus database cascades
- DeleteBehavior enum - EF Core 10 — precise EF Core 10 delete-behavior semantics
- Changing Foreign Keys and Navigations — conceptual nulls, orphan deletion, and timing
- Saving Related Data - EF Core — required/optional delete side effects
- SQLite Foreign Key Support — SQLite FK actions/enforcement evidence