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.

Advanced140–185 minutesshared connection/transaction + ADO.NET labEF Core 10.0.11 · .NET 10.0.11SQLite provider 10.0.11 mandatory baselineSDK checkpoint: 10.0.400Last reviewed: August 2026

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.

01

Construct multiple ServiceHubContext instances over one externally owned open DbConnection.

02

Begin a transaction in one context and enlist another with UseTransactionAsync.

03

Coordinate a raw parameterized ADO.NET command on the same connection/transaction.

04

Explain connection/transaction ownership and disposal ordering.

05

Prove that two contexts still have independent identity maps and stale tracked values.

06

Diagnose failures caused by different physical connections, incompatible providers, or leaked connection 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, 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

csharp · one connection, two contexts
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

csharp · independent trackers, shared transaction
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.

csharp · parameterized raw command enlisted in EF transaction
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

csharp · cannot enlist a transaction from another 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

csharp · same row, different CLR instances
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

  1. Create/open the shared connection.
  2. Create contexts configured with that connection.
  3. Begin or accept the transaction.
  4. Enlist all EF/ADO.NET work.
  5. Commit/rollback transaction.
  6. Dispose contexts.
  7. 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

  1. Open one SqliteConnection manually and create two ServiceHub contexts from it.
  2. Start the transaction in context 1 and enlist context 2.
  3. Update two distinct work orders from the two trackers.
  4. Run the raw ADO.NET scalar command with the shared transaction and verify it sees context 1's uncommitted database value.
  5. Rollback and query both work orders from a fresh context; verify neither update committed.
  6. Inspect both old trackers after rollback and explain why they must be discarded/reconciled.
  7. Repeat and commit; verify from a fresh connection.
  8. Attempt the deliberately wrong separate-connection enlistment in a disposable test and record the provider/EF failure.

Check your understanding

  1. Are identical connection strings enough to share a local transaction?
  2. Does UseTransaction merge two ChangeTrackers?
  3. What must a raw ADO.NET command set?
  4. Why can context 2 become stale after context 1 saves?
  5. Who disposes an externally created shared connection?
  6. 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

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