Skip to main content

How to Share a Transaction Between Two DbContexts in EF Core

· 4 min read
Jagdish Kumawat
Founder @ Dewiride

Most apps never need this, because one DbContext is enough. When you do have two, such as separate contexts for separate modules, they can share one transaction: they need the same connection, and the second has to be told about the transaction. Here it is, working and rolling back.

Prerequisites​

  • A .NET C# project using EF Core. I used EF Core 10.0.14 and MySQL 9.7.4.

Step 1: Share the Connection and the Transaction​

The EF Core docs put it plainly: "To share a transaction, the contexts must share both a DbConnection and a DbTransaction."

An endpoint
g.MapPost("/shared/{commit:bool}", async (bool commit, IConfiguration configuration, TimeProvider clock, CancellationToken cancellationToken) =>
{
await using var connection = new MySqlConnection(configuration.GetConnectionString("Inventory"));
await connection.OpenAsync(cancellationToken);

var options = new DbContextOptionsBuilder<InventoryDbContext>()
.UseMySQL(connection)
.Options;

await using var first = new InventoryDbContext(options);
await using var transaction = await first.Database.BeginTransactionAsync(cancellationToken);

first.Categories.Add(new Category { Name = "Shared one", CreatedAtUtc = clock.GetUtcNow().UtcDateTime });
await first.SaveChangesAsync(cancellationToken);

await using var second = new InventoryDbContext(options);
await second.Database.UseTransactionAsync(transaction.GetDbTransaction(), cancellationToken);

second.Categories.Add(new Category { Name = "Shared two", CreatedAtUtc = clock.GetUtcNow().UtcDateTime });
await second.SaveChangesAsync(cancellationToken);

if (commit)
{
await transaction.CommitAsync(cancellationToken);
}

return Results.Ok();
});

The endpoint in a macOS-style code window, opening a MySqlConnection, passing it to UseMySQL, beginning a transaction on the first context, and calling UseTransactionAsync on the second.

  • new MySqlConnection(...), then UseMySQL(connection). You create the connection, and you dispose of it.
  • GetDbTransaction() gets the underlying transaction out of EF Core's wrapper.
  • UseTransactionAsync tells the second context to use it.

For two different context types, the code is the same, with an options object for each type built on the same connection.

Step 2: Commit​

Output (the rows)
+------------+
| Name |
+------------+
| Shared one |
| Shared two |
+------------+

MySQL's log shows one BEGIN and one COMMIT:

Output (MySQL's log)
+--------------------------------------------------------------+
| statement |
+--------------------------------------------------------------+
| SET TRANSACTION ISOLATION LEVEL REPEATABLE READ |
| BEGIN |
| SAVEPOINT `__EFSavePoint` |
| INSERT INTO `categories` (`CreatedAtUtc`, `Name`) VALUES (ti |
| SELECT `Id` FROM `categories` WHERE ROW_COUNT() = 1 AND `Id` |
| RELEASE SAVEPOINT `__EFSavePoint` |
| SAVEPOINT `__EFSavePoint` |
| INSERT INTO `categories` (`CreatedAtUtc`, `Name`) VALUES (ti |
| SELECT `Id` FROM `categories` WHERE ROW_COUNT() = 1 AND `Id` |
| RELEASE SAVEPOINT `__EFSavePoint` |
| COMMIT |
+--------------------------------------------------------------+

The general query log with one BEGIN, two inserts each with its own savepoint, and one COMMIT.

Step 3: Leave Without Committing​

With commit set to false, both are gone:

Output (counting the rows)
+-------+
| saved |
+-------+
| 0 |
+-------+

Common Mistakes​

MistakeWhat happensDo this instead
Giving each context a connection stringTwo connections, which can't share a transactionPass the one connection
Disposing the connection too earlyThe contexts have nothing to useawait using it first, so it goes last

Troubleshooting​

SymptomLikely causeFix
The second context's rows stay after a rollbackIt isn't on the same connectionCheck the options
does not support user-initiated transactionsRetries are onPut it inside the strategy

Conclusion​

Create the connection yourself, build both contexts on it, begin on one, and UseTransactionAsync on the other. If you can use one DbContext, that's simpler.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.