ASP.NET Core 的应用迁移:针对常见问题的小型库

发布: (2026年1月15日 GMT+8 05:19)
8 min read
原文: Dev.to

Source: Dev.to

AbsolutVision 在 Unsplash 上的照片

在 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:IsMastertrue。其余实例会完全跳过迁移,直接正常启动。这样就避免了竞争条件和重复执行,而无需使用分布式锁。

实际使用案例

  • 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:

ugnez/aspnet-migrations


这并不是革命性的东西,只是填补了我在一个又一个项目中不断遇到的空白。如果你曾经写过“首次部署时运行一次”的 hack,也许可以试试它。

有问题吗?反馈? 我很想了解其他人是如何解决这个问题的。

Back to Blog

相关文章

阅读更多 »

介绍 Go 的 Rate Limiter 库

概述 在现代后端系统中,速率限制(rate limiting)是必不可少的。如果没有它,API 将面临滥用、资源耗尽和不公平使用的风险。该库提供…