Mastering Performance, Design Patterns, and Advanced Features
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.
EF Core Migrations are a powerful tool for evolving your database schema. Beyond basic creation, we'll explore:
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.
Ensuring data integrity is crucial. EF Core provides robust transaction management.
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;
}
}
By default, SaveChanges() wraps operations in a transaction. You can configure this behavior.
Concurrency issues arise when multiple users try to modify the same data simultaneously. EF Core offers strategies to handle this.
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.
Optimizing EF Core performance is key for scalable applications.
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();
Select().Each database provider (SQL Server, PostgreSQL, etc.) has specific optimizations. Familiarize yourself with them.
Leverage LINQ to build sophisticated queries that translate efficiently to SQL.
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();
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();
EF Core's change tracker is powerful but can be optimized.
Sometimes, you may want to stop tracking an entity to prevent unintended updates.
context.Entry(myEntity).State = EntityState.Detached;
Understand the different states an entity can be in: Added, Modified, Deleted, Unchanged, Detached.
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 (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.
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; }
}
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).
Continue to the EF Core Performance module to further optimize your data access layer.