Chapter 14 · Transactions, Savepoints, Execution Strategies, and Cross-Context Coordination
Savepoints and Automatic Rollback Inside Existing Transactions
Use automatic and explicit savepoints, prove partial rollback behavior, and reconcile EF tracked state after the database moves back to a savepoint.
Learning outcomes
A savepoint is a named recovery position inside
an already-open transaction. Rolling back to a savepoint keeps
the outer transaction alive while undoing database commands
after that point. EF Core can create savepoints automatically
around SaveChanges when a transaction is already in
progress, and relational transaction APIs also expose explicit
savepoint operations.
Explain EF automatic savepoints when SaveChanges runs inside an existing transaction.
Use CreateSavepointAsync, RollbackToSavepointAsync, and ReleaseSavepointAsync deliberately.
Observe that database rollback does not rewind CLR object/current-value state automatically.
Reconcile ChangeTracker state after rolling back a previously successful SaveChanges.
Distinguish savepoint support from fake nested transactions and provider limitations.
Explain why SQL Server MARS disables EF automatic savepoint creation.
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, the existing Guid Revision concurrency token, and the Chapter 12 outbox teaching model when referenced. SQL Server retry/MARS/System.Transactions examples are optional provider-specific paths. EF Core 11 previews are excluded.
1. Why a savepoint exists
Suppose one ServiceHub transaction performs phase A successfully, then phase B hits a recoverable problem. Rolling back the entire transaction would discard A; committing after B fails would be unsafe. A savepoint gives the database a checkpoint inside the transaction.
| Operation | Outer transaction | Database changes before savepoint | Changes after savepoint |
|---|---|---|---|
| Create savepoint | remains active | kept | future work begins |
| Rollback to savepoint | remains active | kept | undone |
| Release savepoint | remains active | kept | checkpoint name/resources released |
| Rollback transaction | ends/aborts | undone | undone |
2. EF automatic savepoint before SaveChanges
When SaveChanges executes while an EF transaction
is already active, EF normally creates a savepoint first. If
that save fails, EF rolls the database transaction back to that
savepoint so the outer transaction can potentially continue.
This is especially useful around optimistic concurrency
recovery.
EF documents that savepoints are incompatible with SQL Server Multiple Active Result Sets (MARS). When MARS is enabled, EF does not create automatic savepoints—even if no second result set is active. A failed SaveChanges can therefore leave the transaction in an unknown state; do not enable MARS casually in code that relies on savepoint recovery.
3. Explicit savepoint: prove partial database rollback
await using var tx = await db.Database.BeginTransactionAsync(ct);var order = await db.WorkOrders.SingleAsync(x => x.Id == id, ct);order.ReviseSummary("phase A: diagnostics approved");order.AdvanceRevision();await db.SaveChangesAsync(ct);await tx.CreateSavepointAsync("AfterPhaseA", ct);order.ReviseSummary("phase B: replacement approved");order.AdvanceRevision();await db.SaveChangesAsync(ct);await tx.RollbackToSavepointAsync("AfterPhaseA", ct);
At the database level, phase B is undone and phase A remains
inside the still-open transaction. But the tracked
order object has already passed through a
successful SaveChanges for phase B, so its CLR/current/original
values may describe phase B while the database is back at phase
A.
4. Deliberately wrong: assume rollback rewinds ChangeTracker
await tx.RollbackToSavepointAsync("AfterPhaseA", ct);Console.WriteLine(order.Summary); // may still be the phase-B CLR valueConsole.WriteLine(db.Entry(order).State); // may be Unchanged after prior SaveChangesawait tx.CommitAsync(ct); // database commits phase A, not CLR phase B
A database transaction owns database state; EF’s state manager owns CLR tracking state. Rolling the database back does not perform a time-travel operation on your object graph.
After savepoint rollback, decide what state should continue. Reload affected entries from the database, restore known original/current values, or discard the context and create a new one if the graph is complex. Do not keep writing based on tracker state you have not reconciled.
5. Reconcile and continue
await tx.RollbackToSavepointAsync("AfterPhaseA", ct);await db.Entry(order).ReloadAsync(ct);Console.WriteLine(order.Summary); // phase A value visible inside this transactionorder.ReviseSummary("phase B revised after validation");order.AdvanceRevision();await db.SaveChangesAsync(ct);await tx.ReleaseSavepointAsync("AfterPhaseA", ct);await tx.CommitAsync(ct);
Reload issues a database query using the current transaction/connection and aligns the entity with the rolled-back database state before a new proposal is applied. For multiple related entries, reconcile the entire affected graph or restart with a fresh context rather than selectively reloading one row and assuming relationships are coherent.
6. Savepoint support is provider capability
var adoTransaction = tx.GetDbTransaction();Console.WriteLine(adoTransaction.SupportsSavepoints);
Microsoft.Data.Sqlite supports savepoints. Other providers may
differ or impose restrictions. EF's relational API surface does
not guarantee that every engine/topology implements the same
behavior. Handle NotSupportedException as a
capability mismatch during development/configuration—not as a
reason to silently drop recovery semantics in production.
7. Automatic rollback on a failed save does not decide business recovery
EF can return the database to the pre-savepoint state after a failed SaveChanges, but the application still has to classify the failure. A concurrency conflict may be mergeable; a unique/check constraint violation may require different input; a lock/timeout may be transient; cancellation should normally abort the workflow. Savepoints preserve options—they do not choose policy.
| Failure after savepoint | Database recovery | Tracker/application next step |
|---|---|---|
| DbUpdateConcurrencyException | EF can roll back failed save to automatic savepoint | Resolve/reload/merge and retry within bounded policy. |
| Constraint violation | failed SaveChanges rolled back | Fix invalid proposal or abort; do not generic-retry unchanged input. |
| Provider transient | depends on provider/transaction state | Execution strategy rules apply; entire user transaction may need replay. |
| Cancellation | abort requested work | Rollback/dispose transaction and propagate cancellation. |
8. Hands-on lab
- Reset SQLite and start one explicit transaction.
- Save phase A and create
AfterPhaseA. -
Save phase B, capture
ChangeTracker.DebugView.LongView, then roll back to the savepoint. - Query phase A with a raw scalar/read inside the transaction and compare it to the still-tracked CLR value.
-
Call
ReloadAsync; verify current/original values now match phase A. - Apply a revised phase B, save, commit, then verify from a fresh context.
- Print
SupportsSavepoints. - For the optional SQL Server path, document why enabling MARS changes EF automatic-savepoint behavior.
Check your understanding
- Does rollback to a savepoint end the outer transaction?
- Does RollbackToSavepointAsync rewind CLR properties automatically?
- When does EF normally create an automatic savepoint?
- What SQL Server setting disables EF automatic savepoints?
- Why might discarding the context be safer after a complex rollback?
- Is a savepoint a second independent transaction?
Review the answers
No. It returns database state to the savepoint and keeps the transaction active.
No. Reconcile the ChangeTracker/object graph explicitly.
Before SaveChanges when a transaction is already active on the context, subject to provider limitations.
Multiple Active Result Sets (MARS).
A large graph may contain stale current/original/entity/relationship state that is difficult to reconcile correctly entry by entry.
No. It is a recovery marker inside the same outer transaction.
9. Production judgment and bridge
Savepoints make long multi-step local transactions more recoverable, but they also increase state-management responsibility. Keep transaction work small, reconcile trackers deliberately, and test provider limitations. Lesson 3 adds another replay boundary: retrying execution strategies, where the whole transaction delegate—not merely one failed command—may need to run again.
Authoritative references
- EF Core Transactions - Savepoints — automatic and manual savepoint behavior plus the SQL Server MARS warning.
- Microsoft.Data.Sqlite Transactions - Savepoints — SQLite savepoint support and optimistic offline lock example.
- DbTransaction.SupportsSavepoints — ADO.NET capability signal.
- Change Tracking - EF Core — state manager semantics that remain separate from database rollback.
- Handling Concurrency Conflicts - EF Core — one common reason to recover and retry inside a transaction.