ASP.NET Core 通过代码修改 appsettings.json 并自动重启应用

在 ASP.NET Core 应用中,appsettings.json 是常用的配置文件。默认情况下,通过 reloadOnChange: true 启用文件变更监控,可以让配置在运行时自动重新加载。

不过,仅仅重新加载配置并不意味着应用各模块会重新启动、刷新状态或使新的配置完全生效。对于某些场景(如重大配置变更、连接字符串切换、权重调整等),你可能希望当配置文件变更时让整个应用或关键模块“重启”或重初始化。

ChangeToken.OnChange 提供了一种在配置变更时注册回调的机制。当 IConfigurationRoot 的变更令牌触发时,你可以在回调中执行自定义逻辑(例如停止应用、启动重启流程等)。

不过,要注意的是:自动重启应用属于高级用法,需要你慎重设计,以避免服务中断、状态丢失、安全或资源问题。

下面我们分步骤探讨如何实现,以及要注意的坑和优化策略。

基础:启用变更监控 + 注册回调

首先,你需要在配置加载阶段开启 reloadOnChange:

builder.Configuration
    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
    .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true);

这样 IConfigurationRoot 能够生成变更令牌来监控 JSON 文件修改。

注意:在现代 ASP.NET Core 模板中(使用 CreateDefaultBuilder 或 WebApplication.CreateBuilder),默认就会添加 JSON 配置文件(appsettings.json、appsettings.{Environment}.json)并启用文件变更重载(reloadOnChange = true)。也就是说,这些 JSON 文件作为配置源的一部分,早就被加入 IConfigurationBuilder 管线里。即便你没有在 Program.cs 再写那段 AddJsonFile(...),它也已经被包含了。这个默认行为使得当 JSON 文件发生变动时,会触发内部的变更令牌机制,从而让 ChangeToken.OnChange 能拿到通知。

微软官方文档中提到:默认模板会使用 JSON 配置文件,并带 reloadOnChange 的选项。

所以,如果只是监控 appsettings.json 的话上面的代码可以省略。如果你需要监控一些自定义的json配置文件,可以像上面的代码那样开启 reloadOnChange。

接着,在应用启动后(通常在 Program.cs、WebApplication 构建完成之后)你可以用类似下面的代码订阅变更:

var app = builder.Build();
// 在 app 构建之后
if (builder.Configuration is IConfigurationRoot configRoot)
{
    ChangeToken.OnChange(
        () => configRoot.GetReloadToken(),
        () =>
        {
            Console.WriteLine("配置文件已变动,触发回调");
            // 在这里启动重启流程或其他操作
        });
}

在回调里,你就可以编写你希望执行的逻辑,比如优雅停止、重启、重载某些服务等。

如何在变更时重启应用:核心思路与示例

利用 IHostApplicationLifetime.StopApplication() 

在 ASP.NET Core 的通用主机模型中,可以通过注入 IHostApplicationLifetime(或新版为 IHostApplicationLifetime)来请求停止当前应用:

var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
lifetime.StopApplication();

这样主机会开始关闭流程,包括触发 ApplicationStopping、ApplicationStopped 等事件。若宿主环境(例如容器、Windows 服务、IIS 或 systemd)被配置为自动重启应用,那么停止后就会自动启动新的实例。

在 ChangeToken 回调中结合这个方法,基本流程是:

  1. 在回调中延迟一点时间(防止重复触发)
  2. 在回调中执行 StopApplication()
  3. 可选地在回调里启动新的进程(自重启机制)
  4. 让新的实例接管服务

下面是一个较完整示例(适用于 minimal hosting 的 Program.cs):

var builder = WebApplication.CreateBuilder(args);

// 这部分代码可以省略
builder.Configuration
    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
    .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true);

var app = builder.Build();

if (builder.Configuration is IConfigurationRoot configRoot)
{
    ChangeToken.OnChange(
        () => configRoot.GetReloadToken(),
        () =>
        {
            Console.WriteLine("配置已变更,准备重启");
            Task.Run(async () =>
            {
                await Task.Delay(200);  // 简单去抖
                // 可以做清理操作:通知模块、断开连接、保存状态等

                var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
                lifetime.StopApplication();
                // 如果是网站项目,到这里就可以了,再次请求站点会自动启动
                
                // 可选:启动自身新进程(若环境允许)
                var exe = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName;
                if (!string.IsNullOrEmpty(exe))
                {
                    try
                    {
                        System.Diagnostics.Process.Start(exe);
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"启动新进程失败:{ex.Message}");
                    }
                }
            });
        });
}

