EF Core Transactions
Understanding and managing transactions is crucial for ensuring data integrity and consistency in your applications. EF Core provides robust support for transaction management, allowing you to group multiple database operations into a single atomic unit of work.
Why Use Transactions?
Transactions are essential when you need to perform a series of operations that must either all succeed or all fail together. This is known as atomicity. Common scenarios include:
- Transferring funds between accounts: Debit one account and credit another. Both operations must succeed for the transfer to be complete.
- Creating related entities: When creating an order and its associated line items, both must be saved successfully.
- Updating multiple records in a logical group.
EF Core Transaction Management
EF Core offers several ways to manage transactions:
1. Automatic Transactions (Default Behavior)
By default, EF Core wraps each call to SaveChanges()
within its own database transaction. If SaveChanges()
completes successfully, the transaction is committed. If an exception occurs during SaveChanges()
, the transaction is rolled back automatically.
using (var context = new MyDbContext())
{
// Operation 1
context.Products.Add(new Product { Name = "New Gadget" });
// Operation 2
var customer = context.Customers.Find(1);
customer.OrdersCount++;
context.SaveChanges(); // Automatically wrapped in a transaction
}
2. Explicit Transactions
For more control, you can explicitly begin, commit, or roll back transactions. This is useful when you need to perform multiple SaveChanges()
calls within a single transaction, or when you need to combine EF Core operations with other database operations outside of EF Core's scope.
Starting a Transaction
You can start an explicit transaction using the BeginTransaction()
method on the DatabaseFacade
:
using (var context = new MyDbContext())
{
using (var transaction = context.Database.BeginTransaction())
{
try
{
// Operation 1
context.Products.Add(new Product { Name = "Super Widget" });
context.SaveChanges();
// Operation 2
var order = context.Orders.Find(10);
order.Status = "Shipped";
context.SaveChanges();
transaction.Commit(); // Commit the transaction if all operations succeed
}
catch (Exception ex)
{
transaction.Rollback(); // Roll back the transaction if any operation fails
// Log the exception and re-throw or handle as needed
throw;
}
}
}
Transaction Isolation Levels
You can specify the isolation level when starting a transaction to control how transactions interact with each other:
using (var context = new MyDbContext())
{
// Using a specific isolation level, e.g., ReadCommitted
var transactionOptions = new TransactionOptions
{
IsolationLevel = IsolationLevel.ReadCommitted
};
using (var transaction = context.Database.BeginTransaction(transactionOptions))
{
// ... operations ...
transaction.Commit();
}
}
Common isolation levels include:
Serializable
: The highest level. Ensures transactions are executed serially, preventing phantom reads, non-repeatable reads, and dirty reads.RepeatableRead
: Prevents dirty reads and non-repeatable reads.ReadCommitted
: Prevents dirty reads. This is often the default.ReadUncommitted
: Allows dirty reads.Snapshot
: Uses row versioning to provide a consistent view of the data without blocking other transactions.
3. Distributed Transactions
For scenarios involving multiple databases or resources (e.g., a SQL Server database and a message queue), you can use distributed transactions. EF Core supports integrating with System.Transactions
for this purpose.
using (var scope = new TransactionScope())
{
using (var context1 = new MyDbContext())
{
context1.Products.Add(new Product { Name = "Product A" });
context1.SaveChanges();
}
using (var context2 = new AnotherDbContext())
{
context2.Inventory.Add(new InventoryItem { ItemName = "Item X", Quantity = 10 });
context2.SaveChanges();
}
scope.Complete(); // Commit the distributed transaction
} // If scope.Complete() is not called, the transaction is rolled back
Best Practices for Transactions
- Keep transactions short: Long-running transactions can hold locks and impact concurrency.
- Perform I/O outside the transaction if possible: Operations like logging or sending emails are often better handled after a transaction is committed.
- Handle exceptions gracefully: Always include error handling to roll back transactions when necessary.
- Choose the right isolation level: Understand the trade-offs between consistency and concurrency for your application.
- Use explicit transactions for multi-
SaveChanges()
operations: If you need to ensure that multiple calls toSaveChanges()
succeed or fail together, use an explicit transaction.
SaveChanges()
call will by default start its own transaction if not explicitly managed. If you want multiple SaveChanges()
calls to be part of the same atomic operation, you must wrap them in a single explicit transaction.