Chapter 14 · Transactions, Savepoints, Execution Strategies, and Cross-Context Coordination
Share DbConnection / DbTransaction Across Contexts and Coordinate EF with ADO.NET
Share one open relational connection and transaction across multiple DbContext instances and raw ADO.NET without pretending their change trackers are shared.
Learning outcomes
Sometimes one local transaction must span code owned by
different data-access components: two
DbContext instances, EF plus a hand-written ADO.NET
command, or a gradual migration from legacy data access. EF can
coordinate this when the contexts use the
same relational DbConnection and DbTransaction.
Sharing the transaction does not merge their state managers.
Construct multiple ServiceHubContext instances over one externally owned open DbConnection.
Begin a transaction in one context and enlist another with UseTransactionAsync.
Coordinate a raw parameterized ADO.NET command on the same connection/transaction.
Explain connection/transaction ownership and disposal ordering.
Prove that two contexts still have independent identity maps and stale tracked values.
Diagnose failures caused by different physical connections, incompatible providers, or leaked connection state.
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. Cross-context sharing has two prerequisites
Relational transaction sharing requires both the same
DbConnection object and the same
DbTransaction. Merely using identical connection
strings is not enough: two separately opened connections are
distinct database sessions and cannot share a local transaction
object.
| Shared thing | Required? | Why |
|---|---|---|
| Connection string | not sufficient | Can open different physical/logical sessions. |
| DbConnection instance | yes | The transaction belongs to this connection. |
| DbTransaction instance | yes | Each participant must enlist in the same local transaction. |
| DbContext ChangeTracker | no / impossible to merge automatically | Each context remains its own state manager/identity map. |
2. Build contexts over an externally owned SQLite connection
await using var connection = new SqliteConnection( "Data Source=servicehub-lab.db");await connection.OpenAsync(ct);var options = new DbContextOptionsBuilder<ServiceHubContext>() .UseSqlite(connection) .Options;await using var db1 = new ServiceHubContext(options);await using var db2 = new ServiceHubContext(options);await using var tx = await db1.Database.BeginTransactionAsync(ct);await db2.Database.UseTransactionAsync(tx.GetDbTransaction(), ct);
The application owns the connection here and must keep it open until all contexts and the transaction are finished. A context configured with an external connection should not be treated as the sole lifetime owner in architecture diagrams or cleanup code.
3. EF in context 1, EF in context 2, same database commit
var first = await db1.WorkOrders.SingleAsync(x => x.Id == firstId, ct);first.ReviseSummary("updated by component A");first.AdvanceRevision();await db1.SaveChangesAsync(ct);var second = await db2.WorkOrders.SingleAsync(x => x.Id == secondId, ct);second.ReviseSummary("updated by component B");second.AdvanceRevision();await db2.SaveChangesAsync(ct);await tx.CommitAsync(ct);
If the second SaveChanges fails and the transaction rolls back,
the first context's database update rolls back too—even though
db1 may still contain CLR/tracker state reflecting
its previously successful SaveChanges. As with savepoint
rollback, database rollback and state-manager rewind are
different operations.
4. Add raw ADO.NET evidence inside the same transaction
The raw command below is intentionally a read so it demonstrates transaction enlistment without inventing new ServiceHub write semantics. It uses the same connection and transaction and observes the database state after EF has issued its uncommitted update.
await db1.SaveChangesAsync(ct);await using var command = connection.CreateCommand();command.Transaction = tx.GetDbTransaction();command.CommandText = """ SELECT summary FROM work_orders WHERE work_order_id = $id; """;command.Parameters.AddWithValue("$id", firstId);var summaryInsideTransaction = (string?)await command.ExecuteScalarAsync(ct);Console.WriteLine(summaryInsideTransaction);
A write command would follow the same enlistment rule: set
command.Transaction explicitly and parameterize
values. If raw SQL changes columns also tracked by EF,
refresh/reconcile affected entries before further tracked writes
or risk stale overwrites.
5. Deliberately wrong: same connection string, different connection
await using var db1 = await factory.CreateDbContextAsync(ct);await using var db2 = await factory.CreateDbContextAsync(ct);await using var tx = await db1.Database.BeginTransactionAsync(ct);await db2.Database.UseTransactionAsync(tx.GetDbTransaction(), ct); // wrong topology
If the factory opened/owns another connection for
db2, the transaction belongs to db1's
connection. The repair is to inject one externally created
connection into both context option sets, or choose a different
coordination design.
6. Sharing a transaction does not share identity resolution
var a = await db1.WorkOrders.SingleAsync(x => x.Id == id, ct);var b = await db2.WorkOrders.SingleAsync(x => x.Id == id, ct);Console.WriteLine(ReferenceEquals(a, b)); // False
Each context has its own identity map, original values, relationship fixup, pending changes, interceptors, and query settings. If context 1 saves a new Revision, context 2 can immediately become stale even though both participate in one transaction. Coordinate state deliberately or keep a single context when one state manager is simpler.
7. Ownership and disposal order
- Create/open the shared connection.
- Create contexts configured with that connection.
- Begin or accept the transaction.
- Enlist all EF/ADO.NET work.
- Commit/rollback transaction.
- Dispose contexts.
- Dispose the externally owned transaction/connection at the architecture boundary.
Do not return a shared connection to a pool with an active transaction or modified session state. Provider connection pooling can reuse physical connections later; transaction coordination code must leave them clean.
8. Hands-on lab
-
Open one
SqliteConnectionmanually and create two ServiceHub contexts from it. - Start the transaction in context 1 and enlist context 2.
- Update two distinct work orders from the two trackers.
- Run the raw ADO.NET scalar command with the shared transaction and verify it sees context 1's uncommitted database value.
- Rollback and query both work orders from a fresh context; verify neither update committed.
- Inspect both old trackers after rollback and explain why they must be discarded/reconciled.
- Repeat and commit; verify from a fresh connection.
- Attempt the deliberately wrong separate-connection enlistment in a disposable test and record the provider/EF failure.
Check your understanding
- Are identical connection strings enough to share a local transaction?
- Does UseTransaction merge two ChangeTrackers?
- What must a raw ADO.NET command set?
- Why can context 2 become stale after context 1 saves?
- Who disposes an externally created shared connection?
- When is one DbContext preferable?
Review the answers
No. Participants must use the same DbConnection and DbTransaction objects.
No. It only enlists database commands in the shared transaction.
The same open connection and its Transaction property to the shared DbTransaction, plus parameters for values.
Its independent tracker retains its own original/current values and does not receive another context’s state-manager updates.
The component/architecture boundary that created and owns it, after all transaction participants are finished.
When one unit of work can share one state manager cleanly and separate contexts add no boundary value.
9. Production judgment and bridge
Cross-context local transactions are useful compatibility seams,
not a default microservice architecture. They require explicit
connection ownership and state reconciliation. Lesson 5 moves
beyond one local DbTransaction to ambient
System.Transactions and distributed escalation,
then shows why ServiceHub should usually prefer local database
transactions plus outbox/saga workflow across service
boundaries.
Authoritative references
- EF Core Transactions - Cross-context transaction — sharing DbConnection and DbTransaction across contexts.
- EF Core Transactions - External DbTransactions — coordinating EF with raw ADO.NET.
- Transactions - Microsoft.Data.Sqlite — SQLite local transaction/connection behavior.
- Identity Resolution - EF Core — one state manager per context and identity-map semantics.
- Connection Resiliency - EF Core — transaction replay considerations for provider retry strategies.