Advanced Entity Framework Core

Mastering Performance, Design Patterns, and Advanced Features

Introduction

Welcome to the advanced section of our Entity Framework Core tutorials. This module dives deep into features that can significantly improve the performance, maintainability, and robustness of your data access layer.

Table of Contents

Advanced Migrations

EF Core Migrations are a powerful tool for evolving your database schema. Beyond basic creation, we'll explore:

Generating SQL Scripts

To generate a SQL script for your pending migrations:

dotnet ef migrations script -o migrations.sql

This command generates a script that can be applied to any environment.

Managing Transactions

Ensuring data integrity is crucial. EF Core provides robust transaction management.

Explicit Transactions

You can explicitly control transactions using BeginTransaction():

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

        // Another operation
        var order = context.Orders.Find(orderId);
        order.Status = "Shipped";
        context.SaveChanges();

        transaction.Commit();
    }
    catch (Exception)
    {
        transaction.Rollback();
        throw;
    }
}

Automatic Transactions

By default, SaveChanges() wraps operations in a transaction. You can configure this behavior.

Concurrency Control

Concurrency issues arise when multiple users try to modify the same data simultaneously. EF Core offers strategies to handle this.

Optimistic Concurrency

This is the most common approach. You add a versioning token (e.g., a row version or timestamp) to your entity. EF Core uses this token to detect concurrent updates.

Add a property to your entity:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    [Timestamp]
    public byte[] RowVersion { get; set; } // Or use an int for row number
}

When SaveChanges() is called and a concurrency conflict is detected, an DbUpdateConcurrencyException is thrown. You can catch this exception and implement retry logic or prompt the user.

Performance Tuning

Optimizing EF Core performance is key for scalable applications.

Lazy Loading vs. Eager Loading

Understand the implications of lazy loading (default for navigation properties) and eager loading (using Include() or ThenInclude()) on query performance.

// Eager loading
            var products = context.Products.Include(p => p.Category).ToList();

Efficient Queries

Database Provider Optimizations

Each database provider (SQL Server, PostgreSQL, etc.) has specific optimizations. Familiarize yourself with them.

Complex Queries and Projections

Leverage LINQ to build sophisticated queries that translate efficiently to SQL.

Projections with `Select()`

Create anonymous types or DTOs directly in your queries to fetch only the required fields.

var productSummaries = context.Products
                .Where(p => p.Price > 100)
                .Select(p => new { p.Name, p.Price, CategoryName = p.Category.Name })
                .ToList();

Working with Raw SQL

For performance-critical or complex scenarios not easily expressed in LINQ, you can execute raw SQL.

var products = context.Products.FromSqlRaw("SELECT * FROM Products WHERE Price > {0}", 50).ToList();

Advanced Change Tracking

EF Core's change tracker is powerful but can be optimized.

Detaching Entities

Sometimes, you may want to stop tracking an entity to prevent unintended updates.

context.Entry(myEntity).State = EntityState.Detached;

State Enum

Understand the different states an entity can be in: Added, Modified, Deleted, Unchanged, Detached.

Shadow Properties

Shadow properties are properties that are not defined on your entity class but are mapped to the database. This is often used for metadata like audit fields.

Configure a shadow property in OnModelCreating:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>().Property<DateTime>("LastModified");
}

Accessing a shadow property:

var lastModified = context.Entry(myEntity).Property<DateTime>("LastModified").CurrentValue;

Owned Types

Owned types (formerly complex types) allow you to model types that don't have their own identity and are owned by another entity. This is useful for modeling value objects or structured data that belongs to a single entity.

Defining an Owned Type

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
}

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address ShippingAddress { get; set; }
}

Configuration

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Customer>().OwnsOne(c => c.ShippingAddress);
}

By default, owned types are mapped to the same table as the owner entity, with columns prefixed by the navigation property name (e.g., ShippingAddress_Street).

Next Steps

Continue to the EF Core Performance module to further optimize your data access layer.