背景

ASP.NET Core引入了Options模式,使用类来表示相关的设置组。简单的来说,就是用强类型的类来表达配置项,这带来了很多好处。
初学者会发现这个框架有3个主要的面向消费者的接口:IOptions<TOptions>、IOptionsMonitor<TOptions>以及IOptionsSnapshot<TOptions>。
这三个接口初看起来很类似,所以很容易引起困惑,什么场景下该用哪个接口呢?

示例

我们先从一小段代码着手(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文件:

  1. {
  2. "Name": "Test 0"
  3. }

上面的代码,首先从appsettings.json文件读取配置,然后向容器注册依赖配置文件的TestOptions,接着分别打印IOptions<>,IOptionsMonitor<>和IOptionsSnapshot<>的值。

接着通过代码来修改TestOptions的值,打印。
然后通过修改appsettings.json文件来修改TestOptions的值,打印。

注意,我们仅注册了一次TestOptions,却可以分别通过IOptions<>,IOptionsMonitor<>和IOptionsSnapshot<>接口来获取TestOptions的值。

如果我们把appsettings.json文件中Name的值修改为Test 2,那么上面这段代码的输出是这样的:

分析

我们可以看到第一次通过代码修改IOptions<>和IOptionsMonitor<>的值后,再次打印都被更新了,但是IOptionsSnapshot<>没有,为什么呢?
让我们从Options框架的源代码着手,理解为什么会这样。
当我们需要使用Options模式时,我们都会调用定义在OptionsServiceCollectionExtensions类上的扩展方法AddOptions(this IServiceCollection services)。

  1. var services = new ServiceCollection();
  2. services.AddOptions();

我们观察AddOptions方法的实现:

  1. public static IServiceCollection AddOptions(this IServiceCollection services)
  2. {
  3. if (services == null)
  4. {
  5. throw new ArgumentNullException(nameof(services));
  6. }
  7.  
  8. services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
  9. services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
  10. services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
  11. services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
  12. services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));
  13. return services;
  14. }

从上面的代码我们可以得知,IOptions<>和IOptionsMonitor<>被注册为单例服务,而IOptionsSnapshot<>被注册为范围服务。
由于IOptions<>和IOptionsMonitor<>都被注册为单例服务,因此每次获取的都是同一个实例,所以更改了以后的值是保留的。
而IOptionsSnapshot<>被注册为范围服务,所以每次创建新范围时获取的都是一个新的值,外部的更改只对当次有效,不会保留到下次(不能跨范围,对于ASP.NET Core来说不能跨请求)。

我们继续看第二次修改,第二次修改配置文件后IOptionsMonitor<>和IOptionsSnapshot<>的值更新了,而IOptions<>的值没有更新。
IOptions<>好理解,它被注册为单例服务,第一次访问的时候生成实例并加载配置文件中的值,此后再也不会读取配置文件,所以它的值不会更新。
IOptionsSnapshot<>被注册为范围服务,每次重新生成一个新的范围时,它都会从配置文件中获取值,因此它的值会更新。
但是,IOptionsMonitor<>呢,它被注册为单例,为什么也会更新呢?
让我们回到AddOptions的源代码,我们留意到IOptionsMonitor<>的实现是OptionsManager<>。
当我们打开OptionsManager的源代码时,一切都很清楚了。
它的构造函数如下:

  1. public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
  2. {
  3. _factory = factory;
  4. _sources = sources;
  5. _cache = cache;
  6.  
  7. foreach (var source in _sources)
  8. {
  9. var registration = ChangeToken.OnChange(
  10. () => source.GetChangeToken(),
  11. (name) => InvokeChanged(name),
  12. source.Name);
  13.  
  14. _registrations.Add(registration);
  15. }
  16. }

原来OptionsMonitor的更新能力是从IOptionsChangeTokenSource<TOptions>而来,但是这个接口的实例又是谁呢?
我们回到最开始的代码的第10行:

  1. 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<>是单例,因此一旦生成了,除非通过代码的方式更改,它的值是不会更新的。
IOptionsMonitor<>也是单例,但是它通过IOptionsChangeTokenSource<> 能够和配置文件一起更新,也能通过代码的方式更改值。
IOptionsSnapshot<>是范围,所以在配置文件更新的下一次访问,它的值会更新,但是它不能跨范围通过代码的方式更改值,只能在当前范围(请求)内有效。

官方文档是这样介绍的:
IOptionsMonitor<TOptions>用于检索选项和管理TOptions实例的选项通知,它支持下面的场景:

  • 实例更新通知。
  • 命名实例。
  • 重新加载配置。
  • 选择性的让实例失效。

IOptionsSnapshot<TOptions>在需要对每个请求重新计算选项的场景中非常有用。
IOptions<TOptions>可以用来支持Options模式,但是它不支持前面两者所支持的场景,如果你不需要支持上面的场景,你可以继续使用IOptions<TOptions>。

所以你应该根据你的实际使用场景来选择到底是用这三者中的哪一个。
一般来说,如果你依赖配置文件,那么首先考虑IOptionsMonitor<>,如果不合适接着考虑IOptionsSnapshot<>,最后考虑IOptions<>。
有一点需要注意,在ASP.NET Core应用中IOptionsMonitor可能会导致同一个请求中选项的值不一致——当你正在修改配置文件的时候——这可能会引发一些奇怪的bug。
如果这个对你很重要,请使用IOptionsSnapshot,它可以保证同一个请求中的一致性,但是它可能会带来轻微的性能上的损失。
如果你是在app启动的时候自己构造Options(比如在Startup类中):

  1. services.Configure<TestOptions>(opt => opt.Name = "Test 0");

