0
How to handle Transaction in .NET Core?
1 Answer
0
Handling transactions in .NET Core (especially with Entity Framework Core) is straightforward once you understand when to rely on automatic behavior and when to take manual control.
1. Default Behavior (Auto Transactions)
When you call:
await dbContext.SaveChangesAsync();
Entity Framework Core automatically wraps all changes in a transaction.
Example
using (var context = new AppDbContext())
{
context.Users.Add(new User { Name = "A" });
context.Orders.Add(new Order { Amount = 100 });
await context.SaveChangesAsync(); // Auto transaction
}
- If anything fails → everything rolls back
- If success → everything commits
Use this for simple CRUD operations
2. Manual Transaction (Full Control)
Use this when:
- Multiple
SaveChanges()calls - Cross-service logic
- Complex business operations
Example
using (var context = new AppDbContext())
{
using (var transaction = await context.Database.BeginTransactionAsync())
{
try
{
context.Users.Add(new User { Name = "A" });
await context.SaveChangesAsync();
context.Orders.Add(new Order { Amount = 100 });
await context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch (Exception)
{
await transaction.RollbackAsync();
throw;
}
}
}
- Gives you fine-grained control
- Useful for multi-step operations
3. Using TransactionScope (Cross-Context / Distributed)
Use TransactionScope when:
- Multiple DB contexts
- Multiple databases
- External services involved
Example
using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
using (var context1 = new AppDbContext())
{
context1.Users.Add(new User { Name = "A" });
await context1.SaveChangesAsync();
}
using (var context2 = new AppDbContext())
{
context2.Orders.Add(new Order { Amount = 200 });
await context2.SaveChangesAsync();
}
scope.Complete();
}
Important:
- Requires
TransactionScopeAsyncFlowOption.Enabledfor async - Can escalate to distributed transaction (MSDTC) → expensive
4. Handling Isolation Levels
You can control concurrency behavior:
using (var transaction = await context.Database.BeginTransactionAsync(
System.Data.IsolationLevel.Serializable))
{
// Your logic
}
Common Levels
ReadCommitted(default) → safe + fastRepeatableRead→ prevents re-reading changesSerializable→ strictest (but slowest)
5. Best Practices (Production Level)
Keep transactions short
- Avoid long-running operations inside transactions.
Avoid external API calls inside transaction
Bad:
await transaction.BeginAsync();
await CallExternalAPI(); // risky
Use retry policies
Combine with:
- Polly
- EF Core execution strategies
Use one DbContext per transaction
- DbContext is NOT thread-safe
6. Advanced Pattern (Execution Strategy + Transaction)
For resilient systems (like SQL Azure):
var strategy = context.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
using (var transaction = await context.Database.BeginTransactionAsync())
{
try
{
context.Users.Add(new User { Name = "RetryUser" });
await context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
});
7. When to Use What?
| Scenario | Approach |
|---|---|
| Simple insert/update | Auto transaction |
| Multiple SaveChanges | Manual transaction |
| Multiple DB / services | TransactionScope |
| Cloud + retry needed | ExecutionStrategy |
Final Insight
- 80% cases → SaveChanges() is enough
- 15% → Manual transaction
- 5% → Distributed transactions