.Net Core中的Options使用以及源码解析 在.Net Core中引入了Options这一使用配置方式,通常来讲我们会把所需要的配置通过IConfiguration对象配置成一个普通的类,并且习惯上我们会把这个类的名字后缀加上Options。所以我们在使用某一个中间件,或者使用第三方类库时,经常会看到配置对应Options的代码,例如关于Cookie的中间件就会配置CookiePolicyOptions这一个对象。
使用Options 在.Net Core中使用Options主要分为两个步骤:
向容器中注入TOptions的配置。目的是告诉容器当我获取这个TOptions时,这个TOptions包含的一些字段如何写入,所以我们需要传入一个Action。注意:默认情况下,这个TOptions需要一个无参的构造函数。
从容器中获取TOptions对象。在获取的时候有三种获取方式:IOptions,IOptionsMonitor,IOptionsSnapshot。
配置TOptions 在配置TOptions的时候,你会发现所有的方法都是泛型的,每一个Options类型都有一套独立的管理系统。入口是Configure方法,它有多个重载,但最终都会调用这个方法
1 public static IServiceCollection Configure <TOptions >(this IServiceCollection services, string name, Action<TOptions> configureOptions )
当不传递name时,默认使用Microsoft.Extensions.Options.DefaultName,他等于string.Empty
1 2 3 4 5 6 7 8 9 10 11 namespace Microsoft.Extensions.Options { public static class Options { public static readonly string DefaultName = string .Empty; } }
有的时候我们会看到在调用Configure时并没有传递Action,而是直接传递了一个IConfiguration,那是因为在内部帮我们转化了一下,最终传递的还是一个Action
1 options => ConfigurationBinder.Bind (config, options)
另外,我们可以看到ConfigureAll这个方法,这个内部也是调用了Configure方法,只不过把name设置成null,后续在创建TOptions时,会把name为nul的Action应用于所有实例。
最后还有一个PostConfigure方法,它和Configure方法使用方式一模一样,也是在创建TOptions时调用。只不过先后顺序不一样,PostConfigure在Configure之后调用。
现在我们来看实际的用法:
1 2 3 4 5 6 7 8 9 10 services.Configure<EmailOption>(op => op.Title = "Default Name" ); services.Configure<EmailOption>("FromMemory" , op => op.Title= "FromMemory" ); services.Configure<EmailOption>("FromConfiguration" , Configuration.GetSection("Email" )); services.AddOptions<EmailOption>("AddOption" ).Configure(op => op.Title = "AddOption Title" ); services.Configure<EmailOption>(null , op => op.From = "Same With ConfigureAll" ); services.PostConfigure<EmailOption>(null , op => op.Body = "Same With PostConfigureAll" );
EmailOption是一个很简单的类:
1 2 3 4 5 6 7 8 public class EmailOption { public string Title { get ; set ; } public string Body { get ; set ; } public string From { get ; set ; } }
在上面所示的用法,多了一个AddOptions的用法。这种方式会创建了一个OptionsBuilder,用来辅助配置TOptions对象,其内部实现是和Configure,PostConfigure方法一样的。
使用Options 既然我们告诉了容器TOption是如何配置的,那么在使用的时候只需要通过注入的方式取获取就行了。总共用三种获取方式:
IOptions:这种方式只能获取默认名称的那个TOptions,且不能监控配置源出现变化的情况。调用时访问它的Value属性即可。
IOptionsMonitor:这种方式可以获取所有名称的TOptions,且可以监控配置源出现变化的情况。调用它的TOptions Get(string name)方法即可获取TOptions
IOptionsSnapshot:此接口继承于IOptions,这种方式也可以获取所有名称的TOptions和监控配置源出现变化的情况。调用它的TOptions Get(string name)方法即可获取TOptions。但是它的实现和第二种完全不一样,后面会详细解释。
下面我们来看一下具体的使用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class HomeController : Controller { IOptions<EmailOption> _options; IOptionsMonitor<EmailOption> _optionsMonitor; IOptionsSnapshot<EmailOption> _optionsSnapshot; public HomeController (IOptions<EmailOption> options, IOptionsMonitor<EmailOption> optionsMonitor, IOptionsSnapshot<EmailOption> optionsSnapshot ) { _options = options; _optionsMonitor = optionsMonitor; _optionsSnapshot = optionsSnapshot; } public IActionResult Demo () { EmailOption defaultEmailOption = _options.Value; EmailOption defaultEmailOption1 = _optionsMonitor.CurrentValue; EmailOption fromMemoryEmailOption1 = _optionsMonitor.Get("FromMemory" ); EmailOption fromConfigurationEmailOption1 = _optionsMonitor.Get("FromConfiguration" ); EmailOption defaultEmailOption2 = _optionsSnapshot.Value; EmailOption fromMemoryEmailOption2 = _optionsSnapshot.Get("FromMemory" ); EmailOption fromConfigurationEmailOption2 = _optionsSnapshot.Get("FromConfiguration" ); return View(); } }
注意:如果是基于IConfiguration的TOptions需要进行监控,必须此IConfiguration是可监控的。
源码解析 我们在配置Options的时候,其实会向容器内部注入IConfigureOptions或者IConfigureNamedOptions以及IPostConfigureOptions这几种对象。随后负责创建TOptions的工厂类 IOptionsFactory,也以注入的形式获取这几个对象来创建需要的TOptions。其中IConfigureNamedOptions继承于IConfigureOptions。相关的UML图如下:
IOptionsFactory的实现类是OptionsFactory,Create(string name)的核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public TOptions Create (string name ) { var options = new TOptions(); foreach (var setup in _setups) { if (setup is IConfigureNamedOptions<TOptions> namedSetup) { namedSetup.Configure(name, options); } else if (name == Options.DefaultName) { setup.Configure(options); } } foreach (var post in _postConfigures) { post.PostConfigure(name, options); } return options; }
到这里,我们知道了如何通过提供的配置信息,去产生一个TOptions对象。接下来我们看看 IOptions,IOptionsSnapshot,IOptionsMonitor是如何实现的,以及它们是如何实现配置源的动态更新。
每当我们调用Configure方法的时候,系统都会调用AddOptions,其内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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; }
其中IOptionsFactory就不必说,它就是用来产生对象的,这是它唯一的用处。而IOptions<>,IOptionsSnapshot<>的实现类都是OptionsManager。OptionsManager在创建时会注入IOptionsFactory,同时内部还有一个OptionsCache根据name保存产生的对象。**注意:这里的OptionsCache并不是注入到容器里的那个实例。**它的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class OptionsManager <TOptions > : IOptions <TOptions >, IOptionsSnapshot <TOptions > where TOptions : class , new () { private readonly IOptionsFactory<TOptions> _factory; private readonly OptionsCache<TOptions> _cache = new OptionsCache<TOptions>(); public OptionsManager (IOptionsFactory<TOptions> factory ) { _factory = factory; } public TOptions Value { get { return Get(Options.DefaultName); } } public virtual TOptions Get (string name ) { name = name ?? Options.DefaultName; return _cache.GetOrAdd(name, () => _factory.Create(name)); } }
IOptions在注册到容器时是以单例的形式,所以以这种方式产生的对象会被全局缓存起来(缓存在OptionsManager的内部OptionsCache里),也不会被更新,并且它只能获取默认名称的TOptions,但是它效率更高。
IOptionsSnapshot在注册到容器时是以Scoped的形式,所以这种方式产生的对象不会全局缓存,每一次请求都会创建新的对象,能觉察到配置源的改变。又因为它也有一个内部的OptionsCache,所以能做到同一请求周期内是不会改变的。
而IOptionsMonitor是以单例的形式注入到容器中,并且IOptionsMonitorCache也是单例的形式注入到容器中,这个IOptionsMonitorCache后续会在创建OptionsMonitor的时候注入进去,所以OptionsMonitor的缓存也是全局唯一的。但是我们之前已经说过,这个也是能觉察到配置源更新的,那又是如何实现的呢?那是因为还会注入一个IOptionsChangeTokenSource类型,它会觉察到配置源的改变,一旦发生改变就会告知OptionsMonitor从缓存中移除相应的对象。关于移除缓存的核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class OptionsMonitor <TOptions > : IOptionsMonitor <TOptions > where TOptions : class , new () { private readonly IOptionsMonitorCache<TOptions> _cache; private readonly IOptionsFactory<TOptions> _factory; private readonly IEnumerable<IOptionsChangeTokenSource<TOptions>> _sources; public OptionsMonitor (IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache ) { _factory = factory; _sources = sources; _cache = cache; foreach (var source in _sources) { ChangeToken.OnChange<string >( () => source.GetChangeToken(), (name) => InvokeChanged(name), source.Name); } } private void InvokeChanged (string name ) { name = name ?? Options.DefaultName; _cache.TryRemove(name); ... } }
IOptionsChangeTokenSource需要在配置Options的时候进行配置,如果我们配置的时候调用的IConfiguration的重载,那么他会自动注入一个ConfigurationChangeTokenSource,核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static IServiceCollection Configure <TOptions >(this IServiceCollection services, string name, IConfiguration config, Action<BinderOptions> configureBinder ) where TOptions : class { if (services == null ) { throw new ArgumentNullException(nameof (services)); } if (config == null ) { throw new ArgumentNullException(nameof (config)); } services.AddOptions(); services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config)); return services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder)); }
最佳实践 既然有如此多的获取方式,那应该如何选择?
如果TOption不需要监控且整个程序就只有一个同类型的TOption,那么强烈建议使用IOptions。
如果TOption需要监控或者整个程序有多个同类型的TOption,那么只能选择IOptionsMonitor或者IOptionsSnapshot。
当IOptionsMonitor和IOptionsSnapshot都可以选择时,如果Action是一个比较耗时的操作,那么建议使用IOptionsMonitor,反之选择IOptionsSnapshot
如果需要对配置源的更新做出反应时(不仅仅是配置对象TOptions本身的更新),那么只能使用IOptionsMonitor,并且注册回调。
本贴相关的代码和UML图在https://github.com/zhurongbo111/AspNetCoreDemo/tree/master/02-Options