Entity Framework Core Advanced Features

This document delves into advanced features of Entity Framework Core (EF Core) that can help you build more robust, performant, and maintainable data access solutions.

Transactions

EF Core automatically manages transactions for operations that modify the database. When you call SaveChanges(), EF Core wraps the operations within a database transaction. If any operation fails, the entire transaction is rolled back, ensuring data consistency.

You can also explicitly manage transactions for more control:


using (var context = new MyDbContext())
{
    using (var transaction = context.Database.BeginTransaction())
    {
        try
        {
            // Perform multiple operations
            context.Add(new Product { Name = "New Gadget" });
            context.Update(existingItem);
            context.SaveChanges();

            // If all operations succeed
            transaction.Commit();
        }
        catch (Exception ex)
        {
            // If any operation fails, rollback
            transaction.Rollback();
            // Handle the exception
            Console.WriteLine($"An error occurred: {ex.Message}");
        }
    }
}
            

Concurrency Control

Concurrency control is essential when multiple users or processes might try to update the same data simultaneously. EF Core provides mechanisms to handle this, most commonly through optimistic concurrency.

You can mark a property (e.g., a row version or timestamp) in your entity model that EF Core will use to detect conflicts. When saving changes, EF Core checks if the database value of this property matches the value loaded when the entity was retrieved. If not, an exception (typically DbUpdateConcurrencyException) is thrown.


public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    // Mark a row version for optimistic concurrency
    [Timestamp]
    public byte[] RowVersion { get; set; }
}
            

You can then catch this exception and implement strategies like re-fetching data, merging changes, or informing the user.

Performance Tuning

Optimizing EF Core performance is crucial for scalable applications. Key strategies include:

Logging

EF Core provides built-in logging capabilities that are invaluable for debugging and understanding database interactions. You can log SQL commands being executed, parameter values, and more.


public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<MyDbContext>(options =>
        options.UseSqlServer("YourConnectionString")
               .LogTo(Console.WriteLine, LogLevel.Information)); // Log to console
}
            

You can also configure logging to write to files or other sinks.

Advanced Change Tracking

EF Core meticulously tracks changes to entities. Understanding this process can help you optimize saving operations and diagnose issues.

You can manually control the state of an entity:


// Mark an entity as modified without loading it
context.Entry(product).State = EntityState.Modified;

// Mark an entity as detached
context.Entry(product).State = EntityState.Detached;
            

EF Core also provides the DetectChanges() method, which you can call explicitly if you need to ensure all tracked changes are detected before saving.

Executing Raw SQL

Sometimes, you might need to execute raw SQL queries for complex operations or performance optimizations not easily achievable with LINQ. EF Core allows you to do this.


// Execute a query that returns entities
var products = context.Products.FromSqlRaw("SELECT * FROM Products WHERE Category = {0}", "Electronics");

// Execute a command that doesn't return entities
context.Database.ExecuteSqlRaw("UPDATE Products SET Price = Price * 1.1 WHERE Category = {0}", "Books");
            

Be cautious when using raw SQL, as it bypasses some of EF Core's abstractions and can introduce security vulnerabilities if not handled carefully (e.g., SQL injection).

Calling Stored Procedures

You can call stored procedures from EF Core, either to execute them directly or to map their results to entities.


// Calling a stored procedure with output parameters
var productIdParam = new SqlParameter("@NewProductId", 0) { Direction = System.Data.ParameterDirection.Output };
context.Database.ExecuteSqlRaw("EXEC AddNewProduct @Name, @Price, @NewProductId OUT",
    new SqlParameter("@Name", "Super Widget"),
    new SqlParameter("@Price", 99.99),
    productIdParam);
int newId = (int)productIdParam.Value;

// Calling a stored procedure that returns a set of results
var data = context.MyCustomData.FromSqlInterpolated($"EXEC GetProductsByCategory {categoryName}");
            

Lazy Loading

Lazy loading allows related entities to be loaded only when they are first accessed. This can simplify code but may lead to performance issues (N+1 problem) if not used judiciously.

To enable lazy loading:

Eager Loading

Eager loading explicitly loads related entities along with the main entity in a single query. This is often more performant than lazy loading when you know you'll need the related data.


var blogs = context.Blogs
                 .Include(b => b.Posts) // Eager load Posts
                 .ToList();
            

You can chain Include() calls to load multiple levels of related data, and use ThenInclude() for more complex relationships.

Explicit Loading

Explicit loading provides a middle ground, allowing you to load related data on demand after the main entity has already been retrieved. This avoids the N+1 problem of lazy loading and the potential over-fetching of eager loading.


var blog = context.Blogs.Find(blogId);
// Manually load the Posts for this specific blog
context.Entry(blog).Collection(b => b.Posts).Load();
// Manually load a single related entity
var author = context.Authors.Find(authorId);
context.Entry(author).Reference(a => a.ContactInfo).Load();