Skip to main content

How to Set the Transaction Isolation Level in EF Core

· 3 min read
Jagdish Kumawat
Founder @ Dewiride

BeginTransactionAsync takes an isolation level as its first argument, and that's the whole API. The useful part is knowing what gets sent, how to confirm that MySQL really is using it, and which levels the driver refuses.

Prerequisites​

  • A .NET C# project using EF Core. I used EF Core 10.0.14, MySql.Data 26.10.0 and MySQL 9.7.4.
  • What the levels do. READ COMMITTED vs REPEATABLE READ in MySQL shows the difference between the two you're most likely to choose.

Step 1: Pass the Level​

IsolationLevel here is System.Data.IsolationLevel:

An endpoint
g.MapGet("/iso/{level}", async (IsolationLevel level, InventoryDbContext db, CancellationToken cancellationToken) =>
{
var strategy = db.Database.CreateExecutionStrategy();

return await strategy.ExecuteAsync(
async ct =>
{
await using var transaction = await db.Database.BeginTransactionAsync(level, ct);

var count = await db.Categories.CountAsync(ct);

await Task.Delay(2000, ct);

await transaction.CommitAsync(ct);

return Results.Ok(new { level = level.ToString(), count });
},
cancellationToken);
});

The endpoint in a macOS-style code window, passing the isolation level to BeginTransactionAsync inside ExecuteAsync.

Step 2: See What's Sent​

For READ COMMITTED:

Output (MySQL's log)
+------------------------------------------------+
| statement |
+------------------------------------------------+
| SET TRANSACTION ISOLATION LEVEL READ COMMITTED |
| BEGIN |
| SELECT COUNT(*) FROM `categories` AS `c` |
| COMMIT |
+------------------------------------------------+

The driver sends SET TRANSACTION ISOLATION LEVEL before each BEGIN, which applies to the next transaction only. So this doesn't affect other transactions on the connection. With no level, it sends REPEATABLE READ, MySQL's default.

Step 3: Confirm It in MySQL​

While the transaction was open, another session asked information_schema.innodb_trx:

Output (ReadCommitted)
+---------------------+
| trx_isolation_level |
+---------------------+
| READ COMMITTED |
+---------------------+
Output (Serializable)
+---------------------+
| trx_isolation_level |
+---------------------+
| SERIALIZABLE |
+---------------------+

Two query results from information_schema.innodb_trx, showing READ COMMITTED and SERIALIZABLE.

Step 4: Know What Maps to What​

IsolationLevelMySQL
ReadUncommittedREAD UNCOMMITTED
ReadCommittedREAD COMMITTED
RepeatableRead, or UnspecifiedREPEATABLE READ
SerializableSERIALIZABLE
SnapshotRefused with NotSupportedException
ChaosRefused with NotSupportedException

Common Mistakes​

MistakeWhat happensDo this instead
Raising the level to fix a racePlain reads lock under Serializable, and it can still deadlockUse a conditional UPDATE
Using the wrong IsolationLevelSystem.Transactions has one tooUse System.Data
Expecting it to apply to SaveChangesAsync aloneThat call starts its own, at the defaultStart one with the level

Troubleshooting​

SymptomLikely causeFix
Snapshot isolation level is not supportedMySQL has no such levelThe fix
The level seems to be ignoredYou checked another transactionCheck while it's open
does not support user-initiated transactionsRetries are onThe fix

Conclusion​

Pass a System.Data.IsolationLevel to BeginTransactionAsync. The driver sets it before BEGIN, for that transaction only, and innodb_trx shows whether it took effect.

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.