You are here: Documentation > .NET > APIs > Data Access > Entity Framework Core > Advanced Topics

Entity Framework Core: Advanced Topics

Dive deeper into Entity Framework Core (EF Core) with these advanced topics. Explore techniques to optimize performance, handle complex scenarios, and leverage the full power of EF Core in your .NET applications.

Key Advanced Concepts

Change Tracking Explained

EF Core's change tracking mechanism is fundamental to how it detects and persists changes made to entities. Understanding how EF Core tracks entity states (Added, Modified, Deleted, Unchanged, Detached) and property values is crucial for efficient data operations.

The DbContext instance maintains a cache of entities it's tracking. When you query entities, they are automatically added to the change tracker. Modifications to entity properties are recorded. When SaveChanges() is called, EF Core inspects the change tracker to generate the appropriate SQL commands.

// Example: Detecting changes
                var product = await _context.Products.FindAsync(1);
                product.Price = 15.99M;
                // EF Core automatically marks the entity as Modified.

                var entry = _context.Entry(product);
                Console.WriteLine($"Entity State: {entry.State}"); // Outputs: Modified
                Console.WriteLine($"Original Price: {entry.OriginalValues["Price"]}"); // Outputs: Original price
                

You can also manually manage the state of entities using methods like _context.Entry(entity).State = EntityState.Deleted;.

Concurrency Control

Concurrency occurs when multiple users or processes attempt to modify the same data simultaneously. EF Core provides strategies to handle these conflicts gracefully.

  • Optimistic Concurrency: This is the most common approach. It assumes conflicts are rare. EF Core detects conflicts by checking a versioning column (e.g., a rowversion or timestamp) or by comparing all original values of the entity. If a conflict is detected during SaveChanges(), an DbUpdateConcurrencyException is thrown.
  • Pessimistic Concurrency: This involves locking the data to prevent other users from modifying it while one user is working on it. EF Core supports this via database-specific mechanisms like SELECT ... FOR UPDATE.

To implement optimistic concurrency, mark a property in your entity with the [Timestamp] attribute (for SQL Server) or configure a row version property in OnModelCreating.

// Entity with Timestamp for optimistic concurrency
                public class Product
                {
                    public int Id { get; set; }
                    public string Name { get; set; }
                    public decimal Price { get; set; }
                    public byte[] RowVersion { get; set; } // For optimistic concurrency
                }

                // Handling DbUpdateConcurrencyException
                try
                {
                    await _context.SaveChangesAsync();
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    // Handle the concurrency conflict, e.g., reload the entity,
                    // prompt the user, or merge changes.
                    var entityEntries = ex.Entries;
                    foreach (var entry in entityEntries)
                    {
                        var databaseValues = await entry.GetDatabaseValuesAsync();
                        if (databaseValues == null)
                        {
                            Console.WriteLine("The entity being updated has been deleted by another user.");
                            return;
                        }

                        var proposedValues = entry.CurrentValues;
                        var originalValues = entry.OriginalValues;

                        // Refresh original values and client values from the database
                        entry.Reload();

                        // You might want to show the user the conflicting values
                        // and let them decide how to resolve it.
                    }
                    // Retry saving after resolving conflicts
                    // await _context.SaveChangesAsync();
                }
                

Performance Optimizations

Efficiently querying and saving data is vital for application performance. EF Core offers several strategies:

  • Asynchronous Operations: Always use asynchronous methods like ToListAsync(), FindAsync(), and SaveChangesAsync() to prevent blocking the application's thread pool.
  • Query Optimization:
    • Projection: Select only the properties you need using Select() to reduce the amount of data transferred from the database.
    • Filtering: Use Where() clauses to fetch only relevant data.
    • Lazy Loading vs. Eager Loading: Understand when to use Include() (eager loading) versus relying on lazy loading (requires proxy creation enabled). Eager loading is generally preferred for performance when you know you'll need related data.
    • AsNoTracking(): For read-only queries, use AsNoTracking() to disable change tracking, which significantly improves performance.
  • Batching: Perform multiple inserts, updates, or deletes in a single database roundtrip. EF Core itself doesn't provide built-in batching for multiple entities, but libraries like EFCore.BulkExtensions can be used.
  • Connection Pooling: EF Core utilizes ADO.NET's connection pooling, so reusing DbContext instances within a unit of work is important.
  • ExecuteSqlRawAsync()/ExecuteSqlInterpolatedAsync(): For complex or highly optimized operations, consider executing raw SQL.
// Optimized query using AsNoTracking() and Select()
                var productNames = await _context.Products
                    .AsNoTracking() // Disable change tracking for read-only query
                    .Where(p => p.Price > 10)
                    .Select(p => p.Name) // Project only the Name property
                    .ToListAsync();

                // Eager loading
                var productsWithCategories = await _context.Products
                    .Include(p => p.Category)
                    .ToListAsync();
                

Custom Conventions

Conventions allow you to automatically configure your model based on common patterns in your entity classes. You can define custom conventions to enforce specific naming conventions, automatically set property types, or apply configurations.

Custom conventions are implemented by inheriting from IEntityTypeConvention, IPropertyConvention, etc., or by using the fluent API builder in OnModelCreating.

