背景ASP.NET Core引入了Options模式,使用类来表示相关的设置组。简单的来说,就是用强类型的类来表达配置项,这带来了很多好处。 示例我们先从一小段代码着手(TestOptions类只有一个字符串属性Name,代码略): 1 class Program 2 { 3 static void Main(string[] args) 4 { 5 var builder = new ConfigurationBuilder(); 6 builder.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); //注意最后一个参数值,true表示配置文件更改时会重新加载。 7 var configuration = builder.Build(); 8 var services = new ServiceCollection(); 9 services.AddOptions(); 10 services.Configure<TestOptions>(configuration); //这里通过配置文件绑定TestOptions 11 var provider = services.BuildServiceProvider(); 12 Console.WriteLine("修改前:"); 13 Print(provider); 14 15 Change(provider); //使用代码修改Options值。 16 Console.WriteLine("使用代码修改后:"); 17 Print(provider); 18 19 Console.WriteLine("请修改配置文件。"); 20 Console.ReadLine(); //等待手动修改appsettings.json配置文件。 21 Console.WriteLine("修改appsettings.json文件后:"); 22 Print(provider); 23 } 24 25 static void Print(IServiceProvider provider) 26 { 27 using(var scope = provider.CreateScope()) 28 { 29 var sp = scope.ServiceProvider; 30 var options1 = sp.GetRequiredService<IOptions<TestOptions>>(); 31 var options2 = sp.GetRequiredService<IOptionsMonitor<TestOptions>>(); 32 var options3 = sp.GetRequiredService<IOptionsSnapshot<TestOptions>>(); 33 Console.WriteLine("IOptions值: {0}", options1.Value.Name); 34 Console.WriteLine("IOptionsMonitor值: {0}", options2.CurrentValue.Name); 35 Console.WriteLine("IOptionsSnapshot值: {0}", options3.Value.Name); 36 Console.WriteLine(); 37 } 38 } 39 40 static void Change(IServiceProvider provider) 41 { 42 using(var scope = provider.CreateScope()) 43 { 44 var sp = scope.ServiceProvider; 45 sp.GetRequiredService<IOptions<TestOptions>>().Value.Name = "IOptions Test 1"; 46 sp.GetRequiredService<IOptionsMonitor<TestOptions>>().CurrentValue.Name = "IOptionsMonitor Test 1"; 47 sp.GetRequiredService<IOptionsSnapshot<TestOptions>>().Value.Name = "IOptionsSnapshot Test 1"; 48 } 49 } 50 } appsettings.json文件: { "Name": "Test 0" } 上面的代码,首先从appsettings.json文件读取配置,然后向容器注册依赖配置文件的TestOptions,接着分别打印IOptions<>,IOptionsMonitor<>和IOptionsSnapshot<>的值。 接着通过代码来修改TestOptions的值,打印。 注意,我们仅注册了一次TestOptions,却可以分别通过IOptions<>,IOptionsMonitor<>和IOptionsSnapshot<>接口来获取TestOptions的值。 如果我们把appsettings.json文件中Name的值修改为Test 2,那么上面这段代码的输出是这样的: 分析我们可以看到第一次通过代码修改IOptions<>和IOptionsMonitor<>的值后,再次打印都被更新了,但是IOptionsSnapshot<>没有,为什么呢? var services = new ServiceCollection(); services.AddOptions(); 我们观察AddOptions方法的实现: 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; } 从上面的代码我们可以得知,IOptions<>和IOptionsMonitor<>被注册为单例服务,而IOptionsSnapshot<>被注册为范围服务。 我们继续看第二次修改,第二次修改配置文件后IOptionsMonitor<>和IOptionsSnapshot<>的值更新了,而IOptions<>的值没有更新。 public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache) { _factory = factory; _sources = sources; _cache = cache; foreach (var source in _sources) { var registration = ChangeToken.OnChange( () => source.GetChangeToken(), (name) => InvokeChanged(name), source.Name); _registrations.Add(registration); } } 原来OptionsMonitor的更新能力是从IOptionsChangeTokenSource<TOptions>而来,但是这个接口的实例又是谁呢? services.Configure<TestOptions>(configuration); 这是一个定义在Microsoft.Extensions.Options.ConfigurationExtensions.dll的扩展方法,最后实际调用的是它的一个重载方法,代码如下: 1 public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config, Action<BinderOptions> configureBinder) 2 where TOptions : class 3 { 4 if (services == null) 5 { 6 throw new ArgumentNullException(nameof(services)); 7 } 8 9 if (config == null) 10 { 11 throw new ArgumentNullException(nameof(config)); 12 } 13 14 services.AddOptions(); 15 services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config)); 16 return services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder)); 17 } 秘密就在上面的第15行,ConfigurationChangeTokenSource,它引用了代表配置文件的对象config,所以配置文件更新,IOptionsMonitor就会跟着更新。 结论IOptions<>是单例,因此一旦生成了,除非通过代码的方式更改,它的值是不会更新的。 官方文档是这样介绍的:
IOptionsSnapshot<TOptions>在需要对每个请求重新计算选项的场景中非常有用。 所以你应该根据你的实际使用场景来选择到底是用这三者中的哪一个。 services.Configure<TestOptions>(opt => opt.Name = "Test 0"); IOptions<>最简单,也许是一个不错的选择,Configure扩展方法还有其他重载可以满足你的更多需求。 |
|