Chapter 12 · Saving Data: Insert, Update, Delete, Batching, ExecuteUpdate, and ExecuteDelete

SaveChanges Pipeline, Command Ordering, Generated Keys, AcceptAllChanges, and Failure Semantics

Trace a ServiceHub SaveChanges call from change detection through dependency ordering, transactions, batching, generated-value propagation, acceptance, and failure-state diagnosis.

Advanced135–170 minutesSaveChanges pipeline + failure labEF Core 10.0.11 · .NET 10.0.11SQLite provider 10.0.11 baselineSDK checkpoint: 10.0.400Last reviewed: August 2026

Learning outcomes

Chapter 11 stopped at the state manager: EF knew which ServiceHub entities were Added, Modified, or Deleted. This lesson follows those entries through the write pipeline until database commands either commit or fail. The practical question is not “does SaveChanges issue SQL?” but which SQL, in what dependency order, under which transaction, with which generated values, and what remains in memory if the call fails?

01

Trace DetectChanges, modification-command creation, dependency ordering, batching, transaction execution, generated-value propagation, and AcceptAllChanges.

02

Explain why principal inserts normally precede dependent inserts and dependent deletes precede principal deletes.

03

Observe generated SQLite keys and the ServiceHub Revision concurrency predicate in command logs.

04

Use SaveChanges(false) only when deliberately separating database success from tracker acceptance.

05

Diagnose DbUpdateException without assuming the ChangeTracker was rolled back to its pre-operation state.

06

Verify database atomicity separately from client-side entity state.

Reproducible baseline

Mandatory labs use .NET SDK 10.0.400, .NET runtime 10.0.11, Microsoft.EntityFrameworkCore/SQLite 10.0.11, dotnet-ef 10.0.11, the disposable servicehub-lab.db, deterministic ServiceHub seed data, and no paid tooling. SQL Server/PostgreSQL native-loader examples are optional provider comparisons. EF Core 11 previews are excluded from the mandatory path.

1. SaveChanges is a pipeline, not one command

For a normal tracking context, SaveChangesAsync first ensures tracked state is current, turns entry/property state into provider modification commands, orders commands so relational dependencies can be satisfied, groups commands into provider batches where possible, executes them, propagates database-generated values, and—unless requested otherwise—accepts the resulting state as the new tracker baseline.

Stage What EF is deciding Evidence to capture
Change detection Which tracked properties/relationships changed. DebugView + IsModified before save.
Modification commands Which INSERT/UPDATE/DELETE operations are required. Database.Command logs.
Dependency ordering Which command must precede another for keys/FKs. Ordered SQL log plus FK constraints.
Transaction/batching Which statements share atomic scope / round trip. Transaction + command logs; provider docs.
Generated values Keys/defaults/computed values returned from store. EntityEntry values before/after save.
AcceptAllChanges Whether Added/Modified/Deleted states become their accepted post-save states. DebugView after save.

2. A related insert shows command dependency ordering

Course continuity: Chapter 05 introduced WorkOrderNote with private setters. Add this small domain factory before the insert lab; it changes object construction only and does not alter the relational schema.

csharp · minimal factory added to WorkOrderNote
public static WorkOrderNote Create(WorkOrder order, string text){    ArgumentNullException.ThrowIfNull(order);    if (string.IsNullOrWhiteSpace(text))        throw new ArgumentException("Note text is required.", nameof(text));    return new WorkOrderNote    {        WorkOrder = order,        Text = text.Trim()    };}
csharp · principal + dependent in one unit of work
var order = WorkOrder.Open(    "WO-2026-001201",    "Inspect compressor vibration after restart",    new ServiceAddress("21 Service Rd", "Baku", "AZ", "AZ1000", "AZ"));// WorkOrderNote already exists in the ServiceHub relationship model.var note = WorkOrderNote.Create(    order,    "Dispatch created the initial inspection note");order.Notes.Add(note);db.Add(order);Console.WriteLine($"Temporary before save: {db.Entry(order).Property(x => x.Id).IsTemporary}");Console.WriteLine(db.ChangeTracker.DebugView.LongView);await db.SaveChangesAsync(ct);Console.WriteLine($"Order key: {order.Id}");Console.WriteLine($"Temporary after save: {db.Entry(order).Property(x => x.Id).IsTemporary}");Console.WriteLine($"Order state: {db.Entry(order).State}");Console.WriteLine($"Note state: {db.Entry(note).State}");

The store-generated work-order key must be known before a dependent row can persist its foreign key. EF's update pipeline uses model relationships to order compatible commands. Do not depend on incidental statement ordering where no relational dependency exists; depend on constraints and transaction semantics instead.

sql · representative SQLite shape
INSERT INTO "work_orders" (...) VALUES (...) RETURNING "work_order_id";INSERT INTO "work_order_notes" ("work_order_id", "text") VALUES (@p_order, @p_text)RETURNING "work_order_note_id";

The exact SQL, parameter names, RETURNING usage, and whether statements share a batch are provider/version details; capture the actual log in the lab.

