EF Core - Transactions

Transactions in Entity Framework Core (EF Core) are essential for ensuring data consistency and integrity during database operations. By using transactions, you can group multiple operations into a single unit of work, ensuring that either all operations succeed or none are applied. This tutorial covers the fundamentals of using transactions in EF Core to manage data integrity effectively.


1. Understanding Transactions

A transaction is a sequence of operations performed as a single logical unit of work. In the context of databases, transactions ensure that a series of operations either all succeed or all fail, maintaining data integrity.

        
            
public class TransactionExample
{
    public void PerformTransaction()
    {
        using (var transaction = connection.BeginTransaction())
        {
            try
            {
                // Execute operations
                transaction.Commit();
            }
            catch
            {
                transaction.Rollback();
                throw;
            }
        }
    }
}
        
    

This example illustrates the basic concept of a transaction in database operations.


2. Using Explicit Transactions in EF Core

EF Core provides the DbContext.Database.BeginTransaction method to start an explicit transaction. Use this method to manage transactions manually, committing or rolling back changes as needed.

        
            
public async Task UseExplicitTransactionAsync()
{
    using (var transaction = await _context.Database.BeginTransactionAsync())
    {
        try
        {
            var product = new Product { Name = "New Product", Price = 10.99m };
            _context.Products.Add(product);
            await _context.SaveChangesAsync();

            var order = new Order { ProductId = product.Id, Quantity = 5 };
            _context.Orders.Add(order);
            await _context.SaveChangesAsync();

            await transaction.CommitAsync();
        }
        catch (Exception)
        {
            await transaction.RollbackAsync();
            throw;
        }
    }
}
        
    

This example demonstrates how to use an explicit transaction to manage multiple database operations.


3. Implicit Transactions with SaveChanges

By default, EF Core automatically wraps the SaveChanges method in an implicit transaction. This ensures that all changes within a single call to SaveChanges are either applied or discarded.

        
            
public async Task UseImplicitTransactionAsync()
{
    try
    {
        var product = new Product { Name = "Sample Product", Price = 20.99m };
        _context.Products.Add(product);

        var order = new Order { ProductId = product.Id, Quantity = 5 };
        _context.Orders.Add(order);

        // SaveChangesAsync automatically wraps these changes in a transaction
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateException ex)
    {
        Console.WriteLine($"An error occurred during save: {ex.Message}");
    }
}
        
    

This example shows how implicit transactions are used with SaveChanges.


4. Transaction Scopes

Use TransactionScope to manage transactions across multiple database contexts or distributed resources. This is useful when working with multiple databases or ensuring consistency across different data sources.

        
            
public void UseTransactionScope()
{
    using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
    {
        try
        {
            var product = new Product { Name = "Scoped Product", Price = 20.99m };
            _context.Products.Add(product);
            _context.SaveChanges();

            using (var anotherContext = new AnotherDbContext())
            {
                var category = new Category { Name = "Electronics" };
                anotherContext.Categories.Add(category);
                anotherContext.SaveChanges();
            }

            scope.Complete(); // Commit the entire scope
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Transaction failed: {ex.Message}");
            // No need to call rollback as it is handled by TransactionScope
        }
    }
}
        
    

This example illustrates how to use a TransactionScope to manage transactions across multiple DbContexts.


5. Handling Transaction Exceptions

Handle exceptions within transactions using try-catch blocks to ensure that any errors result in a rollback, maintaining data integrity.

        
            
public async Task HandleTransactionExceptionAsync()
{
    using (var transaction = await _context.Database.BeginTransactionAsync())
    {
        try
        {
            // Perform database operations
            await _context.SaveChangesAsync();
            await transaction.CommitAsync();
        }
        catch (DbUpdateException ex)
        {
            await transaction.RollbackAsync();
            Console.WriteLine($"Transaction rolled back due to an error: {ex.Message}");
        }
    }
}
        
    

This example demonstrates how to handle exceptions in a transaction and roll back changes if an error occurs.


6. Savepoints in Transactions

Use savepoints within a transaction to create intermediate points to which you can roll back, allowing partial rollbacks without affecting the entire transaction.

        
            
public async Task UseSavepointAsync()
{
    using (var transaction = await _context.Database.BeginTransactionAsync())
    {
        try
        {
            var product = new Product { Name = "Savepoint Product", Price = 30.99m };
            _context.Products.Add(product);
            await _context.SaveChangesAsync();

            await transaction.CreateSavepointAsync("BeforeOrder");

            var order = new Order { ProductId = product.Id, Quantity = 2 };
            _context.Orders.Add(order);
            await _context.SaveChangesAsync();

            // Error occurs, rollback to savepoint
            await transaction.RollbackToSavepointAsync("BeforeOrder");

            await transaction.CommitAsync();
        }
        catch (Exception ex)
        {
            await transaction.RollbackAsync();
            Console.WriteLine($"Transaction failed: {ex.Message}");
        }
    }
}
        
    

This example shows how to use savepoints within a transaction to roll back to a specific point.


7. Nested Transactions

EF Core supports nested transactions, allowing you to create transactions within transactions. This is useful for complex operations requiring multiple levels of transactional control.

        
            
public async Task UseNestedTransactionsAsync()
{
    using (var transaction = await _context.Database.BeginTransactionAsync())
    {
        try
        {
            var product = new Product { Name = "Nested Product", Price = 25.99m };
            _context.Products.Add(product);
            await _context.SaveChangesAsync();

            using (var nestedTransaction = await _context.Database.BeginTransactionAsync())
            {
                try
                {
                    var order = new Order { ProductId = product.Id, Quantity = 4 };
                    _context.Orders.Add(order);
                    await _context.SaveChangesAsync();

                    await nestedTransaction.CommitAsync();
                }
                catch
                {
                    await nestedTransaction.RollbackAsync();
                    Console.WriteLine("Nested transaction rolled back");
                }
            }

            await transaction.CommitAsync();
        }
        catch
        {
            await transaction.RollbackAsync();
            Console.WriteLine("Outer transaction rolled back");
        }
    }
}
        
    

This example illustrates how to use nested transactions in EF Core.


8. Best Practices for Using Transactions

Here are some best practices for using transactions in EF Core:


9. Transactions in Distributed Systems

In distributed systems, managing transactions across multiple services and databases requires careful consideration of consistency models and potential trade-offs between consistency and availability.

        
            
public void UseDistributedTransaction()
{
    using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
    {
        try
        {
            var product = new Product { Name = "Distributed Product", Price = 50.99m };
            _context.Products.Add(product);
            _context.SaveChanges();

            using (var anotherContext = new AnotherDbContext())
            {
                var supplier = new Supplier { Name = "Global Supplier" };
                anotherContext.Suppliers.Add(supplier);
                anotherContext.SaveChanges();
            }

            scope.Complete(); // Commit both transactions
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Distributed transaction failed: {ex.Message}");
            // TransactionScope will handle rollback automatically
        }
    }
}
        
    

This example discusses the challenges and solutions for managing transactions in distributed systems.


10. Future of Transactions in EF Core

The future of transactions in EF Core includes ongoing improvements in performance, support for more complex transaction scenarios, and integration with cloud-based services for distributed transactions.


Summary

Transactions in EF Core are vital for ensuring data consistency and integrity during database operations. By understanding and effectively utilizing transactions, you can build reliable and robust applications that maintain data consistency even in the face of errors or complex operations.