// Example: Convention to pluralize table names
                public class TablePluralizationConvention : IEntityTypeConvention
                {
                    public void Apply(IConventionEntityTypeBuilder entityTypeBuilder)
                    {
                        var tableName = entityTypeBuilder.Metadata.Relational().TableName;
                        if (tableName.EndsWith("y"))
                        {
                            entityTypeBuilder.ToTable(tableName + "ies");
                        }
                        else
                        {
                            entityTypeBuilder.ToTable(tableName + "s");
                        }
                    }
                }

                // In DbContext.OnModelCreating:
                protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
                {
                    configurationBuilder.Conventions.Add(new TablePluralizationConvention());
                }
                

Working with Expression Trees

EF Core translates LINQ queries into SQL using Expression Trees. While you typically don't need to manually construct them for standard queries, understanding them is beneficial for advanced scenarios like creating custom query operators or dynamic querying.

Expression trees represent code as a data structure. EF Core analyzes these trees to generate SQL.

// Manual creation of a simple expression tree
                var parameter = Expression.Parameter(typeof(Product), "p");
                var property = Expression.Property(parameter, "Price");
                var constant = Expression.Constant(10M);
                var comparison = Expression.GreaterThan(property, constant);
                var lambda = Expression.Lambda>(comparison, parameter);

                // You could then use this lambda with LINQ methods,
                // but EF Core does this automatically.
                // _context.Products.Where(lambda).ToList();
                

Compiled Models

Compiled Models are a performance optimization that pre-compiles your EF Core model and queries into C# code. This avoids the overhead of model discovery and query translation at runtime, leading to faster startup times and improved query performance.

To use compiled models:

  1. Install the Microsoft.EntityFrameworkCore.Design NuGet package.
  2. Add a class that implements IModelCustomizer.
  3. In the customizer, use the ModelBuilder to configure your model.
  4. Use the dotnet ef compile-models command.
// Example IModelCustomizer
                public class MyModelCustomizer : IModelCustomizer
                {
                    public void Customize(ModelBuilder modelBuilder)
                    {
                        modelBuilder.Entity().Property(p => p.Name).IsRequired();
                        // ... other configurations
                    }
                }

                // Command: dotnet ef compile-models --context YourDbContext --output-dir CompiledModels
                

Interceptors

Interceptors allow you to hook into EF Core's pipeline and intercept various operations, such as saving changes, executing commands, or opening connections. This is useful for cross-cutting concerns like logging, auditing, or modifying commands.

You can create custom interceptors by implementing interfaces like DbCommandInterceptor or SaveChangesInterceptor.

// Example DbCommandInterceptor for logging SQL
                public class SqlLoggingInterceptor : DbCommandInterceptor
                {
                    public override InterceptionResult CommandExecuting(DbCommand command, CommandEventData eventData, InterceptionResult result)
                    {
                        Console.WriteLine($"Executing SQL: {command.CommandText}");
                        return result;
                    }
                }

                // In DbContext:
                protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
                {
                    optionsBuilder.AddInterceptors(new SqlLoggingInterceptor());
                }
                

Global Query Filters

Global query filters allow you to apply a Where() clause to all queries for a specific entity type. This is commonly used for soft deletes or multi-tenancy.

Configure global filters in OnModelCreating using HasQueryFilter().

// Entity with a IsDeleted flag for soft delete
                public class Product
                {
                    public int Id { get; set; }
                    public string Name { get; set; }
                    public bool IsDeleted { get; set; } = false;
                }

                // In DbContext.OnModelCreating:
                protected override void OnModelCreating(ModelBuilder modelBuilder)
                {
                    modelBuilder.Entity().HasQueryFilter(p => !p.IsDeleted);
                    // To include deleted items:
                    // _context.Products.IgnoreQueryFilters().Where(...).ToList();
                }
                

Executing Raw SQL Queries

While LINQ is the preferred way to query data, EF Core allows you to execute raw SQL queries when necessary, for example, to use database-specific features or optimize complex queries.

Use FromSqlRaw() or FromSqlInterpolated() to execute raw SQL and map the results to your entity types.

// Using FromSqlRaw
                var products = await _context.Products
                    .FromSqlRaw("SELECT * FROM Products WHERE Price > {0}", 50)
                    .ToListAsync();

                // Using FromSqlInterpolated (safer against SQL injection)
                var productName = "Laptop";
                var priceThreshold = 1000M;
                var filteredProducts = await _context.Products
                    .FromSqlInterpolated($"SELECT * FROM Products WHERE Name = {productName} AND Price > {priceThreshold}")
                    .ToListAsync();

                // Executing non-query SQL (e.g., stored procedures)
                await _context.Database.ExecuteSqlRawAsync("EXEC UpdateProductPrice @Id, @NewPrice", new SqlParameter("@Id", 1), new SqlParameter("@NewPrice", 20.00M));
                

Batch Delete and Update

EF Core's SaveChanges() operation typically translates each entity's state change into a separate SQL statement. For large numbers of entities, this can lead to many roundtrips to the database.

While EF Core doesn't have built-in batching for SaveChanges(), you can use third-party libraries like EFCore.BulkExtensions or execute SQL commands directly for more efficient batch operations.

Using EFCore.BulkExtensions (example):

// Requires installing EFCore.BulkExtensions package
                // using EFCore.BulkExtensions;

                var productsToUpdate = await _context.Products.Where(p => p.Price < 5).ToListAsync();
                foreach(var p in productsToUpdate) { p.Price *= 1.1M; } // Increase price by 10%
                await _context.BulkUpdateAsync(productsToUpdate); // Efficiently updates multiple entities

                var productsToDelete = await _context.Products.Where(p => p.IsDeleted).ToListAsync();
                await _context.BulkDeleteAsync(productsToDelete); // Efficiently deletes multiple entities