EF Core 迁移管线源码级解析:从模型差异比对到 SQL 执行的完整流程
【免费下载链接】efcoreEF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.项目地址: https://gitcode.com/GitHub_Trending/ef/efcore
本文基于 EF Core 仓库中的迁移子系统实现,完整拆解 EF Core Migrations 的两条核心管线——设计时"添加迁移"(Scaffold)与运行时"应用迁移"(Migrate),覆盖MigrationsScaffolder、MigrationsModelDiffer、MigrationsSqlGenerator、HistoryRepository、Migrator与MigrationCommandExecutor的调用关系,并深入讲解模型快照(Model Snapshot)的 property bag 格式与SnapshotModelProcessor的向后兼容修复机制。读完后,你可以准确理解dotnet ef migrations add与migrate命令背后的每个环节,知道在哪个扩展点定制 SQL 生成、在哪里定位迁移测试。
一、Migrations 子系统的两条管线
EF Core 的数据库迁移由两条职责不同的管线构成,它们共享同一套中间表示——MigrationOperation(迁移操作)列表:
添加迁移(设计时):MigrationsScaffolder.ScaffoldMigration()→MigrationsModelDiffer.GetDifferences()→ 得到MigrationOperation列表 → 由CSharpMigrationsGenerator和CSharpSnapshotGenerator产出 Up/Down 代码与模型快照代码。
应用迁移(运行时):Migrator.MigrateAsync()→ 读取__EFMigrationsHistory表确定已应用迁移 → 对每个待应用迁移调用MigrationsSqlGenerator.Generate(operations)生成命令 →MigrationCommandExecutor在事务中执行。
从源码结构看,设计时管线位于src/EFCore.Design程序集,运行时管线位于src/EFCore.Relational程序集,两者的交汇点正是操作列表:设计时"产出"操作并固化为 C# 代码,运行时"解释"操作并翻译成 SQL。
二、添加迁移:Scaffolder 如何把模型变化翻译成代码
2.1 核心入口 ScaffoldMigration
设计时的入口是 MigrationsScaffolder。其ScaffoldMigration(migrationName, rootNamespace, subNamespace, language, dryRun)方法的完整流程(见 src/EFCore.Design/Migrations/Design/MigrationsScaffolder.cs#L66-L203)可以归纳为以下几步:
- 命名与冲突检查:禁止使用保留名
migration(避免基类循环依赖),并通过MigrationsAssembly.FindMigrationId检查迁移名是否重复; - 命名空间与目录推导:计算迁移命名空间(默认子命名空间为
Migrations),并通过ContainsForeignMigrations检测当前命名空间下是否存在其他DbContext的迁移类——若存在冲突且子命名空间是默认值,会自动改名为<ContextName>Migrations子命名空间; - 模型差异比对(关键三行代码):
var modelSnapshot = Dependencies.MigrationsAssembly.ModelSnapshot; var lastModel = Dependencies.SnapshotModelProcessor.Process(modelSnapshot?.Model)?.GetRelationalModel(); var upOperations = Dependencies.MigrationsModelDiffer .GetDifferences(lastModel, Dependencies.Model.GetRelationalModel()); var downOperations = upOperations.Count > 0 ? Dependencies.MigrationsModelDiffer.GetDifferences(Dependencies.Model.GetRelationalModel(), lastModel) : [];注意这里lastModel来自快照模型,且先经过SnapshotModelProcessor.Process()做兼容性修复(第四节详述),再取关系模型(GetRelationalModel())与当前DbContext的关系模型做双向 diff:正向 diff 产生Up()操作,反向 diff 产生Down()操作; 4.破坏性变更警告:若upOperations中任一操作标记了IsDestructiveChange(如删表、删列),会输出DesignStrings.DestructiveOperation警告; 5.代码生成:通过MigrationsCodeGeneratorSelector.Select(language)选择代码生成器(默认 C#),依次调用GenerateMigration(迁移 Up/Down 代码)、GenerateMetadata(.Designer.cs元数据)、GenerateSnapshot(模型快照代码),最终封装为ScaffoldedMigration返回。
2.2 移除迁移 RemoveMigration
同一类还提供RemoveMigration(src/EFCore.Design/Migrations/Design/MigrationsScaffolder.cs#L243-L401),对应dotnet ef migrations remove命令。其逻辑值得注意:
- 只有当最后一个迁移的
TargetModel与快照模型没有差异时才允许移除(否则说明迁移已被手动改过,会提示 "Manually Deleted"); - 若该迁移已应用到数据库(通过
HistoryRepository.GetAppliedMigrations()检查),默认抛出"需先回滚"的异常;传入force: true时会调用Migrator.Migrate(previousMigrationId)先回滚数据库,再删除迁移文件、元数据文件; - 快照文件要么被删除(移除的是第一个迁移),要么用前一个迁移的
TargetModel重新生成并回写。
2.3 代码生成器 CSharpMigrationsGenerator / CSharpSnapshotGenerator
| 类型 | 职责 | 源码位置 |
|---|---|---|
CSharpMigrationsGenerator | 将操作列表编译为迁移类的Up()/Down()代码(builder.CreateTable、migrationBuilder.Sql等 Builder 调用) | CSharpMigrationsGenerator |
CSharpMigrationOperationGenerator | 将单个MigrationOperation翻译为一行 Builder 代码 | CSharpMigrationOperationGenerator |
CSharpSnapshotGenerator | 生成模型快照类(property bag 形式) | CSharpSnapshotGenerator |
MigrationsCodeGeneratorSelector | 按项目语言(C#/F# 等)选择生成器 | MigrationsCodeGeneratorSelector 接口 |
MigrationsCodeGenerator.GenerateMigration是抽象方法,C# 实现里对每个操作调用对应的ICSharpMigrationOperationGenerator方法,把操作对象渲染为builder.CreateColumn(...)之类的语句;GenerateSnapshot则渲染出快照类(MigrationsCodeGenerator基类位于 MigrationsCodeGenerator)。
三、应用迁移:Migrator 的执行流程
3.1 Migrator 主流程
运行时管线的核心是 Migrator(Migrate/MigrateAsync均委托到MigrateImplementation),其执行序列为:
- 数据库与连接准备:若数据库不存在则通过
IRelationalDatabaseCreator.Create()创建,随后打开连接; - 确保历史表存在:
_historyRepository.CreateIfNotExists(),通过执行策略(IExecutionStrategy)包裹,保证在可重试的连接场景下正确重入; - 事务与数据库锁:在
MigrateImplementation中开启事务(MigrationTransactionIsolationLevel默认null,由 provider 可覆盖),并调用HistoryRepository.AcquireDatabaseLock()获取数据库级锁,防止并发迁移——LockReleaseBehavior(LockReleaseBehavior)决定锁在事务提交还是命令执行时释放; - 计算待执行迁移:
PopulateMigrations用HistoryRepository.GetAppliedMigrations()返回的已应用列表与目标迁移做差集,确定要应用的迁移集合及其顺序; - 逐迁移执行命令列表:每个迁移的命令由
MigrationsSqlGenerator生成后交给MigrationCommandExecutor.ExecuteNonQuery执行,执行状态记录在MigrationExecutionState(当前迁移 ID、已提交命令索引、是否完成种子数据),支持失败重试时从断点续跑; - 种子数据:若
CoreOptionsExtension.Seeder已注册且尚未执行,则在全部迁移完成后调用; - 回滚:目标迁移低于当前状态时,管线反向生成
Down()命令列表并执行。
Migrator的构造函数聚合了运行时管线所需的全部服务:IMigrationsAssembly、IHistoryRepository、IMigrationsSqlGenerator、IRawSqlCommandBuilder、IMigrationCommandExecutor、IMigrationsModelDiffer、IDesignTimeModel等(见 src/EFCore.Relational/Migrations/Internal/Migrator.cs#L42-L78),这也是自定义 provider 迁移行为时的主要服务注册点。
3.2 MigrationsSqlGenerator:操作到 SQL 的分发
基类 MigrationsSqlGenerator 的Generate(operations, model, options)方法通过一张静态分发表GenerateActions(src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs#L30-L66)把每种操作类型路由到对应的Generate(...)重载,由 MigrationCommandListBuilder 收集为MigrationCommand列表。分发表覆盖了全部内置操作类型:CreateTableOperation、AddColumnOperation、DropForeignKeyOperation、AlterColumnOperation、CreateIndexOperation、CreateSequenceOperation、EnsureSchemaOperation、RenameTableOperation、SqlOperation、InsertDataOperation/UpdateDataOperation/DeleteDataOperation等。
所有迁移操作定义在 src/EFCore.Relational/Migrations/Operations/,均继承自 MigrationOperation。这是设计时 diff 结果与运行时 SQL 生成之间的稳定中间层,provider 定制 SQL 时通常继承MigrationsSqlGenerator并覆盖对应操作的生成逻辑。
3.3 历史表 __EFMigrationsHistory
迁移状态追踪依赖__EFMigrationsHistory表。基类 HistoryRepository 中定义了常量:
public const string DefaultTableName = "__EFMigrationsHistory";表包含MigrationId(主键)与ProductVersion两列,各 provider 通过派生类(如SqlServerHistoryRepository、SqliteHistoryRepository)生成方言化的建表/读写 SQL。从功能测试可以直观看到该表在真实数据库中的形态——SQL Server 版本(test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsInfrastructureSqlServerTest.cs):
IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL BEGIN CREATE TABLE [__EFMigrationsHistory] ( [MigrationId] NVARCHAR(150) NOT NULL, [ProductVersion] NVARCHAR(32) NOT NULL, CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId]) ); END INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) VALUES (N'20240101000000_InitialCreate', N'9.0.0');SQLite 版本见 test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsInfrastructureSqliteTest.cs。单元测试中还有 schema 变体([my].[__EFMigrationsHistory])与CreateIfNotExists幂等行为的断言,见 test/EFCore.SqlServer.Tests/Migrations/SqlServerHistoryRepositoryTest.cs。
四、模型快照:property bag 格式与向后兼容
4.1 快照为什么是 Dictionary 而非实体类型
模型快照文件(<ContextName>ModelSnapshot.cs)中每个实体类型声明为:
b.Entity("typeof(Dictionary<string, object>)")即快照使用typeof(Dictionary<string, object>)(property bag 格式)而非真实 CLR 实体类型。这意味着:查看快照中的ClrType时不能假设它对应真实的实体类型——快照的职责是完整保存"关系模型状态"以供下次 diff,而不是描述领域模型。这一设计让快照与实体类的重命名、命名空间调整解耦,只关心数据库结构本身。
4.2 SnapshotModelProcessor:旧快照的修复器
[SnapshotModelProcessor](https://link.gitcode.com/i/6d6dc9859eb02c610fcfe6584ed98f90)在设计时被用来对历史版本的模型快照做向后兼容修复,使其能被当前版本的 EF Core 使用。其Process(IReadOnlyModel? model, bool resetVersion = false)方法(src/EFCore.Relational/Migrations/SnapshotModelProcessor.cs#L57-L101)会:
- 读取模型上的产品版本注解
GetProductVersion(),据此分发版本相关的修复逻辑:- 1.x 版本:将无
Relational:前缀的关系注解重命名为带前缀的新格式(ProcessElement中按RelationalAnnotationNames白名单迁移); - 2.0/2.1 版本:修复拥有类型(owned types)的主键定义(
UpdateOwnedTypes,把基于唯一外键的拥有关系改挂到主键上,并为主键缺失的拥有类型补设主键); - 1.x~3.x 版本:把旧式以注解形式存储的序列迁移为
ISequence集合(UpdateSequences); - 8.x/9.x 版本:强制 complex property 的
IsNullable = false(UpdateComplexPropertyNullability);
- 1.x 版本:将无
- 移除
ChangeDetector.SkipDetectChanges注解,若resetVersion: true则把产品版本重置为当前版本,最后通过IModelRuntimeInitializer.Initialize(designTime: true)完成设计时模型的最终化。
该服务是 Scoped 生命周期,且明确支持 provider 派生并注册为自己的ISnapshotModelProcessor实现以追加 provider 专属修复(见其 XML 文档注释)。在MigrationsScaffolder与RemoveMigration中,快照模型在被 diff 前都会先经过Process(),这正是"升级 EF 大版本后旧迁移工程仍可正常生成新迁移"的机制保障。
五、测试布局:如何验证迁移子系统
迁移相关测试按层次组织:
- 迁移操作与生成器单元测试:test/EFCore.Relational.Tests/Migrations/,含
MigrationCommandExecutorTest.cs、MigrationCommandListBuilderTest.cs及Operations/子目录下的操作级测试; - 模型 diff 测试:test/EFCore.Relational.Tests/Migrations/Internal/ 中的
MigrationsModelDifferTest*.cs,验证"两个模型之间 diff 出正确的操作列表"; - provider 功能测试:test/EFCore.SqlServer.FunctionalTests/Migrations/ 与 test/EFCore.Sqlite.FunctionalTests/Migrations/,用真实数据库验证
HistoryRepositorySQL、并发迁移、种子数据等端到端行为; - 历史仓库单元测试:test/EFCore.SqlServer.Tests/Migrations/SqlServerHistoryRepositoryTest.cs、test/EFCore.Sqlite.Tests/Migrations/SqliteHistoryRepositoryTest.cs,断言各方言下建表、读写历史行的精确 SQL。
修改MigrationsSqlGenerator、MigrationsModelDiffer、操作类、HistoryRepository或Migrator时,按上述路径找到对应测试并补充用例,是仓库约定的验证方式。
六、关键类型索引
| 类型/概念 | 说明 | 位置 |
|---|---|---|
MigrationsScaffolder | 设计时脚手架:diff 模型、生成迁移代码与快照 | src/EFCore.Design/Migrations/Design/MigrationsScaffolder.cs |
MigrationsModelDiffer | 计算两个关系模型之间的操作差异 | 接口 IMigrationsModelDiffer |
MigrationsSqlGenerator | 操作列表 → SQL 命令(provider 定制 SQL 的扩展点) | src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs |
HistoryRepository | __EFMigrationsHistory表访问与数据库锁 | src/EFCore.Relational/Migrations/HistoryRepository.cs |
Migrator | 运行时迁移执行器(事务、重试、回滚) | src/EFCore.Relational/Migrations/Internal/Migrator.cs |
SnapshotModelProcessor | 旧版本快照的兼容修复 | src/EFCore.Relational/Migrations/SnapshotModelProcessor.cs |
MigrationOperation及派生类 | 中间操作表示(CreateTable、AddColumn、RenameTable…) | src/EFCore.Relational/Migrations/Operations/ |
CSharpMigrationsGenerator/CSharpSnapshotGenerator | 生成 Up/Down/Snapshot C# 代码 | src/EFCore.Design/Migrations/Design/ |
小结
EF Core 迁移子系统的精妙之处在于"操作列表"这一中间表示:设计时由MigrationsScaffolder通过MigrationsModelDiffer从"旧快照模型 → 新模型"的 diff 中产出操作并固化为 C# 代码;运行时由Migrator驱动MigrationsSqlGenerator把操作翻译成方言 SQL,在事务与数据库锁保护下逐条执行并写入__EFMigrationsHistory。SnapshotModelProcessor则为跨越 EF 大版本升级的旧工程提供了快照格式迁移的安全网。理解这条从模型到 SQL 的完整链路,是定制 provider 迁移行为、排查迁移失败问题以及补充子系统测试的基础。
【免费下载链接】efcoreEF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.项目地址: https://gitcode.com/GitHub_Trending/ef/efcore
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考