Chapter 14 · Transactions, Savepoints, Execution Strategies, and Cross-Context Coordination
Ambient / System.Transactions Boundaries, Distributed Transactions, and Safer Architectural Alternatives
Bound TransactionScope and distributed transactions by provider/platform capability, then choose local transactions plus outbox/saga patterns for cross-service workflows.
Learning outcomes
An ambient transaction is a transaction
implicitly available to code through
System.Transactions.Transaction.Current, usually
created by TransactionScope. It can simplify
in-process coordination when providers support it, but it can
also hide enlistment, escalate to a distributed transaction, and
fail completely on unsupported providers/platforms. EF Core does
not make distributed transactions portable.
Explain TransactionScope, ambient enlistment, Required/RequiresNew/Suppress, and async-flow requirements.
Record the mandatory SQLite result: Microsoft.Data.Sqlite does not support System.Transactions.
Bound SQL Server/SqlClient ambient/distributed support by provider and platform rather than EF abstraction.
Explain distributed-transaction escalation and .NET’s Windows-only System.Transactions distributed support.
Reject TransactionScope as a way to make HTTP/message-broker calls atomic with a database.
Design ServiceHub cross-service consistency with local transaction + outbox + idempotent saga/process coordination.
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. TransactionScope changes how code finds a transaction
using var scope = new TransactionScope( TransactionScopeOption.Required, TransactionScopeAsyncFlowOption.Enabled);await db.SaveChangesAsync(ct);await otherSupportedResource.DoDatabaseWorkAsync(ct);scope.Complete();
Required joins an existing ambient transaction or
creates one; RequiresNew creates a new ambient
transaction; Suppress runs without the ambient
transaction. For async code,
TransactionScopeAsyncFlowOption.Enabled is required
so the ambient transaction flows across continuations.
2. Mandatory SQLite capability check: do not rely on TransactionScope
Microsoft.Data.Sqlite documentation explicitly says System.Transactions is not supported and recommends ADO.NET transactions instead. Therefore TransactionScope is not a valid mandatory ServiceHub SQLite lab. The correct local mechanism is BeginTransaction/DbTransaction as taught in Lessons 1–4.
await using var tx = await db.Database.BeginTransactionAsync(ct);try{ // ServiceHub aggregate + outbox database work only await db.SaveChangesAsync(ct); await tx.CommitAsync(ct);}catch{ await tx.RollbackAsync(ct); throw;}
This negative capability test is part of provider-aware engineering: a common .NET API existing in the framework does not mean the active ADO.NET provider implements it.
3. Provider support and distributed escalation are separate questions
| Question | Example answer | Why it matters |
|---|---|---|
| Does provider enlist in System.Transactions? | SqlClient supports it; Microsoft.Data.Sqlite does not | Unsupported providers may ignore/fail ambient semantics. |
| Is it still one local resource? | One compatible database connection/resource manager may remain local | Local ambient transaction can be cheaper/simpler than distributed coordination. |
| Did multiple durable resource managers enlist? | May escalate to distributed transaction | Requires DTC-capable infrastructure and operations. |
| Does the platform support distributed System.Transactions? | .NET distributed transaction support is Windows-only | Non-Windows deployments cannot assume it works. |
| Does TransactionScope dispose asynchronously? | No async commit/rollback on scope disposal | Synchronous disposal can block the executing thread. |
4. Deliberately wrong: put database + HTTP + broker inside TransactionScope
using var scope = new TransactionScope( TransactionScopeAsyncFlowOption.Enabled);await db.SaveChangesAsync(ct);await httpClient.PostAsJsonAsync("https://inventory/...", request, ct);await messageBus.PublishAsync(message, ct);scope.Complete();
An ordinary HTTP server or message broker does not become a System.Transactions participant because the call happened lexically inside the scope. A database rollback cannot un-send a completed HTTP request. Conversely, a broker publish can fail after the database committed. This is the same dual-write problem Chapter 12’s outbox addressed.
5. Safer ServiceHub boundary: local transaction + outbox
var order = await db.WorkOrders.SingleAsync(x => x.Id == id, ct);order.ReviseSummary("dispatch approved");order.AdvanceRevision();db.Set<OutboxMessage>().Add(new OutboxMessage{ Id = commandId, // stable idempotency identity OccurredUtc = clock.GetUtcNow().UtcDateTime, Type = "WorkOrderDispatchApproved", PayloadJson = BuildMinimalOutboxPayload(order)});await db.SaveChangesAsync(ct); // one local database transaction// Separate dispatcher publishes only after the row is durable.
The local database stays strongly atomic. Cross-service progress becomes a durable workflow rather than an impossible global lock over independently deployed systems.
6. Saga/process manager: consistency over time, not one giant transaction
A saga coordinates a multi-step business workflow where each participant commits a local transaction and later steps may require compensating actions. A process manager records workflow state and decides which command/event comes next. Neither provides isolation equivalent to a single database transaction; they provide explicit recovery and progress semantics across boundaries.
| Local database transaction | Distributed transaction | Outbox + saga/process manager |
|---|---|---|
| Atomic within one database/resource | Atomic across supported enlisted resources | Each service commits locally; workflow converges through durable messages |
| Short, efficient when scoped well | Operationally heavy; provider/platform constrained | Designed for independently deployed services and partial failure |
| Rollback restores local DB state | Coordinator can commit/abort participants | Compensation/idempotency handles already-committed steps |
| Use for ServiceHub aggregate + outbox | Use only when requirements/infrastructure truly justify it | Preferred general cross-service learning/production pattern |
7. Idempotency and compensation are first-class
If the outbox dispatcher republishes after a crash, downstream handlers should recognize the stable message/command ID. If step 3 of a saga fails after steps 1 and 2 committed, the business must define whether to retry, compensate, pause for operator review, or mark the workflow failed. “Rollback everything” may no longer be physically possible.
A compensation is a new business action that attempts to counter an earlier committed action. It can itself fail and may not perfectly restore the original world—for example, an email cannot be unsent and an external reservation may incur fees.
8. Hands-on boundary lab
- Record the Microsoft.Data.Sqlite documentation result that System.Transactions is unsupported; do not create a misleading TransactionScope success test on SQLite.
- Run the free local alternative: one ServiceHub update + outbox insert, inject a failure, and prove one local transaction rolls back both.
- Run the outbox dispatcher twice with the same stable ID and verify the fake downstream consumer deduplicates/idempotently handles the replay.
- Model a three-step saga table/state machine on paper or in a small test: reserve part → schedule technician → notify customer; define failure/compensation for each step.
-
For optional Windows + SQL Server/SqlClient, test a local
TransactionScope with
TransactionScopeAsyncFlowOption.Enabledand recordTransaction.Currentplus provider behavior. - Do not require MSDTC/distributed transactions for the mandatory lab. If experimenting, document Windows-only support, DTC configuration, firewall/security, provider enlistment, and operational rollback/recovery.
- Capture workflow IDs, local transaction outcome, outbox delivery attempts, and compensation status in structured logs.
Check your understanding
- Does EF Core make System.Transactions work on every provider?
- What must TransactionScope use in async code?
- Are distributed System.Transactions portable across Linux and Windows?
- Will TransactionScope make an HTTP request roll back?
- What problem does an outbox solve?
- What is compensation?
Review the answers
No. Providers must implement support; Microsoft.Data.Sqlite explicitly does not.
TransactionScopeAsyncFlowOption.Enabled so the ambient transaction flows across async continuations.
No. EF docs state distributed transaction support is Windows-only on modern .NET.
No. The remote service is not automatically an enlisted transactional resource.
It atomically records local state and durable intent to publish, avoiding the database/message dual-write gap.
A new business action used to counter an already-committed saga step; it is not a physical rollback and can fail.
9. Production judgment and bridge to Chapter 15
Prefer the narrowest coordination mechanism that matches the topology: one SaveChanges transaction, a short explicit DbTransaction, savepoints, a provider execution-strategy delegate, or shared local DbTransaction when legacy components genuinely need it. Treat ambient/distributed transactions as provider/platform infrastructure choices, never universal EF features. Across service boundaries, make local commits durable with an outbox and coordinate progress explicitly. Chapter 15 now shifts from runtime transaction boundaries to schema-change transactions: migration snapshots, generated operations, scripts, bundles, seeding, deployment ownership, and rollback strategy.
Authoritative references
- EF Core Transactions - System.Transactions — ambient transaction support, async-flow note, provider limitations, and Windows-only distributed support.
- TransactionScope API - .NET 10 — ambient transaction programming model.
- TransactionScopeAsyncFlowOption — async ambient-transaction flow semantics.
- Microsoft.Data.Sqlite ADO.NET limitations — explicit statement that System.Transactions is unsupported.
- Connection Resiliency - EF Core — execution-strategy interaction with ambient/user transactions and replay.
- Transactional Outbox pattern - Azure Architecture Center — durable local transaction plus later message publication pattern.