app.MapGet("/", () => "OK");

app.Run();

在这个示例里:

  • 延迟 200ms 简单去抖,避免因文件写入的多个通知导致重复重启
  • 调用 StopApplication() 请求优雅关闭
  • 尝试启动新的进程(如果可行)作为自启动机制

借助外部宿主重启机制

在实际生产环境中,最可靠的方式通常是将“重启责任”交给宿主环境:

  • 在容器(Docker / Kubernetes)中设置 restartPolicy
  • 在 systemd、Supervisor、Windows 服务中设置“服务失败重启”策略
  • 在 IIS 或 Azure 等平台配置健康探测 / 应用自动重启

这样一旦应用停止(通过 StopApplication()),宿主会自动启动一个新的实例,而你的应用本身无需自行 Process.Start。

这种方式通常更稳定、更安全,也更符合操作惯例。

为何 ChangeToken.OnChange 可能被触发多次?如何防抖?

一个常见的问题是:即使你修改 appsettings.json 只有一次,也可能触发 OnChange 回调次数为 2 次甚至更多。原因如下:

  • 文件监控机制(例如 FileSystemWatcher)可能在一次文件写入操作中发出多个事件(修改、关闭、创建、重命名等)。
  • 底层 IFileProvider 对同一变更可能多次通知 IChangeToken。
  • 如果你用多个 AddJsonFile 注册多个 JSON 文件,变更可能触发多个来源的通知。
  • 如果你在回调中写回文件或修改配置,也可能引起“自触发”变更回调。

为了避免重复的重启或重复逻辑执行,你需要在回调中做去重 / 防抖处理。常见策略包括:

  • 延迟执行 + 取消前一个延迟(Debounce)
  • 对比哈希值 / 版本号,只有内容真正改变才执行
  • 使用一次性订阅 + 回调内重新注册
  • 使用标志变量防止重叠执行

例如:

bool isHandling = false;

ChangeToken.OnChange(
    () => configRoot.GetReloadToken(),
    () =>
    {
        if (isHandling) return;
        isHandling = true;
        Task.Run(async () =>
        {
            await Task.Delay(200);
            // 对比哈希或时间戳判断是否真正变更
            lifetime.StopApplication();
            isHandling = false;
        });
    });

这样就可以避免因多次通知而多次触发重启。

适用场景、优点与风险提醒

适用场景

  • 开发 / 测试环境:希望修改配置后立即生效且无需重启整个应用
  • 运维工具 / 内部后台系统:配置变更时重启服务可能被接受
  • 小型服务 / 无状态服务:重启引发的中断影响较小

优点

  • 修改配置文件即可触发应用更新,无需人工重启
  • 对于某些配置变更(例如连接字符串、日志级别、开关控制等)可以即时生效
  • 能在回调中插入清理、备份、通知等流程

风险与限制

  • 服务中断:重启过程中会有短暂不可用窗口
  • 状态丢失:如果应用持有状态(In-Memory 缓存、会话、连接等),重启时可能丢失
  • 权限 / 环境限制:启动新进程或重启机制在某些宿主环境可能被禁止
  • 无限重启 / 重入风险:若配置频繁变更或回调引起二次变更,可能导致反复重启
  • 资源释放与异常处理:必须在停止流程中优雅释放连接、事务、线程等资源
  • 不可靠在所有环境中:部分宿主环境可能对 StopApplication 无响应或无法自动重启

总结建议

  • 使用 ChangeToken.OnChange + IHostApplicationLifetime.StopApplication() 是一种可行的“在配置变更时重启”方式,但最好作为一种备选方案,而不是常规机制
  • 更稳妥、更推荐的做法是:让宿主层处理重启(例如容器或服务平台自动重启)
  • 在回调中加入去抖 / 重复判断逻辑,避免多次触发
  • 若可能,尽量使用动态重载、模块刷新或 IOptionsMonitor<T> 来处理配置变更,而不是重启整个应用
  • 在编写回调逻辑时,注意异常处理、资源释放、中断控制等细节
评论