SuperAPI AI 工具集

efcore-patterns

编程与开发

查看项目

更新于 10-15 10:26

技能介绍

Entity Framework Core Patterns 是一套面向 .NET 开发者的 EF Core 最佳实践技能,帮助在项目中正确配置和使用 Entity Framework Core。

适用场景

在以下情况使用本技能:在新项目中配置 EF Core;优化查询性能;管理数据库迁移;将 EF Core 与 .NET Aspire 集成;调试变更跟踪问题;高效加载多个导航集合(查询拆分)。

核心原则

  • 默认 NoTracking:大多数查询是只读的,跟踪应显式选择加入。
  • 绝不手动编辑迁移:始终使用 CLI 命令。
  • 专用迁移服务:将迁移执行与应用启动分离。
  • 使用 ExecutionStrategy 进行重试:处理瞬时数据库故障。
  • 显式更新:在 NoTracking 下,显式将实体标记为更新。

模式 1:默认 NoTracking

在 DbContext 中配置默认禁用变更跟踪,以提升读密集型工作负载的性能。在构造函数中设置 ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking。

NoTracking 生效时:只读查询正常工作;写入需要显式处理。错误做法是查询实体后直接修改属性并调用 SaveChangesAsync,此时不会发生任何事。正确做法是调用 dbContext.Orders.Update(order) 显式标记整个实体为已修改,或在查询时使用 .AsTracking()。

何时使用跟踪:在 UI 中显示数据(否)、API GET 端点(否)、更新单个实体(是或显式 Update())、带导航的复杂更新(是)、批量操作(否 + ExecuteUpdate)。

显式 Add/Update 模式:创建时始终使用 Add;更新时显式标记为已修改;删除时附加并移除。

模式 2:绝不手动编辑迁移

关键:始终使用 EF Core CLI 命令管理迁移。绝不手动编辑迁移文件(除在 Up()/Down() 中自定义 SQL 外)、直接删除迁移文件、重命名迁移文件或在项目间复制迁移。

创建迁移:dotnet ef migrations add AddCustomerTable --project src/MyApp.Infrastructure --startup-project src/MyApp.Api。如有多个 DbContext,可加 --context ApplicationDbContext。

移除迁移: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。可指定目标迁移或回滚到之前的迁移。

生成 SQL 脚本:dotnet ef migrations script --project src/MyApp.Infrastructure --startup-project src/MyApp.Api --output migrations.sql;使用 --idempotent 生成可多次安全运行的幂等脚本。

模式 3:配合 Aspire 的专用迁移服务

使用专用迁移服务将迁移执行与主应用分离,确保迁移在应用启动前完成、关注点清晰分离、在测试环境中可控地播种数据。

项目结构示例:MyApp.AppHost(Aspire 编排)、MyApp.Api(主应用)、MyApp.Infrastructure(DbContext 与迁移)、MyApp.MigrationService(专用迁移运行器)。

MigrationService 的 Program.cs 中通过 Host.CreateApplicationBuilder 构建,添加 Aspire 服务默认值,注册 PostgreSQL DbContext(指定 MigrationsAssembly),并注册 MigrationWorker 作为托管服务。

MigrationWorker 继承 BackgroundService,在 ExecuteAsync 中创建作用域、获取 DbContext、调用 RunMigrationsAsync,完成后通过 IHostApplicationLifetime.StopApplication() 停止应用。RunMigrationsAsync 使用 dbContext.Database.CreateExecutionStrategy() 处理瞬时故障,检查待处理迁移并调用 MigrateAsync。

AppHost 配置中,先添加 PostgreSQL 与数据库,再添加迁移服务项目并 WaitFor(db) 与 WithReference(db),API 项目使用 WaitForCompletion(migrations) 等待迁移完成后再启动。

模式 4:用于瞬时故障的 ExecutionStrategy

对可能瞬时失败的操作始终使用 CreateExecutionStrategy()。注意:不能将 CreateExecutionStrategy() 与用户发起的事务一起使用。若需要带重试的事务,事务必须位于策略回调内部:在 strategy.ExecuteAsync 内调用 BeginTransactionAsync,成功则 CommitAsync,失败则 RollbackAsync 并重新抛出。

模式 5:使用 ExecuteUpdate/ExecuteDelete 进行批量操作

对于批量操作,使用 EF Core 7+ 的 ExecuteUpdateAsync 和 ExecuteDeleteAsync,而不是加载实体。加载所有实体再逐个修改会较慢;ExecuteUpdateAsync 生成单条 SQL UPDATE 语句,ExecuteDeleteAsync 生成单条 SQL DELETE 语句。

常见陷阱

  1. NoTracking 下忘记更新:查询实体后修改属性并 SaveChangesAsync 会静默失败,需显式调用 Update。
  2. N+1 查询问题:循环中访问导航属性会触发逐条查询,应使用 Include 预加载。
  3. 多个 DbContext 实例的跟踪冲突:同一实体被不同上下文跟踪可能导致异常,应使用单一上下文或先分离。
  4. 未一致使用异步:在异步上下文中使用同步 ToList() 会阻塞线程,应全程使用 ToListAsync()。
  5. 在循环内查询:逐次 FindAsync 应改为单次 Where(o => orderIds.Contains(o.Id)) 查询。

DI 中的 DbContext 生命周期

ASP.NET Core 默认 Scoped,即每个 HTTP 请求一个实例。后台服务中应为每个工作单元创建作用域。Actor 或长生命周期对象应使用工厂模式:注册 AddDbContextFactory,在每次操作时通过 IDbContextFactory.CreateDbContextAsync() 创建新上下文。

模式 6:查询拆分以防止笛卡尔积爆炸

当通过 Include() 加载多个导航集合时,EF Core 生成单条查询可能导致笛卡尔积爆炸。例如 10 个订单各含 10 个条目会得到 100 行,而非 10 + 10。

全局配置(多数情况推荐):在 DbContext 配置中通过 UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) 启用查询拆分。

按查询覆盖:在结构明确更高效时使用 .AsSingleQuery() 覆盖全局拆分行为。

权衡:SplitQuery 无笛卡尔积爆炸、适合大集合,但有多轮往返和潜在一致性问题的缺点;SingleQuery 单次往返、事务一致,但多个集合时会有笛卡尔积爆炸。建议全局默认 SplitQuery,对已知更优的特定查询用 AsSingleQuery() 覆盖。

何时优先 SingleQuery:小型且结构清晰的导航图(2-3 层)等场景。