Transactions in Entity Framework Core
Transactions are fundamental to database operations, ensuring data integrity by grouping multiple operations into a single, atomic unit. If any operation within a transaction fails, the entire transaction can be rolled back, leaving the database in its original state.
Understanding Transactions
When you save changes using Entity Framework Core's `DbContext.SaveChanges()`, it typically executes these changes within a database transaction. This provides a default level of safety for your data modifications.
Explicitly Managing Transactions
In scenarios where you need to perform multiple `SaveChanges()` operations as part of a single logical unit, or when you need finer control over transaction behavior (like setting isolation levels), you can explicitly manage transactions.
Using `BeginTransaction()`
The `DbContext` provides a method `Database.BeginTransaction()` to start a new transaction. You then execute your operations and explicitly commit or roll back the transaction.
using (var context = new MyDbContext())
{
using (var transaction = context.Database.BeginTransaction())
{
try
{
// Operation 1
var user = new User { Name = "Alice" };
context.Users.Add(user);
context.SaveChanges();
// Operation 2
var product = new Product { Name = "Laptop", Price = 1200.00m };
context.Products.Add(product);
context.SaveChanges();
// If all operations are successful, commit the transaction
transaction.Commit();
Console.WriteLine("Transaction committed successfully.");
}
catch (Exception ex)
{
// If any operation fails, roll back the transaction
transaction.Rollback();
Console.WriteLine($"Transaction rolled back due to an error: {ex.Message}");
// Handle the exception appropriately
}
}
}
Using `TransactionScope` (Distributed Transactions)
For more complex scenarios involving multiple databases or resource managers (distributed transactions), you can leverage the `TransactionScope` class from System.Transactions
.
using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
using (var context1 = new MyDbContext())
{
var user = new User { Name = "Bob" };
context1.Users.Add(user);
context1.SaveChanges();
}
using (var context2 = new AnotherDbContext()) // Example with a second DbContext
{
var order = new Order { UserId = 1, OrderDate = DateTime.Now };
context2.Orders.Add(order);
context2.SaveChanges();
}
// If all operations are successful, complete the scope to commit
scope.Complete();
Console.WriteLine("Distributed transaction completed.");
}
// If an exception occurs before scope.Complete(), the transaction is automatically rolled back.
TransactionScopeAsyncFlowOption.Enabled
for async operations.
Transaction Isolation Levels
Database transactions have isolation levels that define how concurrent transactions affect each other. EF Core allows you to specify these levels when starting a transaction.
using (var context = new MyDbContext())
{
var transactionOptions = new TransactionArgs
{
IsolationLevel = System.Data.IsolationLevel.RepeatableRead
};
using (var transaction = context.Database.BeginTransaction(transactionOptions))
{
// ... operations ...
transaction.Commit();
}
}
Common isolation levels include:
ReadUncommitted
ReadCommitted
(often the default)RepeatableRead
Serializable
Choosing the appropriate isolation level is crucial for balancing data consistency and concurrency performance.
Best Practices
- Keep transactions as short as possible to minimize locking.
- Handle exceptions gracefully and ensure rollback occurs when needed.
- Use `using` statements for `DbContext` and transaction objects to ensure proper disposal.
- Understand the implications of different isolation levels.
For more details on specific database provider implementations and advanced transaction management, please refer to the official Entity Framework Core documentation.