IOptionsMonitor是热更新唯一可靠选择,因其通过IChangeToken监听文件变化并自动重载,而IOptions仅初始化时读取一次;需确保配置文件复制到输出目录、JSON结构匹配、避免服务内缓存CurrentValue。

为什么 IOptionsMonitor<t></t> 是热更新的唯一可靠选择
因为 IOptions<t></t> 在注入时只读取一次配置快照,后续文件变更完全无感知;而 IOptionsMonitor<t></t> 内部绑定 IChangeToken,监听文件系统变化并自动触发重加载。不用它,所谓“热更新”基本是自己轮询或手动重读,既不安全也不符合 .NET 原生设计。
常见错误现象:IOptions<appsettings></appsettings> 注入后修改 appsettings.json,代码里取到的值始终不变;或者用 Configuration.GetSection(...).Get<t>()</t> 手动读取,但没注册变更监听,结果还是静态的。
- 必须在
Program.cs中用services.AddOptions<appsettings>().BindConfiguration("AppSettings")</appsettings>配合services.AddSingleton<ioptionsmonitor>>()</ioptionsmonitor>(后者由框架自动注册,通常只需确保前者存在) -
IOptionsMonitor<t></t>每次调用CurrentValue或Get(string)都返回最新值,无需缓存或手动刷新 - 如果配置类含嵌套对象或集合,确保 JSON 结构与 C# 类属性名、类型严格匹配,否则变更后反序列化失败,
CurrentValue会回退为默认值(不是抛异常)
appsettings.json 修改后不生效?检查文件 CopyToOutputDirectory 和 Watch 行为
.NET 默认只监听输出目录(如 bin/Debug/net8.0/)下的配置文件,而不是源码目录里的 appsettings.json。如果文件没被复制过去,或者被 IDE 编辑器锁住,IOptionsMonitor 根本收不到变更通知。
使用场景:你在 VS 里改了 appsettings.json,保存后程序没反应——大概率是文件根本没落到运行时目录,或文件系统监控被绕过。
- 确认
.csproj中该文件包含<copytooutputdirectory>PreserveNewest</copytooutputdirectory> - 不要用记事本直接编辑输出目录里的文件(权限/锁问题多),优先通过 VS 编辑源文件,靠构建自动同步
- Linux/macOS 下注意文件系统是否启用 inotify(Docker 容器里常需加
--privileged或挂载/proc/sys/fs/inotify) - 开发时可临时加日志验证监听是否工作:
configuration.GetReloadToken().RegisterChangeCallback(_ => Console.WriteLine("Config reloaded")), null)
自定义配置源(如数据库、Consul)怎么接入热更新
IOptionsMonitor<t></t> 本身不关心配置从哪来,只依赖背后的 IConfiguration 是否支持变更通知。所以关键不是替换 IOptionsMonitor,而是让自定义配置源返回带有效 IChangeToken 的 IConfigurationSection。
性能影响:轮询数据库查配置(比如每秒一次)会带来不必要的压力;用长连接+事件驱动(如 PostgreSQL LISTEN/NOTIFY)才是合理路径。
- 避免手写定时器轮询:它和
IOptionsMonitor的生命周期不耦合,容易造成内存泄漏或重复回调 - 推荐封装为
IConfigurationProvider实现,在Load()后调用OnReload()触发变更,再由IOptionsMonitor自动消费 - 如果用
Microsoft.Extensions.Configuration.Consul等第三方包,确认其版本支持Watch模式(v3+ 通常支持,v1/v2 多数只支持启动时拉取)
热更新时对象引用没变?小心单例服务里的配置缓存
IOptionsMonitor<t></t> 每次返回的是新实例(或结构体副本),但如果你在某个单例服务里把 CurrentValue 的结果存成字段,那这个字段永远不会更新——它只是个快照。
容易踩的坑:写了个 MyService,构造函数里读了一次 _options.CurrentValue.Timeout 存到私有字段,后面所有逻辑都用这个字段,导致永远用的是启动时的值。
- 永远不要在服务内部缓存
IOptionsMonitor<t>.CurrentValue</t>的结果(尤其是引用类型) - 正确做法是每次需要时调用
_options.CurrentValue,或用_options.Get("name")获取命名实例 - 如果性能敏感(比如高频访问),可考虑用
IOptionsSnapshot<t></t>(按请求生命周期刷新),但注意它不跨请求热更新,只适合短期作用域
配置热更新真正的复杂点不在注册或监听,而在于你是否真正理解「值的生命周期」和「引用的时效性」。很多人卡在“明明监听了,但业务逻辑还是旧值”,问题几乎都出在服务内部做了隐式缓存,或者混淆了 IOptions、IOptionsSnapshot、IOptionsMonitor 三者的适用边界。


