IOptions<>最简单,也许是一个不错的选择,Configure扩展方法还有其他重载可以满足你的更多需求。

IOptions、IOptionsMonitor以及IOptionsSnapshot的更多相关文章

  1. .Net Core 配置文件读取 - IOptions、IOptionsMonitor、IOptionsSnapshot

    原文链接:https://www.cnblogs.com/ysmc/p/16637781.html 众所周知,appsetting.json 配置文件是.Net 的重大革新之心,抛开了以前繁杂的xml ...

  2. 【5min+】更好的选项实践。.Net Core中的IOptions

    系列介绍 [五分钟的dotnet]是一个利用您的碎片化时间来学习和丰富.net知识的博文系列.它所包含了.net体系中可能会涉及到的方方面面,比如C#的小细节,AspnetCore,微服务中的.net ...

  3. 看看.NET Core几个Options的简单使用

    前言 配置,对我们的程序来说是十分重要的一部分.或多或少都会写一部分内容到配置文件中去. 由其是在配置中心(Apollo等)做起来之前,配置文件一定会是我们的首选. 在.NET Core中,习惯的是用 ...

  4. .NET Core 学习资料精选:入门

    开源跨平台的.NET Core,还没上车的赶紧的,来不及解释了-- 本系列文章,主要分享一些.NET Core比较优秀的社区资料和微软官方资料.我进行了知识点归类,让大家可以更清晰的学习.NET Co ...

  5. 在.NET Core中用最原生的方式读取Nacos的配置

    背景 之前老黄写过一篇<ASP.NET Core结合Nacos来完成配置管理和服务发现>简单介绍了如何让.NET Core程序接入Nacos,之前的SDK里面更多的是对Nacos的Open ...

  6. ASP.NET Core通过Nacos SDK读取阿里云ACM

    背景 前段时间,cranelee 在Github上给老黄提了个issues, 问到了如何用Nacos的SDK访问阿里云ACM. https://github.com/catcherwong/nacos ...

  7. .NET Core 选项模式【Options】的使用

    ASP.NET Core引入了Options模式,使用类来表示相关的设置组.简单的来说,就是用强类型的类来表达配置项,这带来了很多好处.利用了系统的依赖注入,并且还可以利用配置系统.它使我们可以采用依 ...

  8. 跟我一起学.NetCore之选项(Options)核心类型简介

    前言 .NetCore中提供的选项框架,我把其理解为配置组,主要是将服务中可供配置的项提取出来,封装成一个类型:从而服务可根据应用场景进行相关配置项的设置来满足需求,其中使用了依赖注入的形式,使得更加 ...

  9. Asp.Net Core 选项模式的三种注入方式

    前言 记录下最近在成都的面试题, 选项模式的热更新, 没答上来 正文 选项模式的依赖注入共有三种接口, 分别是 IOptions<>, IOptionsSnapshot<>, ...

随机推荐

  1. piranha(注意iptables和selinux的问题)

    piranha是红帽官方提供的一套工具,安装和配置都非常简单,可以快速部署. piranha方案原理结构描述: piranha方案是基于lvs基础上设计的一套负载均衡高可用解决方案 LVS运行在一对有 ...

  2. Laravel 队列使用

    触发 任务的触发,主要的实现是在IlluminateFoundationBusDispatchesJobs这个trait中实现的,其只包含两个方法 protected function dispatc ...

  3. PyCharm 介绍、安装、入门使用

    一.Pycharm介绍 前面几年的时间,我一直用的eclipse,后面开始听同事说用IntelliJ IDEA了,而且说是目前业界最好用的java开发工具,IDE(集成开发环境),没有之一.PyCha ...

  4. 网络健身O2O,能火吗?

       谈到中国想要020的那些项目,总给人一种土豪烧钱的怪异形象,而最终的成败因素也变得简单,也即谁能烧到最后,谁就能称霸市场,可问题在于,前期投入太多,谁也不甘心主动退出,最后,只落得个油尽灯枯.这 ...

  5. 从 ListView 到 RecyclerView 的用法浅析

    文章目录 要走好明天的路,必须记住昨天走过的路,思索今天正在走着的路. ListView,一种在垂直滚动列表中显示条目的视图:RecyclerView,一种在局限的窗口呈现大数据集合的灵活视图.Rec ...

  6. [python每日一练]--0012:敏感词过滤 type2

    题目链接:https://github.com/Show-Me-the-Code/show-me-the-code代码github链接:https://github.com/wjsaya/python ...

  7. 想清楚再入!VR硬件创业能“要你命”

    每一次跨时代新产品的出现,总会让科技行业疯狂一阵儿,十年前是智能手机,今天自然是VR.自2015年开始,VR火的越来越让人欣喜,让人兴奋,更让人越来越看不清,越来越害怕.数不清的大小品牌义无反顾的杀入 ...

  8. 爬虫cookies详解

    cookies简介 cookie是什么? Cookie,有时也用其复数形式 Cookies,指某些网站为了辨别用户身份.进行 session 跟踪而储存在用户本地终端上的数据(通常经过加密).定义于 ...

  9. 为什么要用location的hash来传递参数?

    分页功能代码实现 <div> <a class="btn" href="#" style="..." @Click.pre ...

  10. Java入门教程十二(集合与泛型)

    在 Java 中数组的长度是不可修改的.然而在实际应用的很多情况下,无法确定数据数量.这些数据不适合使用数组来保存,这时候就需要使用集合. Java 的集合就像一个容器,用来存储 Java 类的对象. ...