前两篇介绍的都是已IConfiguration为基础的配置,这里在说说.net core提供的一种全新的辅助配置机制:Options。
Options,翻译成中文就是选项,可选择的意思,它依赖于.net core提供的DI机制(DI机制以后再说),Options的对象是具有空构造函数的类。
Options是一个独立的拓展库,它不像IConfiguration那样可以从外部文件获取配置,它其实可以理解为一种代码层面的配置,.net core内部大量的实现类采用了IOptions机制,基本上,.net core中任何一个依赖DI存在的库,或多或少都会有Options的影子,比如日志的LoggerFilterOptions,认证授权的AuthenticationOptions等等,
一、原理
想了一下,这里原理的介绍可以分成两个部分:配置和读取
配置
Options的配置一般采用IServiceCollection的Configure,ConfigureAll,PostConfigure,PostConfigureAll,ConfigureOptions和带泛型参数的AddOptions<TOptions>等拓展方法以及他们的重载来实现,同时,Options可以指定一个名称,用来区分同一类型的Options,如果不指定名称,那么默认将采用Options.DefaultName(源码)作为名称,其实也就是空字符串(不是null,当名称是null时代表全部,后面介绍)。其实这几个方法的本质就是往DI容器中注册IConfigureOptions<TOptions>(源码)或者IPostConfigureOptions<TOptions>(源码)接口的服务,只不过注册进去的类或者名称不一样而已,可以查看源码(源码)。
Configure和ConfigureAll
Configure和ConfigureAll是最主要的配置入口,对同一个类型可以多次进行配置,其中,Configure是对指定名称的Options进行配置,而ConfigureAll是对同一类型的所有Options进行配置,其实ConfigureAll(action)等价于Configure(null,action),这里是前面说的Options的默认名称不是null,而是空字符串(源码):
public static IServiceCollection ConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class => services.Configure(name: null, configureOptions: configureOptions);
所以我们只需要关注Configure方法就可以了,Configure注册的服务是ConfigureNamedOptions<TOptions>(源码),它实现了IConfigureNamedOptions<TOptions>接口,而IConfigureNamedOptions<TOptions>接口是IConfigureOptions<TOptions>接口的一个子接口,接口实现内容如下(源码):
// IConfigureNamedOptions<TOptions>接口实现
public virtual void Configure(string name, TOptions options) { if (options == null) { throw new ArgumentNullException(nameof(options)); } // Null name is used to configure all named options. if (Name == null || name == Name) { Action?.Invoke(options); } } public void Configure(TOptions options) => Configure(Options.DefaultName, options);// IConfigureOptions<TOptions>接口实现
从实现方法也可以看到,当Options的名称为null时,表示对所有此类型的Options均进行配置。
总之,我们只需要记住,Configure和ConfigureAll方法只是往DI中对IConfigureOptions<TOptions>接口注册ConfigureNamedOptions<TOptions>服务,只不过ConfigureAll注册的名称是null,Configure注册的名称默认是Options.DefaultName。
PostConfigure和PostConfigureAll
有了Configure和ConfigureAll,为什么还要有PostConfigure和PostConfigureAll?举个例子,我们要组装车子,Configure1配置好了轮子,Configure2配置好了车架,Configure3配置好了内饰,那组装要等这三个配置好了才能组装吧,这也就是PostConfigure的由来。
和ConfigureAll一样,PostConfigureAll与PostConfigure的区别就是PostConfigureAll使用的name是null(源码):
public static IServiceCollection PostConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class => services.PostConfigure(name: null, configureOptions: configureOptions);
所以,我们也只需要关注PostConfigure方法就可以了,而PostConfigure方法注册的服务是PostConfigureOptions<TOptions>(源码),它实现的是IPostConfigureOptions<TOptions>接口(源码):
public virtual void PostConfigure(string name, TOptions options) { if (options == null) { throw new ArgumentNullException(nameof(options)); } // Null name is used to initialize all named options. if (Name == null || name == Name) { Action?.Invoke(options); } }
实现内容几乎和ConfigureNamedOptions<TOptions>是一样的,总之,只需要记住,PostConfigure和PostConfigureAll方法只是往DI中对IPostConfigureOptions<TOptions>接口注册PostConfigureOptions<TOptions>服务,只不过PostConfigureAll注册的名称是null,PostConfigure注册的名称默认是Options.DefaultName。
ConfigureOptions
前面说到,无论是Configure还是PostConfigure,都是往DI容器中注册IConfigureOptions<TOptions>和IPostConfigureOptions<TOptions>的服务,但是他们配置的载体是委托Action,因此,ConfigureOptions方法允许我们自己以类的形式作为载体去进行配置,只不过需要我们自己去实现IConfigureOptions<TOptions>或IPostConfigureOptions<TOptions>接口,或者我们也可以使用默认实现好了的几个Options:ConfigureOptions<TOptions>、ConfigureNamedOptions<TOptions>和PostConfigureOptions<TOptions>,如果自己实现,比如有实现类:
public class TestConfigureOptions : IConfigureOptions<TestOptions>, IPostConfigureOptions<TestOptions> { public void Configure(TestOptions options) { //配置 } public void PostConfigure(string name, TestOptions options) { //配置 } } public class TestOptions { //属性 }
然后可以使用ConfigureOptions方法配置了:
public void ConfigureServices(IServiceCollection services) { services.ConfigureOptions<TestConfigureOptions>(); ... }
AddOptions<TOptions>
带泛型的AddOptions<TOptions>方法返回一个OptionsBuilder<TOptions>方法(源码),它则可进行更多的配置,比如上面Configure和PostConfigure方法的功能,但是OptionsBuilder<TOptions>只是配置包含名称的Options,默认名称就是Options.DefaultName,也就是说OptionsBuilder<TOptions>无法配置像ConfigureAll和PostConfigureAll那样的功能。
OptionsBuilder<TOptions>除了包含Configure和PostConfigure方法的功能,主要还有几个功能:
1、OptionsBuilder<TOptions>允许我们从DI中获取服务或者其他配置来进行操作进一步的配置,比如我们有下面的Options:
public class VarOptions { public int Var { get; set; } } public class SumOptions { public int Sum { get; set; } } public class MultipleOptions { public int Multiple { get; set; } }
然后我们使用配置:
public void ConfigureServices(IServiceCollection services) { services.Configure<VarOptions>("Var1", options => { options.Var = 1; }); services.Configure<VarOptions>("Var2", options => { options.Var = 2; }); services.AddOptions<SumOptions>().Configure<IOptionsFactory<VarOptions>>((options, factory) => { var varOption1 = factory.Create("Var1"); var varOption2 = factory.Create("Var2"); options.Sum = varOption1.Var + varOption2.Var; }); services.AddOptions<MultipleOptions>().Configure<IOptionsFactory<VarOptions>>((options, factory) => { var varOption1 = factory.Create("Var1"); var varOption2 = factory.Create("Var2"); options.Multiple = varOption1.Var * varOption2.Var; }); ... }
可以看到,VarOptions有两个名称:Var1和Var2,我们的SumOptions和MultipleOptions的配置是从DI中获取VarOptions的配置来生成的。
注意的是,OptionsBuilder<TOptions>的Configure和PostConfigure方法往DI中注册的服务也不一样,除了ConfigureNamedOptions<TOptions>和PostConfigureOptions<TOptions>,还会有很多ConfigureNamedOptions<TOptions,TDep1,TDep2...>和PostConfigureOptions<TOptions,TDep1,TDep2...>这样的服务实现类。
2、OptionsBuilder<TOptions>提供了Validate方法及它的重载,允许我们配置完Options后,可以自定义的对Options进行验证,比如上面我们将SumOptions增加验证,要求相加后的值要大于10:
services.AddOptions<SumOptions>().Configure<IOptionsFactory<VarOptions>>((options, factory) => { var varOption1 = factory.Create("Var1"); var varOption2 = factory.Create("Var2"); options.Sum = varOption1.Var + varOption2.Var; }).Validate(options => options.Sum > 10);
这样,当配置完SumOptions之后,在验证时,发现它的Sum属性不大于10,那么就会抛出异常了。
注意,这个验证是在获取配置使用的时候进行的
本质上,OptionsBuilder<TOptions>的Validate方法其实是往DI中注册IValidateOptions<TOptions>接口的服务:ValidateOptions<TOptions>和很多ValidateOptions<TOptions,TDep1,TDep2...>。
3、OptionsBuilder<TOptions>可以给Options增加特性验证,熟悉EF的朋友肯定都知道,我们可以是实体的属性增加一些特性,比如RequiredAttribute,MaxLengthAttribute等,然后EF就是自动帮我们进行验证了,同样的,我们也可以对Options使用这些特性,比如,我们有下面的一个Options:
public class TestOptions { [Required, MaxLength(5)] public string Value { get; set; } }
然后做下面的配置:
public void ConfigureServices(IServiceCollection services) { services.AddOptions<TestOptions>("Test1").Configure(options => { options.Value = null; }).ValidateDataAnnotations(); services.AddOptions<TestOptions>("Test2").Configure(options => { options.Value = "1234567890"; }).ValidateDataAnnotations(); services.AddOptions<TestOptions>("Test3").Configure(options => { options.Value = "abc"; }).ValidateDataAnnotations(); ... }
当我们获取名称是Test1的Options是会因为Required特性报错,当我们获取名称是Test2的Options时,会因为MaxLength(5)报错,而Test3是正确的。
另外,可以看到,这里验证只是使用了ValidateDataAnnotations方法(源码),其实它只是Options验证的一个拓展,它只不过是使用了DataAnnotationValidateOptions<TOptions>(源码)来做验证,而DataAnnotationValidateOptions<TOptions>就是实现了 IValidateOptions<TOptions>接口的一个类:
public static OptionsBuilder<TOptions> ValidateDataAnnotations<TOptions>(this OptionsBuilder<TOptions> optionsBuilder) where TOptions : class { optionsBuilder.Services.AddSingleton<IValidateOptions<TOptions>>(new DataAnnotationValidateOptions<TOptions>(optionsBuilder.Name)); return optionsBuilder; }
读取
Options的配置说完了,再看看读取。
无论是在配置的Configure,PostConfigure,还是ConfigureOptins,AddOptions<TOptions>方法,都是执行一个不带泛型参数的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; }
可以看到,这个方法就是注册5个类,它们就和Options读取有关,我们可以在服务(比如控制器)的构造函数中注入Options,比如:
[ApiController] [Route("[controller]")] public class HomeController : ControllerBase { public HomeController(IOptions<TestOptions> options, IOptionsFactory<TestOptions> optionsFactory, IOptionsMonitor<TestOptions> optionsMonitor, IOptionsSnapshot<TestOptions> optionsSnapshot, IOptionsMonitorCache<TestOptions> optionsMonitorCache) { var options1 = options.Value; var options2 = optionsFactory.Create(Options.DefaultName); var options3 = optionsMonitor.CurrentValue;//或者使用optionsMonitor.Get(name) var options4 = optionsSnapshot.Get(Options.DefaultName); var options5 = optionsMonitorCache.GetOrAdd(Options.DefaultName, () => new TestOptions()); } ... }
但是这五种方式的表现不一样:
IOptions<TOptions>:全局缓存配置(Singleton),也就是说Configure和PostConfigure等方法的配置内容只会被执行一遍,然后全局使用这一个配置 IOptionsSnapshot<TOptions>:范围内的配置(Scoped,这个以后DI中说,暂时可以认为一个http请求响应就是一个Scoped),也就是说一个Scoped范围内,Configure和PostConfigure等方法的配置内容只会被执行一遍 IOptionsMonitor<TOptions>:全局可监听的配置(Singleton),首先从IOptionsMonitorCache<TOptions>缓存加载,没有加载到则使用IOptionsFactory<TOptions>创建,同时我们可以注册IOptionsChangeTokenSource<TOptions>来进行监听,决定何时清除缓存然后重新创建Options IOptionsFactory<TOptions>:Options的创建工厂(Singleton),他没有缓存,直接创建Options,这样从某种层面来说有性能的损失。 IOptionsMonitorCache<TOptions>:IOptionsMonitor<TOptions>的缓存(Singleton),如果需要,我们可以直接从DI中获取缓存操作,来决定IOptionsMonitor<TOptions>接下来是从缓存中获取Options还是使用IOptionsFactory<TOptions>创建
另外它们的实现类也有区别:
1、IOptions<TOptions>和IOptionsSnapshot<TOptions>都是采用OptionsManager<TOptions>(源码),它的源码很简单,实际上就是从DI中获取IOptionsFactory<TOptions>工厂来创建Options
2、IOptionsMonitorCache<TOptions>的服务类是OptionsCache<TOptions>(源码),它其实就是管理Options集合的类,比如增加,移除,清空等等。
3、IOptionsFactory<TOptions>的服务类是OptionsFactory<TOptions>(源码),它从DI中获取TOptions的所有IConfigureOptions<TOptions>、IPostConfigureOptions<TOptions>和 IValidateOptions<TOptions>的服务类,可以看看它的Create方法(源码):
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);
}
if (_validations != null)
{
var failures = new List<string>();
foreach (var validate in _validations)
{
var result = validate.Validate(name, options);
if (result.Failed)
{
failures.AddRange(result.Failures);
}
}
if (failures.Count > 0)
{
throw new OptionsValidationException(name, typeof(TOptions), failures);
}
}
return options;
}
现在,上面不断介绍的往DI中注册的IConfigureOptions<TOptions>、IPostConfigureOptions<TOptions>和 IValidateOptions<TOptions>知道在哪里用,怎么用的了吧。
4、IOptionsMonitor<TOptions>的服务类是OptionsMonitor<TOptions>(源码),它注入IOptionsFactory<TOptions>,IOptionsMonitorCache<TOptions>,还有所有的IOptionsChangeTokenSource<TOptions>,它会优先从IOptionsMonitorCache<TOptions>缓存中获取Options,如果缓存没有,则使用IOptionsFactory<TOptions>创建并放入缓存中,而IOptionsChangeTokenSource<TOptions>是IOptionsMonitor<TOptions>的监听机制,它决定了IOptionsMonitorCache<TOptions>何时刷新,从而可以让IOptionsFactory<TOptions>去创建。
二、Options和IConfiguration
Options和IConfiguration是可以结合使用的,IConfiguration从外部读取配置,然后使用Options将配置读取到我们熟悉的实体中使用,还可以和IConfiguration的重新加载机制结合。
.net core中通过拓展IServiceCollection的Configure方法(源码)和OptionsBuilder<TOptions>的Bind方法(源码)来集合IConfiguration,不过最终都是同下面的Configure方法进行注册(源码):
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)); }
可以看到,它注册的是IConfigureOptions<TOptions>接口的NamedConfigureFromConfigurationOptions<TOptions>(源码)服务,而NamedConfigureFromConfigurationOptions<TOptions>只是ConfigureNamedOptions<TOptions>的一个子类,只不过NamedConfigureFromConfigurationOptions<TOptions>中是将IConfiguration中的配置值通过它的Bind拓展方法绑定到实体Options上。
另外,这里面还注册了IOptionsChangeTokenSource<TOptions>的服务ConfigurationChangeTokenSource<TOptions>(源码),它的作用就是将Options的监听与IConfiguration的重新加载机制结合起来。
在使用时,举个例子,比如appsettings.json有如下配置
{ ... "Data": { "Value1": 1, "Value2": 3.14, "Value3": true, "Value4": [ 1, 2, 3 ], "Value5": { "Value1": 2, "Value2": 5.20, "Value3": false, "Value4": [ 4,5,6,7 ] } } }
然后我们有一个对应的ptions
public class DataOptions { public int Value1 { get; set; } public decimal Value2 { get; set; } public bool Value3 { get; set; } public int[] Value4 { get; set; } public DataOptions Value5 { get; set; } }
然后只需要结合IConfiguration和Options注册即可:
public void ConfigureServices(IServiceCollection services) { services.Configure<DataOptions>(Configuration.GetSection("Data")); ... }
接下来就可以直接以Options的方式读取配置了
三、Options使用例子
下面例子的Demo已上传:https://pan.baidu.com/s/10mU79U6YYCj4-yQies6zRQ (提取码: yywq )
更多集成使用的Demo可以参考这里我封装实现的.net core对RabbitMQ,ActiveMQ,Kafka等操作的Demo:https://gitee.com/shanfeng1000/dotnetcore-demo
不带名称的Options
不带名称的Options常用于一些全局的配置,比如MvcOptions,或者一些创建工厂的配置Options,也就是说往往我们的DI中只存在一个服务类或者不用区分服务类的时候,往往使用的是不带名称的Options。
举个例子,比如我们有下面的连接工厂类及连接类:
public interface IConnectionFactory { /// <summary> /// 创建连接 /// </summary> /// <returns></returns> IConnection Create(); } public class ConnectionFactory : IConnectionFactory { IOptionsMonitor<ConnectionFactoryOptions> optionsMonitor; public ConnectionFactory(IOptionsMonitor<ConnectionFactoryOptions> optionsMonitor) { this.optionsMonitor = optionsMonitor; } /// <summary> /// 创建连接 /// </summary> /// <returns></returns> public IConnection Create() { return new Connection(optionsMonitor.CurrentValue.ConnectionString); } } public class ConnectionFactoryOptions { /// <summary> /// 连接字符串 /// </summary> public string ConnectionString { get; set; } } public interface IConnection { /// <summary> /// 打开连接 /// </summary> void Open(); /// <summary> /// 关闭连接 /// </summary> void Close(); } public class Connection : IConnection { string connectionString; public Connection(string connectionString) { this.connectionString = connectionString; } /// <summary> /// 打开连接 /// </summary> public void Open() { Console.WriteLine("Connecting:" + connectionString); Console.WriteLine("Connection Opened!"); } /// <summary> /// 关闭连接 /// </summary> public void Close() { Console.WriteLine("Disconnecting:" + connectionString); Console.WriteLine("Connection Closed!"); } }