dotnet-starter-kit EF Core 迁移实战指南:集中式 Migrations 项目、DbMigrator 应用与故障排查手册
【免费下载链接】dotnet-starter-kitProduction Grade Cloud-Ready .NET 10 Starter Kit (Web API + React Client) with Multitenancy Support, and Clean/Modular Architecture that saves roughly 200+ Development Hours! All Batteries Included.项目地址: https://gitcode.com/GitHub_Trending/do/dotnet-starter-kit
导读
本指南围绕 FSH(FullStackHero)dotnet-starter-kit 仓库中面向开发流程的migration-helper工作流文档展开,系统讲解这套生产级 .NET 10 Starter Kit 中 EF Core 迁移的管理方式:全部迁移集中在src/Host/FSH.Starter.Migrations.PostgreSQL一个项目中、按模块/上下文分目录存放;数据库不在 API 启动时迁移,而是由独立的FSH.Starter.DbMigrator宿主进程统一执行,并以 Postgres advisory lock 串行化多租户迁移。读完本文,你将掌握从dotnet tool restore到dotnet ef migrations add、再到DbMigrator apply的完整增删改查迁移闭环,理解快照(snapshot)陷阱、上下文命名、迁移命名规范与各类高频故障的排查方法。
一、核心事实:迁移工程的组织方式(先读再动手)
在使用任何迁移命令之前,需要先理解这套 Starter Kit 与"每个项目自带迁移"的常规做法截然不同的三点设计:
- 所有迁移都集中在一个项目里:
src/Host/FSH.Starter.Migrations.PostgreSQL(项目文件)。它通过 ProjectReference 引用各个模块的运行时项目,例如Modules.Auditing、Modules.Identity、Modules.Multitenancy、Modules.Webhooks、Modules.Billing、Modules.Catalog、Modules.Tickets、Modules.Chat、Modules.Notifications、Modules.Files,因此dotnet ef可以解析到全部模块的 DbContext。 - 按模块/上下文分目录存放:目录名与上下文一一对应,每个目录内既有按时间戳命名的迁移文件对(
{时间戳}_{名称}.cs与对应的.Designer.cs),也有各自独立的{X}DbContextModelSnapshot.cs快照。从仓库现状可以确认的目录与上下文对应关系如下:Audit/→AuditDbContextBilling/→BillingDbContextCatalog/→CatalogDbContextChat/→ChatDbContextEventing/→EventingDbContextFiles/→FilesDbContextIdentity/→IdentityDbContextMultiTenancy/→TenantDbContext(租户目录,注意不是MultitenancyDbContext)Notifications/→NotificationsDbContextTickets/→TicketsDbContextWebhooks/→WebhookDbContext
- 启动项目是 API 宿主:执行
dotnet ef相关命令时,必须显式传--project(迁移项目)、--startup-project(API 宿主)、--context {X}DbContext、--output-dir {X}四件套。
真实的上下文名称清单
文档明确给出了全部真实上下文名,务必原样使用:
IdentityDbContext、TenantDbContext(租户目录——不是 "MultitenancyDbContext")、AuditDbContext、BillingDbContext、CatalogDbContext、TicketsDbContext、FilesDbContext、ChatDbContext、NotificationsDbContext、WebhookDbContext。
注意WebhookDbContext是单数 "Webhook"(不带 s),而FilesDbContext、TicketsDbContext是复数,拼错会导致 "No DbContext was found"。
工具版本锁定
dotnet-ef被固定在 .config/dotnet-tools.json 中,版本为10.0.2(rollForward: false,不自动向前滚动)。首次使用前必须先执行:
dotnet tool restore同文件还锁定了fullstackhero.cli(10.0.0-rc.1),说明仓库对脚手架类 CLI 同样采用版本锁定策略。
关键认知:API 启动时不迁移数据库
这是最容易踩坑的认知点——数据库不会在 API 启动时被自动迁移。UseHeroMultiTenantDatabases()只负责注册 Finbuckle 的租户解析机制,它不做任何迁移动作。真正执行迁移的是DbMigrator宿主(见下文第三节)。如果你改了实体却只重启 API 而不跑迁移,新字段永远不会出现在数据库里。
二、规范化流程:create-migration 技能(唯一标准配方)
migration-helper明确声明:新增/应用迁移的唯一标准配方是仓库内的create-migration技能,即 .agents/skills/create-migration/SKILL.md。migration-helper本身负责补充"周边事实与故障排查"。完整的四步流程如下。
Step 0 — 恢复锁定工具(首次)
dotnet tool restore # dotnet-ef 锁定在 .config/dotnet-tools.jsonStep 1 — 先构建(快照陷阱)
dotnet ef migrations add读取的是当前快照(snapshot),而快照是从一次构建重新生成的。如果你在修改实体/EF 配置后跳过构建直接生成迁移,就会基于过期的快照生成,导致修改静默丢失。此外,migrations remove会重写快照,因此只能移除最新的迁移,且移除后必须重新构建。
dotnet build src/FSH.Starter.slnxStep 2 — 添加迁移
三个参数缺一不可:--project(迁移项目)、--startup-project(API 宿主)、--context {X}DbContext;再加--output-dir {X}确保迁移落在该上下文对应的模块目录(与现有目录保持一致):
dotnet ef migrations add {MigrationName} \ --project src/Host/FSH.Starter.Migrations.PostgreSQL \ --startup-project src/Host/FSH.Starter.Api \ --context {X}DbContext \ --output-dir {X}关于dotnet ef为何能对BaseDbContext正常工作,技能文档补充说明:因为 4 参数构造函数可以由启动宿主的 DI 满足。仓库中 BaseDbContext 正是这套多租户持久化体系的基础。
Step 3 — 审查生成的 SQL(应用前必做)
dotnet ef migrations script --idempotent \ --project src/Host/FSH.Starter.Migrations.PostgreSQL \ --startup-project src/Host/FSH.Starter.Api \ --context {X}DbContext需要重点扫描的风险点(详见第三节的"迁移审查清单"):意外的删表/删列、向已有表添加无默认值的非空列、以及重命名被实现成 drop+add(造成数据丢失)。必要时手工修改迁移文件或调整模型。
Step 4 — 应用
优先使用规范化路径(会依次迁移租户目录与每个租户的模块 schema):
dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply dotnet run --project src/Host/FSH.Starter.DbMigrator -- list-pending # 先预览本地单上下文的开发场景,也可以直接用dotnet ef database update --context {X}DbContext --project … --startup-project …。
命名规范
文档给出明确的迁移命名约定:
Add{Entity}—— 新增实体(如AddCategories、AddProducts)Add{Property}To{Entity}—— 给已有实体加属性Create{Index}Index—— 新增索引Rename{Old}To{New}—— 重命名
从仓库现有的迁移文件可以看到大量实际例子,例如20260430033849_AddCategories、20260512142004_AddProductImages、20260624152035_RenameWebhookSecretHashToProtectedSecret。
新增模块的额外要求
如果新建模块,除了迁移文件外还需要:在迁移项目中新增{X}/目录,并且迁移项目要引用该模块的运行时项目(参照上文列出的 csproj ProjectReference 列表,新增模块可参考 add-module 技能)。否则dotnet ef将找不到新模块的上下文。
三、应用环节的深度原理:DbMigrator 宿主的完整执行链路
文档强调"DbMigrator宿主负责应用迁移:先迁移租户目录(TenantDbContext),再迁移每个租户各自的模块 schema,通过 Postgres advisory lock 串行化"。结合 Program.cs 源码,可以把这条链路展开为四个阶段:
Step 0 — 等待数据库就绪
PostgresMigratorLock.WaitForDatabaseAsync以指数退避(初始 1 秒,最大 10 秒,总时限 2 分钟)轮询目标数据库是否可连接,专门应对 Aspire/K8s 冷启动时 Postgres 尚未就绪的情况;若 2 分钟内不可达则抛出TimeoutException并以退出码 1 结束。若连接失败但 SQLSTATE 为3D000(数据库不存在),视为服务器可达、数据库待创建,直接放行让 EF 在首次MigrateAsync时建库。
Step 0b — 获取 advisory lock
PostgresMigratorLock.AcquireAsync使用固定 64 位键0xFE514EC0DEB1ADE4执行SELECT pg_advisory_lock(@key)。这是会话级锁:并发的 migrator 进程会在此阻塞排队;持有锁的连接被 dispose(或进程崩溃)时锁自动释放,不会产生孤儿锁。首次运行且数据库不存在时降级为无操作锁(NoopLock),后续运行再获取真实锁。完整实现见 PostgresMigratorLock.cs。
Step 1 — 迁移租户目录
在独立 scope 中取得TenantDbContext,通过GetPendingMigrationsAsync检查待迁移项;list-pending只打印清单,apply则调用MigrateAsync。首次启动时若根租户(MultitenancyConstants.Root,可参考 MultitenancyConstants.cs)不存在,会自动写入,保证后续"每个租户"遍历至少有一个租户可迭代。
Step 2 — 逐租户迁移 + 可选种子
从IMultiTenantStore<AppTenantInfo>读取全部租户(可用--tenant <id>限定单个租户),对每个租户调用ITenantService.MigrateTenantAsync。该服务的实现位于 TenantService.cs,其核心逻辑是:在每个租户的 scope 中遍历所有IDbInitializer,逐一调用initializer.MigrateAsync(cancellationToken)。这就是"每个模块各自迁移 schema"的真正机制——每个模块的 DbInitializer 各自负责本模块上下文在该租户数据库上的迁移。
退出码与失败处理
迁移成功打印[migrator] finished successfully.并返回 0;任何异常被顶层 catch 捕获,打印[migrator] FAILED: {类型}: {消息}与堆栈后返回 1,最后在 finally 中优雅停掉宿主以冲刷日志。这份契约在 MigratorCommand.cs 的帮助文本中也有明确说明。
DbMigrator 命令行参考
从 MigratorCommand.cs 的解析器与帮助文本可提取完整语法:
dotnet run --project src/Host/FSH.Starter.DbMigrator -- [verb] [options]| 动词 | 说明 |
|---|---|
apply | 应用待处理迁移(默认动词)。加--seed同时执行 SeedAsync |
seed | 仅对每个租户执行 SeedAsync 步骤 |
seed-demo | 预置演示租户(acme、globex)的用户、目录、工单与聊天内容。仅限开发环境——除非DOTNET_ENVIRONMENT=Development,否则拒绝执行并以退出码 1 结束 |
list-pending | 只打印待处理迁移,不应用任何东西 |
| 选项 | 说明 |
|---|---|
--tenant <id> | 只作用于单个租户 id(默认全部租户) |
--catalog-only | 跳过逐租户环节,只迁移租户目录 |
--seed | apply 之后调用ITenantService.SeedTenantAsync |
-h, --help | 打印帮助文本 |
设计取舍:为什么不用 API 启动迁移
Program.cs 顶部的注释点明了关键理由:DbMigrator 作为部署步骤运行,因此可以使用具备 DDL 权限的高权限连接串;API 运行时则使用低权限连接串,天然满足最小权限原则。同时 migrator 关闭了 OpenTelemetry、CORS、OpenAPI、Jobs、Mailing、SSE、Realtime、Quotas、FeatureFlags、Idempotency 等运行时能力,只保留 Persistence、Multitenancy 与 Caching(部分模块构造依赖IDistributedCache,无 Redis 时回退内存实现),并在启动前剥离所有BackgroundService,避免它们在表创建之前抢先读写而触发42P01错误。这些处理在 Program.cs 中有完整注释说明。
一个容易忽视的细节
EventingDbContext也随逐租户循环创建 schema(源码注释引用了 issue #1349):AddEventingCore注册了 EventingDbContext 及其 IDbInitializer,因此框架的 outbox/inbox 表会与每个模块的 schema 一同创建。
四、迁移审查清单:应用前必须检查的三类数据风险
migration-helper要求生成迁移脚本后扫描以下风险点,这也是生产环境最常见的三类"悄悄丢数据"事故源头:
- 意外的删表/删列:确认
Down()与Up()都要看,尤其是那些你没有主动删除却被 EF 推断为删除的对象(常见于重命名实体、改导航属性后 EF 认为是删除重建)。 - 向已有表添加无默认值的非空列:对已有数据的表,这会直接导致插入/迁移失败。要么给默认值,要么拆成两步(先加可空列、回填、再改非空)。
- 重命名被实现成 drop+add:EF 无法自动推断重命名,往往会生成"删旧列 + 加新列",存量数据全部丢失。审查时若发现此类脚本,应手工修改为
RENAME COLUMN。
一个真实教训就在仓库里:Webhooks模块的20260624152035_RenameWebhookSecretHashToProtectedSecret,它对应的是对已有列的重命名迁移,正是这种审查场景的样板。
五、故障排查速查表
文档给出的排查表是这篇工作流文档的精华,完整继承如下:
| 症状 | 原因 → 修复 |
|---|---|
| "No DbContext was found" / 多个上下文 | 始终显式传--context {X}DbContext |
| "Build failed" | 先执行dotnet build src/FSH.Starter.slnx |
| 迁移落到了错误目录 | 加--output-dir {X}(与上下文现有目录保持一致) |
| 迁移中缺少变更 | 你在migrations add前没有构建(快照过期) |
| ef 找不到新模块的上下文 | 迁移项目必须引用该模块的运行时项目 |
结合 database.md 规则 还可补充两条与本主题相关的操作纪律:
migrations remove作用于快照:先完整构建再做migrations add,否则可能丢失之前的迁移。- 多租户隔离默认开启:
BaseDbContext会对每个实体自动应用租户查询过滤器;仅在实现IGlobalEntity的实体(如BillingPlan、ImpersonationGrant、Outbox/InboxMessage)上豁免。子类 DbContext 若重写OnModelCreating,必须在最后调用base.OnModelCreating(modelBuilder),否则自动过滤器会丢失——这条同样影响迁移生成时的模型快照。
六、回到文档定位:migration-helper 与 create-migration 的分工
最后澄清一下这份工作流文档在仓库中的定位,方便你后续直接复用:
- create-migration 技能:持有权威的增/审/应用配方(上文第二节的四步流程与结尾 Checklist),是执行时的唯一标准。
- migration-helper 工作流:本文主题文档,负责补充周边事实与故障排查——即第一节的事实清单、第三节的 DbMigrator 原理、第四节的审查要点和第五节的排查表。
- database.md 规则:定义实体基类、租户隔离、AsNoTracking、导航集合子实体值生成等 EF 约定,是触碰实体与 DbContext 前的必读。
三者配合的完整闭环是:改实体 →dotnet build→dotnet ef migrations add(四件套参数)→ 脚本审查 →DbMigrator list-pending预览 →DbMigrator apply应用 → 必要时dotnet tool restore复位锁定工具。这套流程既避免了"API 启动自动迁移"在生产环境的高权限风险,也通过集中式迁移项目 + 每模块独立快照 + advisory lock 串行化,让多租户、多模块的大规模 schema 演进保持可控与可回滚。
【免费下载链接】dotnet-starter-kitProduction Grade Cloud-Ready .NET 10 Starter Kit (Web API + React Client) with Multitenancy Support, and Clean/Modular Architecture that saves roughly 200+ Development Hours! All Batteries Included.项目地址: https://gitcode.com/GitHub_Trending/do/dotnet-starter-kit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考