前兩篇介紹的都是已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開發的基礎,上面的例子也比較清楚,應該都能理解吧。