How do you handle database transactions in Entity Framework Core?
Quick Answer
EF Core wraps each SaveChanges in an implicit transaction. For multi-operation atomicity you control transactions explicitly with Database.BeginTransaction()/BeginTransactionAsync() (commit/rollback), share a transaction across contexts/connections, or use TransactionScope. The execution strategy (retries) requires wrapping manual transactions in Database.CreateExecutionStrategy().Execute(...) to remain resilient.
Detailed Answer
Database transactions in Entity Framework Core ensure data consistency by grouping multiple operations into a single unit of work. If any operation fails, all changes are rolled back.
1. Automatic Transactions (Default Behavior)
EF Core automatically creates a transaction for each SaveChanges() call:
public class OrderService
{
private readonly AppDbContext _context;
public OrderService(AppDbContext context)
{
_context = context;
}
// Single SaveChanges() = single transaction
public async Task CreateOrderAsync(Order order)
{
_context.Orders.Add(order);
_context.OrderItems.AddRange(order.OrderItems);
// This creates and commits a transaction automatically
await _context.SaveChangesAsync();
}
}
2. Manual Transaction Management
Use BeginTransaction() for explicit transaction control:
public async Task TransferMoneyAsync(int fromAccountId, int toAccountId, decimal amount)
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// Withdraw from source account
var fromAccount = await _context.Accounts.FindAsync(fromAccountId);
if (fromAccount.Balance < amount)
throw new InsufficientFundsException();
fromAccount.Balance -= amount;
// Deposit to target account
var toAccount = await _context.Accounts.FindAsync(toAccountId);
toAccount.Balance += amount;
// Create transaction record
_context.Transactions.Add(new Transaction
{
FromAccountId = fromAccountId,
ToAccountId = toAccountId,
Amount = amount,
Timestamp = DateTime.UtcNow
});
// Save all changes
await _context.SaveChangesAsync();
// Commit transaction
await transaction.CommitAsync();
}
catch
{
// Rollback happens automatically when transaction is disposed
await transaction.RollbackAsync();
throw;
}
}
3. Transaction with Isolation Levels
Control transaction isolation for different consistency requirements:
public async Task ProcessOrderWithLockAsync(int orderId)
{
using var transaction = await _context.Database.BeginTransactionAsync(
IsolationLevel.ReadCommitted);
try
{
// Lock the order row for update
var order = await _context.Orders
.FromSqlRaw("SELECT * FROM Orders WITH (UPDLOCK) WHERE Id = {0}", orderId)
.FirstOrDefaultAsync();
if (order.Status != OrderStatus.Pending)
throw new InvalidOperationException("Order already processed");
order.Status = OrderStatus.Processing;
order.ProcessedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
4. Distributed Transactions (Multiple Contexts)
Handle transactions across multiple database contexts:
public async Task ProcessOrderAcrossDatabasesAsync(Order order)
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// Save to main database
_context.Orders.Add(order);
await _context.SaveChangesAsync();
// Save to audit database
using var auditContext = new AuditDbContext();
auditContext.Database.UseTransaction(transaction.GetDbTransaction());
auditContext.AuditLogs.Add(new AuditLog
{
EntityType = "Order",
EntityId = order.Id,
Action = "Created",
Timestamp = DateTime.UtcNow
});
await auditContext.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
5. Transaction Scope (System.Transactions)
Use TransactionScope for distributed transactions:
public async Task ProcessOrderWithTransactionScopeAsync(Order order)
{
using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
try
{
// Multiple operations that can span different databases
await _context.Orders.AddAsync(order);
await _context.SaveChangesAsync();
// Call external service
await _paymentService.ProcessPaymentAsync(order.PaymentInfo);
// Send notification
await _notificationService.SendOrderConfirmationAsync(order);
scope.Complete(); // Commit all operations
}
catch
{
// Automatic rollback when scope is disposed
throw;
}
}
6. Nested Transactions
Handle nested transaction scenarios:
public async Task ProcessBulkOrdersAsync(List<Order> orders)
{
using var outerTransaction = await _context.Database.BeginTransactionAsync();
try
{
foreach (var order in orders)
{
// Each order processing is a nested transaction
await ProcessSingleOrderAsync(order);
}
await outerTransaction.CommitAsync();
}
catch
{
await outerTransaction.RollbackAsync();
throw;
}
}
private async Task ProcessSingleOrderAsync(Order order)
{
using var innerTransaction = await _context.Database.BeginTransactionAsync();
try
{
_context.Orders.Add(order);
await _context.SaveChangesAsync();
// Additional processing
await UpdateInventoryAsync(order.OrderItems);
await innerTransaction.CommitAsync();
}
catch
{
await innerTransaction.RollbackAsync();
throw;
}
}
7. Transaction Best Practices
public class TransactionBestPractices
{
private readonly AppDbContext _context;
// ✅ Good: Use using statements for automatic disposal
public async Task GoodTransactionAsync()
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// Your operations here
await _context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
// ❌ Bad: Manual transaction management without proper cleanup
public async Task BadTransactionAsync()
{
var transaction = await _context.Database.BeginTransactionAsync();
try
{
await _context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
// Missing: transaction.Dispose() - can cause connection leaks
}
// ✅ Good: Appropriate isolation level
public async Task ReadCommittedTransactionAsync()
{
using var transaction = await _context.Database.BeginTransactionAsync(
IsolationLevel.ReadCommitted);
// Operations that need to see committed data
await _context.SaveChangesAsync();
await transaction.CommitAsync();
}
// ✅ Good: Handle transaction timeouts
public async Task TransactionWithTimeoutAsync()
{
using var transaction = await _context.Database.BeginTransactionAsync();
transaction.GetDbTransaction().CommandTimeout = 30; // 30 seconds
try
{
// Long-running operations
await _context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch (SqlException ex) when (ex.Number == -2) // Timeout
{
await transaction.RollbackAsync();
throw new TimeoutException("Transaction timed out", ex);
}
}
}
8. Transaction Monitoring and Logging
public class TransactionMonitoring
{
private readonly ILogger<TransactionMonitoring> _logger;
public async Task MonitoredTransactionAsync()
{
var stopwatch = Stopwatch.StartNew();
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
_logger.LogInformation("Transaction started: {TransactionId}",
transaction.TransactionId);
// Your operations
await _context.SaveChangesAsync();
await transaction.CommitAsync();
stopwatch.Stop();
_logger.LogInformation("Transaction committed successfully in {Duration}ms",
stopwatch.ElapsedMilliseconds);
}
catch (Exception ex)
{
await transaction.RollbackAsync();
stopwatch.Stop();
_logger.LogError(ex, "Transaction rolled back after {Duration}ms",
stopwatch.ElapsedMilliseconds);
throw;
}
}
}
Key Points:
- Automatic Transactions: Each
SaveChanges()creates a transaction automatically - Manual Control: Use
BeginTransaction()for explicit transaction management - Isolation Levels: Control data consistency with different isolation levels
- Distributed Transactions: Handle transactions across multiple databases
- Error Handling: Always rollback on exceptions
- Resource Management: Use
usingstatements for automatic cleanup - Performance: Keep transactions short to avoid blocking
- Monitoring: Log transaction duration and outcomes
Best Practices:
- Keep transactions as short as possible
- Use appropriate isolation levels
- Always handle exceptions and rollback
- Use
usingstatements for automatic disposal - Monitor transaction performance
- Avoid long-running operations in transactions
- Consider using
TransactionScopefor distributed scenarios