关于 efcore-patterns
efcore-patterns 是面向 .NET 开发者的 Entity Framework Core 技能,汇集查询、迁移、可靠性和性能方面的实践建议。
主要功能
- 01默认无跟踪查询建议
- 02使用拆分查询加载集合
- 03基于 CLI 的迁移实践
- 04重试策略与拦截器
- 05批量操作与编译查询
适用场景
- 01在新项目中配置 EF Core
- 02优化含多个集合的查询
- 03规划生产环境数据库迁移
- 04排查更改跟踪问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill efcore-patterns安装后在 Claude Code 中输入/efcore-patterns确认已加载。
详细介绍
Entity Framework Core 模式
何时使用此技能
在以下情况下使用此技能:
- 在新项目中设置 EF Core
- 优化查询性能
- 管理数据库迁移
- 将 EF Core 与 .NET Aspire 集成
- 调试更改跟踪问题
- 高效加载多个导航集合(查询拆分)
核心原则
- 默认使用 NoTracking — 大多数查询都是只读的;需要跟踪时再启用
- 切勿手动编辑迁移 — 始终使用 CLI 命令
- 专用迁移服务 — 将迁移执行与应用程序启动分离
- 使用 ExecutionStrategy 进行重试 — 处理数据库瞬时故障
- 显式更新 — 使用 NoTracking 时,显式将实体标记为待更新
DbContext 生命周期
DbContext 是一个工作单元,应当是短生命周期的。在 ASP.NET Core 中,应将其注册为作用域(每个请求一个):
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
生命周期规则
| 场景 | 生命周期 | 注册方式 |
|---|---|---|
| Web API / MVC 请求 | 作用域(默认) | AddDbContext<T>() |
| 后台服务 | 通过工厂创建作用域 | AddDbContextFactory<T>() |
| Blazor Server | 通过工厂创建作用域 | AddDbContextFactory<T>() |
| 控制台应用 | 瞬态或手动创建 | new AppDbContext(options) |
面向长生命周期服务的 DbContextFactory
后台服务和 Blazor Server 电路的生命周期会超过单个作用域。请使用 IDbContextFactory<T>:
public sealed class OrderProcessor(IDbContextFactory<AppDbContext> contextFactory)
{
public async Task ProcessBatchAsync(CancellationToken ct)
{
await using var db = await contextFactory.CreateDbContextAsync(ct);
var pending = await db.Orders
.Where(o => o.Status == OrderStatus.Pending)
.ToListAsync(ct);
foreach (var order in pending)
{
order.Status = OrderStatus.Processing;
}
await db.SaveChangesAsync(ct);
}
}
builder.Services.AddDbContextFactory<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
池化
AddDbContextPool<T>() 会重用 DbContext 实例,以减少分配开销:
builder.Services.AddDbContextPool<AppDbContext>(options =>
options.UseNpgsql(connectionString), poolSize: 128);
池化限制: 不要在 DbContext 子类中存储每个请求专属的状态。不要在构造函数中注入作用域服务。
模式 1:默认使用 NoTracking
配置 DbContext,默认禁用更改跟踪:
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
public DbSet<Order> Orders => Set<Order>();
public DbSet<Customer> Customers => Set<Customer>();
}
NoTracking 启用时
只读查询可正常使用:
var orders = await dbContext.Orders
.Where(o => o.Status == OrderStatus.Pending)
.ToListAsync();
写入需要显式处理:
// WRONG - Entity not tracked, SaveChanges does nothing
var order = await dbContext.Orders.FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
await dbContext.SaveChangesAsync(); // Nothing happens!
// CORRECT - Explicitly mark entity for update
var order = await dbContext.Orders.FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
dbContext.Orders.Update(order);
await dbContext.SaveChangesAsync();
// ALSO CORRECT - Use AsTracking() for the query
var order = await dbContext.Orders
.AsTracking()
.FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
await dbContext.SaveChangesAsync();
何时使用跟踪
| 场景 | 使用跟踪? | 原因 |
|---|---|---|
| 在 UI 中显示数据 | 否 | 只读,无需更新 |
| API GET 端点 | 否 | 返回数据,不进行修改 |
| 更新单个实体 | 是或显式调用 Update() | 需要保存更改 |
| 使用导航属性进行复杂更新 | 是 | 跟踪会处理关系 |
| 批量操作 | 否 + ExecuteUpdate | 效率更高 |
针对单个查询禁用跟踪
var orders = await db.Orders
.AsNoTracking()
.Where(o => o.CustomerId == customerId)
.ToListAsync(ct);
var ordersWithItems = await db.Orders
.AsNoTrackingWithIdentityResolution()
.Include(o => o.Items)
.Where(o => o.Status == OrderStatus.Active)
.ToListAsync(ct);
模式 2:通过查询拆分防止笛卡尔积膨胀
使用 Include() 加载多个导航集合时,EF Core 会生成一条可能导致笛卡尔积膨胀的查询。
问题
// Single query: produces Cartesian product of OrderItems x Payments
var orders = await db.Orders
.Include(o => o.Items) // N items
.Include(o => o.Payments) // M payments
.ToListAsync(ct);
// Result set: N x M rows per order
解决方案
var orders = await db.Orders
.Include(o => o.Items)
.Include(o => o.Payments)
.AsSplitQuery()
.ToListAsync(ct);
// Executes 3 separate queries: Orders, Items, Payments
全局默认设置
options.UseNpgsql(connectionString, npgsql =>
npgsql.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
权衡
| 方式 | 优点 | 缺点 |
|---|---|---|
| 单条查询(默认) | 原子快照,只需一次往返 | 多个 Include 会导致笛卡尔积膨胀 |
| 拆分查询 | 不会出现笛卡尔积膨胀,传输的数据更少 | 多次往返,不保证原子性 |
经验法则: 包含两个或更多集合导航属性时,使用 AsSplitQuery()。
模式 3:迁移管理
重要: 始终使用 EF Core CLI 命令管理迁移。切勿手动编辑迁移文件(在 Up()/Down() 中编写自定义 SQL 除外)。
创建迁移
dotnet ef migrations add AddCustomerTable \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api
dotnet ef migrations add AddCustomerTable \
--context ApplicationDbContext \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api
删除迁移
dotnet ef migrations remove \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api
应用迁移
dotnet ef database update \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api
dotnet ef database update AddCustomerTable \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api
dotnet ef database update PreviousMigrationName \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api
生成 SQL 脚本
dotnet ef migrations script \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api \
--output migrations.sql
dotnet ef migrations script \
--idempotent \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api
用于生产环境的迁移捆绑包
dotnet ef migrations bundle \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api \
--output efbundle \
--self-contained
./efbundle --connection "Host=prod-db;Database=myapp;Username=deploy;Password=..."
迁移最佳实践
- 始终为生产环境生成幂等脚本(使用
--idempotent标志) - 切勿在生产环境的启动过程中调用
Database.Migrate()-- 使用迁移捆绑包或脚本 - 保持迁移为增量式 -- 添加带默认值的列、添加表、添加索引
- 审查生成的代码 -- EF Core 脚手架生成的 SQL 可能与预期不同
- 使用独立的迁移项目 -- 将迁移保留在基础设施项目中
数据播种
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<OrderStatus>().HasData(
new OrderStatus { Id = 1, Name = "Pending" },
new OrderStatus { Id = 2, Name = "Processing" },
new OrderStatus { Id = 3, Name = "Completed" },
new OrderStatus { Id = 4, Name = "Cancelled" });
}
模式 4:使用 Aspire 的专用迁移服务
将迁移执行与主应用分离。
项目结构
src/
├── MyApp.AppHost/ # Aspire orchestration
├── MyApp.Api/ # Main application
├── MyApp.Infrastructure/ # DbContext and migrations
└── MyApp.MigrationService/ # Dedicated migration runner
MigrationWorker.cs
public class MigrationWorker : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly ILogger<MigrationWorker> _logger;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Migration service starting...");
try
{
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await RunMigrationsAsync(dbContext, stoppingToken);
_logger.LogInformation("Migration service completed successfully.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Migration service failed: {Error}", ex.Message);
throw;
}
finally
{
_hostApplicationLifetime.StopApplication();
}
}
private async Task RunMigrationsAsync(ApplicationDbContext dbContext, CancellationToken ct)
{
var strategy = dbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
var pendingMigrations = await dbContext.Database.GetPendingMigrationsAsync(ct);
if (pendingMigrations.Any())
{
_logger.LogInformation("Applying {Count} pending migrations...",
pendingMigrations.Count());
await dbContext.Database.MigrateAsync(ct);
_logger.LogInformation("Migrations applied successfully.");
}
else
{
_logger.LogInformation("No pending migrations. Database is up to date.");
}
});
}
}
AppHost 配置
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres");
var db = postgres.AddDatabase("appdb");
var migrations = builder.AddProject<Projects.MyApp_MigrationService>("migrations")
.WaitFor(db)
.WithReference(db);
var api = builder.AddProject<Projects.MyApp_Api>("api")
.WaitForCompletion(migrations)
.WithReference(db);
模式 5:用于处理暂时性故障的 ExecutionStrategy
对于可能发生暂时性故障的操作,始终使用 CreateExecutionStrategy():
public async Task UpdateWithRetryAsync(Guid id, Action<Order> update)
{
var strategy = _dbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
var order = await _dbContext.Orders
.AsTracking()
.FirstOrDefaultAsync(o => o.Id == id);
if (order is null) return;
update(order);
await _dbContext.SaveChangesAsync();
});
}
使用事务
var strategy = _dbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
await using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
});
提供程序特定配置
// PostgreSQL
options.UseNpgsql(connectionString, npgsql =>
npgsql.EnableRetryOnFailure(
maxRetryCount: 3,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorCodesToAdd: null));
// SQL Server
options.UseSqlServer(connectionString, sqlServer =>
sqlServer.EnableRetryOnFailure(
maxRetryCount: 3,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorNumbersToAdd: null));
模式 6:拦截器
EF Core 拦截器可以在不修改实体逻辑的情况下处理横切关注点。
审计时间戳拦截器
public sealed class AuditTimestampInterceptor : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken ct = default)
{
if (eventData.Context is null)
return ValueTask.FromResult(result);
var now = DateTimeOffset.UtcNow;
foreach (var entry in eventData.Context.ChangeTracker.Entries<IAuditable>())
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedAt = now;
entry.Entity.UpdatedAt = now;
break;
case EntityState.Modified:
entry.Entity.UpdatedAt = now;
break;
}
}
return ValueTask.FromResult(result);
}
}
public interface IAuditable
{
DateTimeOffset CreatedAt { get; set; }
DateTimeOffset UpdatedAt { get; set; }
}
软删除拦截器
public sealed class SoftDeleteInterceptor : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken ct = default)
{
if (eventData.Context is null)
return ValueTask.FromResult(result);
foreach (var entry in eventData.Context.ChangeTracker.Entries<ISoftDeletable>())
{
if (entry.State == EntityState.Deleted)
{
entry.State = EntityState.Modified;
entry.Entity.IsDeleted = true;
entry.Entity.DeletedAt = DateTimeOffset.UtcNow;
}
}
return ValueTask.FromResult(result);
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>()
.HasQueryFilter(p => !p.IsDeleted);
}
注册
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
options.UseNpgsql(connectionString)
.AddInterceptors(
sp.GetRequiredService<AuditTimestampInterceptor>(),
sp.GetRequiredService<SoftDeleteInterceptor>()));
builder.Services.AddSingleton<AuditTimestampInterceptor>();
builder.Services.AddSingleton<SoftDeleteInterceptor>();
模式 7:编译查询
对于执行频率很高的查询:
public static class CompiledQueries
{
public static readonly Func<AppDbContext, int, Task<Order?>>
GetOrderById = EF.CompileAsyncQuery(
(AppDbContext db, int orderId) =>
db.Orders
.AsNoTracking()
.Include(o => o.Items)
.FirstOrDefault(o => o.Id == orderId));
public static readonly Func<AppDbContext, string, IAsyncEnumerable<Order>>
GetOrdersByCustomer = EF.CompileAsyncQuery(
(AppDbContext db, string customerId) =>
db.Orders
.AsNoTracking()
.Where(o => o.CustomerId == customerId)
.OrderByDescending(o => o.CreatedAt));
}
var order = await CompiledQueries.GetOrderById(db, orderId);
await foreach (var o in CompiledQueries.GetOrdersByCustomer(db, customerId)
.WithCancellation(ct))
{
}
适用场景: 每秒执行数千次的查询。对于常见的 CRUD 操作,标准 LINQ 已足够。
模式 8:批量操作
使用 EF Core 7+ 的 ExecuteUpdateAsync 和 ExecuteDeleteAsync:
// WRONG - Loads all entities into memory
var expiredOrders = await _db.Orders
.Where(o => o.ExpiresAt < DateTimeOffset.UtcNow)
.ToListAsync();
foreach (var order in expiredOrders)
{
order.Status = OrderStatus.Expired;
}
await _db.SaveChangesAsync();
// CORRECT - Single SQL UPDATE statement
await _db.Orders
.Where(o => o.ExpiresAt < DateTimeOffset.UtcNow)
.ExecuteUpdateAsync(setters => setters
.SetProperty(o => o.Status, OrderStatus.Expired)
.SetProperty(o => o.UpdatedAt, DateTimeOffset.UtcNow));
await _db.Orders
.Where(o => o.Status == OrderStatus.Cancelled && o.CreatedAt < cutoffDate)
.ExecuteDeleteAsync();
常见陷阱
1. 使用 NoTracking 时忘记更新
// Silent failure - entity not tracked
var customer = await _db.Customers.FindAsync(id);
customer.Name = "New Name";
await _db.SaveChangesAsync(); // Does nothing!
// Explicit update
var customer = await _db.Customers.FindAsync(id);
customer.Name = "New Name";
_db.Customers.Update(customer);
await _db.SaveChangesAsync();
2. N+1 查询问题
// N+1 queries - one query per order
var customers = await _db.Customers.ToListAsync();
foreach (var customer in customers)
{
var orders = customer.Orders; // Lazy load triggers query
}
// Eager loading - single query
var customers = await _db.Customers
.Include(c => c.Orders)
.ToListAsync();
3. 跟踪冲突
// Tracking conflict
var order1 = await _db1.Orders.AsTracking().FindAsync(id);
var order2 = await _db2.Orders.AsTracking().FindAsync(id);
order2.Status = OrderStatus.Shipped;
await _db2.SaveChangesAsync();
// Use single context or detach
_db1.Entry(order1).State = EntityState.Detached;
4. 在循环中执行查询
// Query per iteration
foreach (var orderId in orderIds)
{
var order = await _db.Orders.FindAsync(orderId);
}
// Single query
var orders = await _db.Orders
.Where(o => orderIds.Contains(o.Id))
.ToListAsync();
使用 EF Core 进行测试
内存提供程序(仅用于单元测试)
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
using var context = new ApplicationDbContext(options);
使用 TestContainers 连接真实数据库
var container = new PostgreSqlBuilder()
.WithImage("postgres:16-alpine")
.Build();
await container.StartAsync();
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseNpgsql(container.GetConnectionString())
.Options;
Agent 常见问题
- 不要将
DbContext注入单例服务——改用IDbContextFactory<T>。 - 不要忘记传播
CancellationToken——将ct传递给所有异步 EF Core 方法。 - 不要在使用迁移的同时调用
Database.EnsureCreated()——仅在测试场景中使用。 - 不要假设多次调用
SaveChangesAsync会处于同一事务中——请使用显式事务包装。 - 不要硬编码连接字符串——从配置中读取。
- 调用
Validator.TryValidateObject时,始终传入validateAllProperties: true。