我最早接触 EF Core 时,对“Code First”方案的理解一直停留在三步走:写实体、写 DbContext、打开终端执行 dotnet ef migrations add xxx,再 dotnet ef database update。这套流程自己开发时用得很顺手,直到后来我开始负责一款需要交付给客户独立部署的企业应用——客户服务器上只有 .NET 运行时,没有 SDK,更不允许随便装开发工具链,而且强烈要求系统里能提供一个“数据库自动升级”的入口,让不熟悉命令行的运维也能操作。这时候我才意识到,迁移的生成动作如果只能依赖命令行,很多真实的交付场景根本跑不起来。
EF Core 的迁移能力本身是可以脱离 CLI 独立存在的。我花了不少时间翻了设计时服务那套代码,最后在应用内部以编程方式把“生成迁移”这件事给做通了。这篇文章就围绕这个话题展开:为什么需要绕过 dotnet ef 直接调迁移生成器、底层是怎么工作的、具体代码怎么落地,以及我在这条路上踩过哪些坑。适合正在做模块化系统、自动化升级平台,或者单纯想把迁移机制弄得更透彻的 .NET 开发者。
1. 从命令行到运行时,为什么会有“想要在代码里生成迁移”这件事
1.1 命令行能做的事,其实就那几个
先说一个经常被忽略的事实:dotnet ef 命令行工具并不是 EF Core 运行时的一部分,它只是一个独立的“设计时入口”。它的工作流程大致是:启动项目、找到 DbContext、加载模型、调用内部迁移生成器、把生成的 C# 文件写到磁盘上。整个过程本质上是在“程序外”操作的。
这也是为什么执行迁移命令时经常要求“项目能成功编译”,因为工具要从编译产物里反射出你的上下文和实体模型。如果项目不是标准的可启动类型,或者某个服务依赖了配置文件、环境变量,命令行工具往往会因为无法完成实例化而抛出一堆让人头疼的异常。
常规开发里,靠 CLI 生成迁移没有任何问题。但在某些产品化场景下,生成迁移这个动作的“执行主体”需要有变化:
- 交付给客户的系统,需要自动检测数据库结构差异并升级,不能要求现场人员敲命令。
- 模块化平台里,业务模块是运行时动态加载的,每次发布都可能带来新的实体变更。
- 程序提供“在线升级数据库”功能,点一下按钮就把迁移文件生成并应用完。
在这些场景里,就需要让程序自己具备“生成迁移”的能力。
1.2 当“生成”必须发生在程序内部
我最开始尝试过的方案是曲线救国:在后台任务里用 Process.Start 去启动一个新的 dotnet ef 进程。思路很直接,代码也简单,但实际用起来问题很多。
首先是目标机器的环境问题。客户服务器可能根本就没装 .NET SDK,而 dotnet ef 必须依赖 SDK 来编译和加载项目。然后是指定项目路径的问题,发布后的目录结构跟开发时完全不同,需要手工去拼各种 .dll 和 .deps.json 的路径,非常脆弱。最后还有一个恶心的点,进程启动是异步的,如果前一次生成尚未结束,下一次请求进来就会打架,出现文件写入冲突。
与其在外面调度一个黑盒进程,不如直接进入到 EF Core 内部把生成器调起来。这个方向才是解决“运行时迁移”的正解。
1.3 别把目标搞混:生成、编译、落地三件事
把“编程方式生成迁移”作为目标之前,得先拆开三个经常被混为一谈的环节:
- 生成迁移代码文件。
- 把生成的代码编译进当前运行的程序集。
- 执行迁移把数据库升级到最新。
我们在命令行里一条命令完成三件事,因为 CLI 本身就在源码环境里工作。可到了运行时,这三件事是分离的。比如,你把 C# 文件写到磁盘后,并不代表当前进程就能立刻使用这段新代码,还需要把迁移类型动态加载进程序集,或者干脆等应用重启。很多人在搜代码生成方案时感到困惑,就是没把这三层职责分清楚。
这篇文章重点讲的是第一件事:如何在进程内调用 EF Core 的迁移生成器,让它像命令行工具一样吐出迁移文件。后面两件事我会在第 3、5 节里结合实际方案展开讨论。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. EF Core 迁移的构造逻辑:它究竟是怎么知道“这次要改什么”的
2.1 迁移目录里的三种文件各司其职
如果有过手写迁移的经历,你会发现用 dotnet ef migrations add 生成的文件其实有三类,每一种都有不能替代的作用。以添加一张 Product 表为例,Migrations 目录里会多出:
{时间戳}_AddProduct.cs,迁移主文件。里面是继承自Migration的类,包含Up和Down方法,定义了“怎么改”和“怎么撤销改”。{时间戳}_AddProduct.Designer.cs,迁移元数据文件。它通过[DbContext(typeof(YourDbContext))]特性告诉 EF Core 这个迁移归属于哪个上下文,内部还包含一个BuildTargetModel方法,用来描述“迁移完成后数据库模型应该长什么样”。YourDbContextModelSnapshot.cs,模型快照文件。它维护的是上一次迁移完成后整个数据库模型的完整状态。每次新增迁移后,这个文件会更新成最新模型。
很多初学者只把注意力放在第一类文件上,遇到迁移冲突时才发现快照和 Designer 文件同样关键。其实这三类文件配合起来,才构成了 EF Core 做差异分析的依据。
2.2 核心比较器:当前模型和上个快照的差异
EF Core 的迁移生成器不是靠数据库反推结构的,它的判定机制是“拿当前上下文模型与上一次迁移留下的快照模型做差异比较”。
我打个比方。模型快照相当于工地上的“已完工图纸”,记录的是上一次施工结束后的房屋形态。你现在手里还有一套“新装修方案”,也就是 DbContext 的 OnModelCreating 里定义的实体集合。当生成迁移时,EF Core 会调用一个内部组件 MigrationsModelDiffer,把这两套图纸逐项比较,发现哪里多了柱子、哪些墙体拆了,然后把差异转化成一个个操作,比如 AddColumn、CreateTable、DropForeignKey。
这里有个很容易踩的认知误区:如果生成的迁移文件没有成功更新快照文件,下一次再执行迁移生成时,生成器会发现上次的差异仍然没有记录在案,于是把同样的迁移再生成一遍。换句话说,快照文件是“上一次操作的真实成果”,而不是锦上添花的附赠品。它必须跟迁移主文件同步更新,否则整个差异链就断了。
2.3 设计时服务那套东西到底是什么
EF Core 里有个概念叫“设计时服务”(Design Time Services),跟运行时服务是两套独立存在的组件。运行时服务负责查询、保存、事务管理;设计时服务负责建模型、算差异、生成迁移文件、生成 SQL 脚本。
正常情况下,dotnet ef 工具会自动把设计时服务装配到一个独立的依赖注入容器里。但如果你想