3. Tracked updates carry mapping and concurrency semantics

csharp · narrow tracked update
var order = await db.WorkOrders.SingleAsync(x => x.Id == id, ct);db.Entry(order).Property(x => x.Revision).OriginalValue = expectedRevision;order.ReviseSummary("Bearing inspection approved for dispatch");order.AdvanceRevision();await db.SaveChangesAsync(ct);
sql · representative concurrency-sensitive UPDATE
UPDATE "work_orders"SET "summary" = @p0, "revision" = @p1WHERE "work_order_id" = @id AND "revision" = @originalRevision;

Chapter 13 will go deeper on optimistic concurrency, but the write-pipeline fact is visible already: mapping metadata determines columns and the concurrency token contributes to the predicate. A zero-row update is not the same failure mechanism as a unique/FK constraint violation.

4. Default SaveChanges transaction behavior is database atomicity

For providers that support transactions, one SaveChanges call is normally wrapped so all changes in that call succeed or the database is rolled back. That guarantee is about the database. It does not mean every CLR object is magically reverted to old property values after an exception.

csharp · induce a disposable constraint failure
var duplicate = WorkOrder.Open(    existingWorkOrderNumber, // unique in the course model    "Intentional duplicate for failure lab",    new ServiceAddress("1 Lab St", "Baku", "AZ", "AZ1000", "AZ"));db.Add(duplicate);try{    await db.SaveChangesAsync(ct);}catch (DbUpdateException ex){    Console.WriteLine(ex.GetType().Name);    Console.WriteLine(db.Entry(duplicate).State); // inspect; do not assume reset    Console.WriteLine(db.ChangeTracker.DebugView.LongView);}

After the provider/database rejects the command, the failed entity commonly remains pending in the tracker. Remove, detach, repair, or abandon the short-lived context based on the error class. Do not catch every DbUpdateException and immediately retry unchanged state.

Failure injection is disposable only

The duplicate-key experiment belongs only in servicehub-lab.db after a deterministic reset. Never use production data to “see what happens.”

5. SaveChanges(false) separates store success from tracker acceptance

csharp · explicit acceptance boundary
order.ReviseSummary("Inspection complete; awaiting customer sign-off");order.AdvanceRevision();await db.SaveChangesAsync(    acceptAllChangesOnSuccess: false,    cancellationToken: ct);Console.WriteLine(db.Entry(order).State); // still ModifiedConsole.WriteLine(db.Entry(order).Property(x => x.Revision).OriginalValue);Console.WriteLine(db.Entry(order).Property(x => x.Revision).CurrentValue);db.ChangeTracker.AcceptAllChanges();Console.WriteLine(db.Entry(order).State); // Unchanged

The database operation may already be committed even though the tracker has not accepted it. That is useful in advanced durability protocols, but dangerous as a default: another SaveChanges before acceptance can try to send the same logical change again.

Not a transaction primitive

SaveChanges(false) is not a rollback switch and does not create a two-phase commit with external systems. Coordinate durability explicitly.

6. Hands-on lab: correlate tracker, commands, and store state

  1. Reset the disposable SQLite database and enable Microsoft.EntityFrameworkCore.Database.Command and transaction logs.
  2. Add one work order and one dependent note; record temporary/permanent keys and command order.
  3. Modify Summary + Revision and compare DebugView with the generated UPDATE predicate.
  4. Delete a note and its parent in a disposable scenario; inspect delete ordering/cascade behavior from Chapter 05.
  5. Trigger a duplicate unique value, capture DbUpdateException, inspect tracker state, then verify the database did not partially apply the failed SaveChanges call.
  6. Repeat a successful modification with SaveChangesAsync(false,...), then call AcceptAllChanges().
  7. Run dotnet ef migrations has-pending-model-changes; this lesson should require no schema change.

Check your understanding

  1. What does SaveChanges normally do before generating writes?
  2. Why can a principal INSERT precede a dependent INSERT?
  3. What does the default transaction protect?
  4. Does a DbUpdateException restore CLR properties?
  5. What does SaveChanges(false) suppress?
  6. What three evidence layers should be correlated?
Review the answers

It ensures tracked changes are detected so entity/property/relationship state is current.

The dependent FK may require the principal's generated key and relational constraints impose a dependency.

Database atomicity for the changes in that SaveChanges call when the provider supports transactions.

No. Inspect/repair or dispose the context; the database rollback and client tracker are separate concerns.

Automatic AcceptAllChanges after successful database execution.

ChangeTracker state, EF/provider command/transaction logs, and actual database rows/constraints.

7. Production judgment and bridge

Use tracked SaveChanges when domain logic, identity generation, relationship ordering, optimistic concurrency, and a unit-of-work boundary matter. Handle known database exceptions deliberately and let unknown failures terminate the short-lived context rather than mutating it blindly. Lesson 2 zooms in on one pipeline stage—batching—and separates statement count, parameter limits, and round trips from marketing-language “bulk” claims.

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