问题
.netcore 读取配置支持热更新,默认CreateDefaultBuilder中读取配置时也设定了开启热更新,然而在项目中发现更改了配置后使用的还是旧的配置信息,经过查看官方文档发现IOptions<>不会读取在应用启动后对 JSON 配置文件所做的更改,刚好项目中使用的是IOptions<>去读取JSON配置,需要使用IOptionsSnapshot<>才可以,下面来探究下IOptions<>、IOptionsMonitor<>、IOptionsSnapshot<> 有什么不同:
例子
构建一个webapi的项目(此处使用.net5),在控制器中注入三种Options接口,在方法中分别输出值代码如下:
public TestController(ILogger<TestController> logger,IOptions<User> options,IOptionsMonitor<User> optionsMonitor,IOptionsSnapshot<User> optionsSnapshot)
{
_logger = logger;
_options = options;
_optionsMonitor = optionsMonitor;
_optionsSnapshot = optionsSnapshot;
}
[HttpGet]
public async Task<IActionResult> GetAsync()
{
_logger.LogInformation($"options修改前:{_options.Value.Name}");
_logger.LogInformation("--------------------");
_logger.LogInformation($"optionsMonitor修改前:{_optionsMonitor.CurrentValue.Name}");
_logger.LogInformation("--------------------");
_logger.LogInformation($"optionsSnapshot修改前:{_optionsSnapshot.Value.Name}");
_logger.LogInformation("--------------------");
await Task.Delay(TimeSpan.FromSeconds(30));
_logger.LogInformation("修改配置文件");
_logger.LogInformation($"options修改后:{_options.Value.Name}");
_logger.LogInformation("--------------------");
_logger.LogInformation($"optionsMonitor修改后:{_optionsMonitor.CurrentValue.Name}");
_logger.LogInformation("--------------------");
_logger.LogInformation($"optionsSnapshot修改后:{_optionsSnapshot.Value.Name}");
_logger.LogInformation("--------------------");
return Ok("ok");
}
初始JSON的配置:
"Info": {
"Name": "dong35888"
},
发布代码使用IIS部署代码之后运行,查看运行日志
第一次请求方法:刚进入方法输出的值都一样,在方法进入休眠时手动修改JSON中name的值,休眠时间到方法继续运行,发现optionsMonitor输出的值修改了,options和optionsSnapshot还是原有的值;第二次请求方法,options输出的还是旧值,optionsMonitor和optionsSnapshot输出的都是修改之后的值,方法进入休眠再次修改JSON中name的值,方法继续options输出最开始的值没变,optionsMonitor输出的最新的值即第二次修改的值,optionsSnapshot输出没变(输出第一次修改后的值);
- IOptions<> 获取的值一直不变
- IOptionsMonitor<> 一直获取的是最新的值
- IOptionsSnapshot<> 一次请求中的值不变,如果有变更,再一次请求中可以获取到新的值
源码
借助源码来分析分析:
public static IServiceCollection AddOptions(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));
return services;
}
public interface IOptionsSnapshot<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] out TOptions> :
IOptions<TOptions>
where TOptions : class
{
/// <summary>
/// Returns a configured <typeparamref name="TOptions"/> instance with the given name.
/// </summary>
TOptions Get(string name);
}
可以看到IOption是以单例方式注册到DI,IOptionsSnapshot是scope方式注册到DI,并且IOptionsSnapshot继承了IOption;单例只构建一次,那就是项目启动IOption构建之后就保持不变了,IOptionsSnapshot每一次请求构建一次,所以在单个方法中它保持不变,第二次请求重新构建时获取了新的值;
针对IOptionsMonitor我们在做一个事例:
private readonly ILogger<TestController> _logger;
private readonly IOptionsMonitor<User> _optionsMonitor;
private readonly User _optionsMonitorUser;
public TestController(ILogger<TestController> logger,IOptions<User> options,IOptionsMonitor<User> optionsMonitor,IOptionsSnapshot<User> optionsSnapshot)
{
_logger = logger;
_optionsMonitor = optionsMonitor;
_optionsMonitorUser = optionsMonitor.CurrentValue;
}
[HttpGet("Monitor")]
public async Task<IActionResult> MonitorAsync()
{
_logger.LogInformation($"optionsMonitor修改前:{_optionsMonitor.CurrentValue.Name}");
_logger.LogInformation("--------------------");
_logger.LogInformation($"optionsMonitorUser修改前:{_optionsMonitorUser.Name}");
_logger.LogInformation("--------------------");
await Task.Delay(TimeSpan.FromSeconds(30));
_logger.LogInformation("手动修改配置文件");
_logger.LogInformation($"optionsMonitor修改后:{_optionsMonitor.CurrentValue.Name}");
_logger.LogInformation("--------------------");
_logger.LogInformation($"optionsMonitorUser修改后:{_optionsMonitorUser.Name}");
_logger.LogInformation("--------------------");
return Ok("ok");
}
在源码中看到IOptionsMonitor也是以单例注册到DI中,但它的实现类却不同,是OptionsMonitor,
public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
{
_factory = factory;
_sources = sources;
_cache = cache;
foreach (IOptionsChangeTokenSource<TOptions> source in _sources)
{
IDisposable registration = ChangeToken.OnChange(
() => source.GetChangeToken(),
(name) => InvokeChanged(name),
source.Name);
_registrations.Add(registration);
}
}
private void InvokeChanged(string name)
{
name = name ?? Options.DefaultName;
_cache.TryRemove(name);
TOptions options = Get(name);
if (_onChange != null)
{
_onChange.Invoke(options, name);
}
}
通过源码大概看到只要监听到变更,就重新获取,从上一个示例也证明这点,_optionsMonitorUser是在构造函数中从_optionsMonitor属性获取到的,修改前与_optionsMonitor中拿的值一致,休眠修改后,_optionsMonitorUser还是原值,_optionsMonitor里面已经是新的对象了;
结论
- IOption<> 是单例,一旦生成就不会再更改,除非程序停止销毁后再构建,那些确保不会发生变更的可以使用,或者修改之后重新启动程序;
- OptionsMonitor<> 也是单例,但是它只要配置有变更,它就会更新,这样可能造成一方法中前面和后面引用的值不一致,不过可以先通过CurrentValue属性拿到值在使用;
- IOptionsSnapshot<> 是scope注册,一次请求里值不会变,变更之后再下一次请求能获取到最新的值,刚好符合我的项目要求;
以上结论纯属个人总结,如有不正确出请指正。
资料
Microsoft.Extensions.Options.ConfigurationExtensions github地址
microsoft文档