ASP.NET Core 的应用迁移:针对常见问题的小型库
Source: Dev.to
在 ASP.NET Core 中管理应用程序更新
你知道这种情况。你构建了一个应用,部署了它,一切都很好。然后你需要推送更新。也许你在添加一个需要一些初始数据的新功能。或者因为你改变了某些工作方式,需要转换已有记录。又或者你只需要在首次运行时创建文件夹结构。
如果你已经做了这件事一段时间,你可能已经有了自己的临时解决方案:
- 数据库中的一个布尔标志,表示“我们已经运行了 v2 设置吗?”
- 在创建默认配置之前检查文件是否存在的代码
- 分散在
Program.cs中的许多if语句
我写过所有这些。它们能工作——直到它们不再工作。
真正的问题是我们没有一个合适的方式来 对应用程序的状态进行版本控制——不是数据库模式,而是应用程序本身。
EF Core Migrations 不涵盖所有内容
EF Core migrations 仍然存在,而且在它们擅长的领域表现出色:管理数据库模式的变更。需要新表?使用 Migration。新列?使用 Migration。索引?使用 Migration。
但 EF Core migrations 在 应用程序代码之前 运行。它们处理的是结构,而不是数据。它们不处理以下情况:
- 在模式更改后种子初始数据
- 当功能上线时发送一次性通知
- 创建文件系统结构
- 从 v1 升级到 v2 时执行清理任务
- 当更改存储方式时转换已有数据
你可以尝试在 EF migrations 中使用原始 SQL 把这些东西塞进去,但说实话,那是一团糟。没有依赖注入,无法访问你的服务,也没有合适的 C# 代码。而且,想要对它们进行测试更是难上加难。
小型应用迁移库
我编写了一个库来处理这个问题。思路很简单:为你的应用程序提供与数据库模式已经拥有的相同的版本语义。
迁移的样子
public class MyMigration : IApplicationMigration
{
public Version Version => new Version(1, 2, 0);
public bool FirstTime { get; set; }
public MyMigration(IMyService service) { … }
public async Task UpAsync(IDictionary cache, CancellationToken ct)
{
// migration logic here
}
public async Task DownAsync(IDictionary cache, CancellationToken ct)
{
// optional rollback (not required)
}
}
注意以下几点
- 构造函数注入可用 —— 你的迁移是 DI 容器的完整成员。
- 版本是显式的 —— 没有基于日期的命名,也没有魔法字符串。只使用
System.Version。 FirstTime标记 —— 告诉你此版本是否曾经被注册过。
最后一点尤其有用。在开发期间,当前版本会在每次启动时重新执行,以便你能够快速迭代。FirstTime 标记让你能够保护那些真正只应执行一次的操作。
生命周期钩子
有时你需要更细粒度的控制。比如在进行复杂的数据转换时,需要在模式更改 之前 捕获数据并在 之后 应用它。库为此提供了钩子:
public interface IApplicationMigration
{
Version Version { get; }
bool FirstTime { get; set; }
Task UpAsync(IDictionary cache, CancellationToken ct);
Task DownAsync(IDictionary cache, CancellationToken ct);
Task BeforeAsync(IDictionary cache, CancellationToken ct) => Task.CompletedTask;
Task AfterAsync(IDictionary cache, CancellationToken ct) => Task.CompletedTask;
}
cache 字典在迁移之间共享,因此你可以在 UpAsync() 方法中读取捕获的数据并按需进行转换。
设置
设置非常简洁:
builder.Services.AddApplicationMigrations()
.AddDbContext(options => …);
当你配置 DbContext 时,库会在执行你的应用迁移之前自动运行 Database.MigrateAsync()。因此你无需自行调用——EF Core 的模式更改会先被应用,然后再运行你的应用迁移。
你仍需要自行实现存储部分——你想在哪里记录哪些版本已经被应用?大多数人使用数据库表,但也可以使用文件、Redis,或任何适合你环境的方式。
基本的数据库实现
public class MigrationVersion
{
public int Id { get; set; }
public string Version { get; set; } = default!;
public DateTime AppliedOn { get; set; }
}
Source: …
多服务器部署
如果你在运行多个实例(负载均衡、复制等),通常不希望它们同时尝试执行迁移。引擎提供了 ShouldRun 属性,你可以重写它来进行控制。
一种常见的做法是通过配置将其中一个实例指定为 “主实例”:
{
"Migrations": {
"IsMaster": true
}
}
然后,在迁移运行器中:
public class MyMigrationRunner : ApplicationMigrationRunner
{
protected override bool ShouldRun => Configuration.GetValue("Migrations:IsMaster");
}
只有主实例的 Migrations:IsMaster 为 true。其余实例会完全跳过迁移,直接正常启动。这样就避免了竞争条件和重复执行,而无需使用分布式锁。
实际使用案例
- Feature rollout with data seeding – 您正在添加一个新的 “Categories”(类别)功能。EF 迁移会创建表,但您需要用默认类别填充它并为现有产品分配类别。一次迁移即可同时完成数据创建和分配。
- Configuration evolution – 您的应用过去以一种方式存储设置,现在改为另一种方式。编写迁移读取旧格式并写入新格式。
- Environment setup – 第一次部署到新服务器?创建必需的目录,生成默认配置文件,搭建应用运行所需的任何东西。
- Notification on upgrade – 想在部署重大版本时给管理员发送邮件吗?将其放入迁移中,并用
FirstTime进行保护。
为什么没有 Down() 方法?
没有用于回滚的 Down() 方法。根据我的经验,回滚方法很少被测试,在真正需要时往往会出问题。我觉得编写一个新的前向迁移来撤销所需的更改更为简洁。版本 1.1 出了问题?版本 1.2 修复它。
获取方式
该包已发布在 NuGet:
dotnet add package AreaProg.AspNetCore.Migrations
它支持 .NET 6、.NET 8、.NET 9 和 .NET 10。
如果想要查看源码,可前往 GitHub:
这并不是革命性的东西,只是填补了我在一个又一个项目中不断遇到的空白。如果你曾经写过“首次部署时运行一次”的 hack,也许可以试试它。
有问题吗?反馈? 我很想了解其他人是如何解决这个问题的。