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:

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:

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
Note: Distributed transactions can be complex and have performance implications. Use them only when absolutely necessary. Ensure that your database provider and any other participating resources support distributed transactions.

Best Practices for Transactions

Tip: When manually managing transactions, remember that each 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.