前两篇介绍的都是已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!"); } }
注意到,我们.net core推荐面向接口开发,所以这里推荐使用了IConnectionFactory和IConnection接口。
另一方面,这些类的服务注册我们可以直接写在Startup中,但是推荐拓展方法做一层封装,然后在Startup中使用services.AddXXXXX()的形式注册,比如这里我们实现拓展类:
public static class ConnectionFactoryExtensions { /// <summary> /// 添加连接 /// </summary> /// <param name="services"></param> /// <param name="configuration"></param> /// <returns></returns> public static IServiceCollection AddConnectionFactory(this IServiceCollection services, IConfiguration configuration) { if (configuration == null) throw new ArgumentNullException(nameof(configuration)); services.Configure<ConnectionFactoryOptions>(configuration); services.TryAddSingleton<IConnectionFactory, ConnectionFactory>(); return services; } }
注意到,这里一般使用TryAddSingleton而不是AddSingleton,这样可以避免重复注册服务,而且,当我们注册不带名称的Options时,优先考虑使用IConfiguration,如果我们的Options数据不是来自IConfiguration,则可使用Action<TOptions>来实现。
假如我们在appsettings.json中有如下配置:
{ ... "ConnectionFactoryOptions": { "ConnectionString": "Oracle ConnectionString" } }
然后我们可以在Startup中这么写:
public void ConfigureServices(IServiceCollection services) { services.AddConnectionFactory(Configuration.GetSection("ConnectionFactoryOptions")); ... }
我们可以使用WebApi的接口Action做个Demo:
/// <summary> /// 不带名称的Connectin工厂测试 /// </summary> /// <returns></returns> [HttpGet("Connection")] public object Connection() { var factory = HttpContext.RequestServices.GetService<IConnectionFactory>(); var connection = factory.Create(); connection.Open(); //do something... Thread.Sleep(1000); connection.Close(); return "success"; }
运行项目,然后调用接口,就可以看到控制台输出:
保持项目处于运行状态,我们可以修改appsettings.json:
{ ... "ConnectionFactoryOptions": { "ConnectionString": "Mysql ConnectionString" } }
然后重新调用接口,你会发现Options重新加载了,其实这本质就是IConfiguration重新加载了:
带名称的Options
有时候,我们往DI中注册的同一类型服务使用Options可能不一样,这种情况多数表现在Client模式下,这个时候就可以采用名称作为区分,比如.netcore 提供的AddAuthentication认证服务注册方法,可以注册多种认证方式,它们使用不同的名称做区分,不同名称的认证方式使用不同的配置,当我们要使用某个名称的认证时,一般只需要在Action中使用AuthorizeAttribute特性修饰,同时制定使用的认证名称即可。
举个例子,比如我们有以下的Client和它的工厂:
public interface IClientFactory { /// <summary> /// 创建Client /// </summary> /// <param name="name"></param> /// <returns></returns> IClient Create(string name); } public class ClientFactory : IClientFactory { IOptionsMonitor<ClientOptions> optionsMonitor; public ClientFactory(IOptionsMonitor<ClientOptions> optionsMonitor) { this.optionsMonitor = optionsMonitor; } /// <summary> /// 创建Client /// </summary> /// <param name="name"></param> /// <returns></returns> public IClient Create(string name) { ClientOptions clientOptions = optionsMonitor.Get(name); return new Client(name, clientOptions); } } public class ClientOptions { /// <summary> /// 时间 /// </summary> public DateTime Time { get; set; } } public interface IClient { /// <summary> /// Do something /// </summary> void Invoke(); } public class Client : IClient { ClientOptions clientOptions; string name; public Client(string name, ClientOptions clientOptions) { this.name = name; this.clientOptions = clientOptions; } /// <summary> /// Do something /// </summary> public void Invoke() { Console.WriteLine($"{name}.Time:{clientOptions.Time:yyyy-MM-dd HH:mm:ss}"); } }
同样的,这里推荐使用面向接口开发,Startup中的注册推荐使用拓展方法封装:
public static class ClientFactoryExtensions { /// <summary> /// 添加Client /// </summary> /// <param name="services"></param> /// <param name="configure"></param> /// <returns></returns> public static IServiceCollection AddClientFactory(this IServiceCollection services, Action<ClientOptions> configure) => services.AddClientFactory(Options.DefaultName, configure); /// <summary> /// 添加Client /// </summary> /// <param name="services"></param> /// <param name="name"></param> /// <param name="configure"></param> /// <returns></returns> public static IServiceCollection AddClientFactory(this IServiceCollection services, string name, Action<ClientOptions> configure) { if (configure == null) throw new ArgumentNullException(nameof(configure)); services.Configure(name, configure); services.TryAddSingleton<IClientFactory, ClientFactory>(); return services; } }
往往,我们的Client配置不是从配置IConfiguration中读取的,所以一般使用Action<TOptions>作为配置载体,然后在Startup中使用:
public void ConfigureServices(IServiceCollection services) { services.AddClientFactory("Client1", options => { options.Time = DateTime.Now; }); services.AddClientFactory("Client2", options => { options.Time = DateTime.Now; }); ... }
同样的,我们可以使用WebApi接口来说明使用方法:
/// <summary> /// 带名称的Client工厂测试 /// </summary> /// <param name="name"></param> /// <returns></returns> [HttpGet("Client")] public object Client(string name) { var factory = HttpContext.RequestServices.GetService<IClientFactory>(); var client = factory.Create(name); client.Invoke(); return "success"; } /// <summary> /// 删除IOptionsMonitorCache中的缓存,可以触发重新创建Options /// </summary> /// <param name="name"></param> /// <returns></returns> [HttpGet("Refresh")] public object Refresh(string name) { var cache = HttpContext.RequestServices.GetService<IOptionsMonitorCache<ClientOptions>>(); cache.TryRemove(name);
Console.WriteLine("Refresh"); return "success"; }
运行起来后调用Client接口,控制台会输出:
因为我们使用的是IOptionsMonitor<TOptions>,它是有缓存存在的,因此每次创建的Options都是一样的,我们可以使用IOptionsMonitorCache<TOptions>来删除缓存,比如上面的Refresh接口:
前面说到,除了使用IOptionsMonitorCache<TOptions>来删除缓存,还可以同过注册IOptionsChangeTokenSource<TOptions>接口的服务来实现,比如这里我们可以添加它的一个通用实现类和拓展方法:
public interface ICommonOptionsChangeTokenSource { /// <summary> /// 触发 /// </summary> void Change(); } public class CommonOptionsChangeTokenSource<TOptions> : IOptionsChangeTokenSource<TOptions>, ICommonOptionsChangeTokenSource { CancellationTokenSource cancellationTokenSource; CancellationChangeToken cancellationChangeToken; public CommonOptionsChangeTokenSource(string name) { Name = name ?? Options.DefaultName; cancellationTokenSource = new CancellationTokenSource(); cancellationChangeToken = new CancellationChangeToken(cancellationTokenSource.Token); } public string Name { get; } public IChangeToken GetChangeToken() { return cancellationChangeToken; } public void Change() { var _cancellationTokenSource = new CancellationTokenSource(); Interlocked.Exchange(ref cancellationChangeToken, new CancellationChangeToken(_cancellationTokenSource.Token)); Interlocked.Exchange(ref cancellationTokenSource, _cancellationTokenSource).Cancel(); } }
public static class CommonOptionsChangeTokenSourceExtensions { /// <summary> /// 添加IOptionsChangeTokenSource /// </summary> /// <typeparam name="TOptions"></typeparam> /// <param name="services"></param> /// <param name="action"></param> /// <returns></returns> public static IServiceCollection AddOptionsChangeTokenSource<TOptions>(this IServiceCollection services, Action<IServiceProvider, ICommonOptionsChangeTokenSource> action) => services.AddOptionsChangeTokenSource<TOptions>(Options.DefaultName, action); /// <summary> /// 添加IOptionsChangeTokenSource /// </summary> /// <typeparam name="TOptions"></typeparam> /// <param name="services"></param> /// <param name="name"></param> /// <param name="action"></param> /// <returns></returns> public static IServiceCollection AddOptionsChangeTokenSource<TOptions>(this IServiceCollection services, string name, Action<IServiceProvider, ICommonOptionsChangeTokenSource> action) { if (action == null) throw new ArgumentNullException(nameof(action)); return services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(serviceProvider => { var source = new CommonOptionsChangeTokenSource<TOptions>(name); action?.Invoke(serviceProvider, source); return source; }); } /// <summary> /// 添加IOptionsChangeTokenSource /// </summary> /// <typeparam name="TOptions"></typeparam> /// <param name="builder"></param> /// <param name="action"></param> /// <returns></returns> public static OptionsBuilder<TOptions> AddOptionsChangeTokenSource<TOptions>(this OptionsBuilder<TOptions> builder, Action<IServiceProvider, ICommonOptionsChangeTokenSource> action) where TOptions : class { builder.Services.AddOptionsChangeTokenSource<TOptions>(builder.Name, action); return builder; } /// <summary> /// 添加IOptionsChangeTokenSource /// </summary> /// <typeparam name="TOptions"></typeparam> /// <param name="services"></param> /// <param name="action"></param> /// <returns></returns> public static IServiceCollection AddOptionsChangeTokenSource<TOptions>(this IServiceCollection services, Action<ICommonOptionsChangeTokenSource> action) => services.AddOptionsChangeTokenSource<TOptions>(Options.DefaultName, action); /// <summary> /// 添加IOptionsChangeTokenSource /// </summary> /// <typeparam name="TOptions"></typeparam> /// <param name="services"></param> /// <param name="name"></param> /// <param name="action"></param> /// <returns></returns> public static IServiceCollection AddOptionsChangeTokenSource<TOptions>(this IServiceCollection services, string name, Action<ICommonOptionsChangeTokenSource> action) { if (action == null) throw new ArgumentNullException(nameof(action)); var source = new CommonOptionsChangeTokenSource<TOptions>(name); action?.Invoke(source); return services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(source); } /// <summary> /// 添加IOptionsChangeTokenSource /// </summary> /// <typeparam name="TOptions"></typeparam> /// <param name="builder"></param> /// <param name="action"></param> /// <returns></returns> public static OptionsBuilder<TOptions> AddOptionsChangeTokenSource<TOptions>(this OptionsBuilder<TOptions> builder, Action<ICommonOptionsChangeTokenSource> action) where TOptions : class { builder.Services.AddOptionsChangeTokenSource<TOptions>(builder.Name, action); return builder; } }
然后在Startup中使用:
public void ConfigureServices(IServiceCollection services) { services.AddOptionsChangeTokenSource<ClientOptions>("Client1", source => { //使用定时器来模拟触发重新创建Options System.Timers.Timer timer = new System.Timers.Timer(); timer.Elapsed += (s, e) => { source.Change(); }; timer.Interval = 3000;//3秒更新一次 timer.Start(); }); ... }
这里采用定时器模拟,真实环境可能是采用一条消息总线或者是消息队列的通知来实现。
这里为名称是Client1的Client添加定时刷新Options缓存的机制,而Client2不变,当运行项目后,再次调用Cient接口,会发现Client1的Time每个3秒刷新一次,而Client2则不变:
四、总结
有关Options的内容就说完了,把它和IConfiguration结合起来是一种非常好的配置形式,这也是.net core开发的基础,上面的例子也比较清楚,应该都能理解吧。