.Net Core 中選項Options的具體實現
目錄
- 由程式碼開始
- 定義一個使用者配置選項
- 定義on配置檔案:myconfig.json
- 建立ServiceCollection
- 示例程式碼
- 程式碼執行結果
- 通過執行程式碼得到的結論
- 問題
- 配合原始碼解決疑惑
- Configure注入
- OptionsManager
- OptionsFactory
- NamedConfigureFromConfigurationOptions
- ValidateOptions
- 結論
.NetCore的配置選項建議結合在一起學習,不瞭解.NetCore 配置Configuration的同學可以看下我的上一篇文章 [.Net Core配置Configuration具體實現]
由程式碼開始
定義一個使用者配置選項
public class UserOptions { private string instanceId; private static int index = 0; public UserOptions() { instanceId = (++index).ToString("00"); Console.WriteLine($"Create UserOptions Instance:{instanceId}"); } public string Name { get; set; } public int Age { get; set; } public override string ToString() => $"Name:{Name} Age:{Age} Instance:{instanceId} "; } public class UserOptions2 { public string Name { get; set; } public int Age { get; set; } public override string ToString() => $" Name:{Name} Age:{Age}"; }
定義json配置檔案:myconfig.json
{ "UserOption": { "Name": "ConfigName-zhangsan","Age": 666 } }
建立ServiceCollection
services = new ServiceCollection(); var configBuilder = new ConfigurationBuilder().AddInMemoryCollection().AddJsonFile("myconfig.json",true,true); var iconfiguration = configBuilder.Build(); services.AddSingleton<IConfiguration>(iconfiguration);
示例程式碼
services.Configure<UserOptions>(x => { x.Name = "張三"; x.Age = new Random().Next(1,10000); }); services.AddOptions<UserOptions2>().Configure<IConfiguration>((x,config) => { x.Name = config["UserOption:Name"]; x.Age = 100; }); ; services.PostConfigure<UserOptions>(x => { x.Name = x.Name + "Post"; x.Age = x.Age; }); services.Configure<UserOptions>("default",x => { x.Name = "Default-張三"; x.Age = new Random().Next(1,10000); }); services.Configure<UserOptions>("config",configuration.GetSection("UserOption")); using (var provider = services.BuildServiceProvider()) { using (var scope1 = provider.CreateScope()) { PrintOptions(scope1,"Scope1"); } //修改配置檔案 Console.WriteLine(string.Empty); Console.WriteLine("修改配置檔案"); var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,"myconfig.json"); File.WriteAllText(filePath,"{\"UserOption\": { \"Name\": \"ConfigName-lisi\",\"Age\": 777}}"); //配置檔案的change回撥事件需要一定時間執行 Thread.Sleep(300); Console.WriteLine(string.Empty); using (var scope2 = provider.CreateScope()) { PrintOptions(scope2,"Scope2"); } Console.WriteLine(string.Empty); using (var scope3 = provider.CreateScope()) { PrintOptions(scope3,"Scope3"); } } static void PrintOptions(IServicewww.cppcns.comScope scope,string scopeName) { var options1 = scope.ServiceProvider.GetService<IOptions<UserOptions>>(); Console.WriteLine($"手動注入讀取,IOptions,{scopeName}-----{ options1.Value}"); var options2 = scope.ServiceProvider.GetService<IOptionsSnapshot<UserOptions>>(); Console.WriteLine($"配置檔案讀取,IOptionsSnapshot,{scopeName}-----{ options2.Value}"); var options3 = scope.ServiceProvider.GetService<IOptionsSnapshot<UserOptions>>(); Console.WriteLine($"配置檔案根據名稱讀取,{scopeName}-----{ options3.Get("config")}"); var options4 = scope.ServiceProvider.GetService<IOptionsMonitor<UserOptions>>(); Console.WriteLine($"配置檔案讀取,IOptionsMonitor,{scopeName}-----{ options4.CurrentValue}"); var options5 = scope.ServiceProvider.GetService<IOptionsMonitor<UserOptions>>(); Console.WriteLine($"配置檔案根據名稱讀取,{scopeName}-----{options5.Get("config")}"); var options6 = scope.ServiceProvider.GetService<IOptions<UserOptions2>>(); Console.WriteLine($"Options2-----{options6.Value}"); }
程式碼執行結果
Create UserOptions Instance:01
手動注入讀取,Scope1----- Name:張三Post Age:6575 Instance:01
Create UserOptions Instance:02
配置檔案讀取,Scope1----- Name:張三Post Age:835 Instance:02
Create UserOptions Instance:03
配置檔案根據名稱讀取,Scope1----- Name:ConfigName-zhangsan Age:666 Instance:03
Create UserOptions Instance:04
配置檔案讀取,Scope1----- Name:張三Post Age:1669 Instance:04
Create UserOptions Instance:05
配置檔案根據名稱讀取,Scope1----- Name:ConfigName-zhangsan Age:666 Instance:05
Options2----- Name:ConfigName-zhangsan Age:100修改配置檔案
Create UserOptions Instance:06手動注入讀取,Scope2----- Name:張三Post Age:6575 Instance:01
Create UserOptions Instance:07
配置檔案讀取,Scope2----- Name:張三Post Age:5460 Instance:07
Create UserOptions Instance:08
配置檔案根據名稱讀取,Scope2----- Name:ConfigName-lisi Age:777 Instance:08
配置檔案讀取,Scope2----- Name:張三Post Age:1669 Instance:04
配置檔案根據名稱讀取,Scope2----- Name:ConfigName-lisi Age:777 Instance:06
Options2----- Name:ConfigName-zhangsan Age:100手動注入讀取,Scope3----- Name:張三Post Age:6575 Instance:01
Create UserOptions Instance:09
配置檔案讀取,Scope3----- Name:張三Post Age:5038 Instance:09
Create UserOptions Instance:10
配置檔案根據名稱讀取,Scope3----- Name:ConfigName-lisi Age:777 Instance:10
配置檔案讀取,Scope3----- Name:張三Post Age:1669 Instance:04
配置檔案根據名稱讀取,Scope3www.cppcns.com----- Name:ConfigName-lisi Age:777 Instance:06
Options2----- Name:ConfigName-zhangsan Age:100
通過執行程式碼得到的結論
- Options可通過手動初始化配置項配置(可在配置時讀取依賴注入的物件)、或通過IConfiguration繫結配置
- PostConfiger可在Configer基礎上繼續配置
- 可通過IOptionsSnapshot或IOptionsMonitor根據配置名稱讀取配置項,未指定名稱讀取第一個注入的配置
- IOptions和IOptionsMonitor生命週期為Singleton,IOptionsSnapshot生命週期為Scope
- IOptionsMonitor可監聽到配置檔案變動去動態更新配置項
問題
- IOptions,IOptionsMonitor 如何/何時注入、初始化
- Options指定名稱時內部是如何設定的
- Options如何繫結的IConfiguration
- IOptionsMonitor是如何同步配置檔案變動的
配合原始碼解決疑惑
Configure注入
public static IServiceCollection Configure<TOptions>(this IServiceCollection services,Action<TOptions> configureOptions) where TOptions : class { return services.Configure(Microsoft.Extensions.Options.Options.DefaultName,configureOptions); } public static IServiceCollection Configure<TOptions>(this IServiceCollection services,string name,Action<TOptions> configureOptions) where TOptions : class { services.AddOptions(); services.AddSingleton((IConfigureOptions<TOptions>)new ConfigureNamedOptions<TOptions>(name,configureOptions)); return services; } public static IServiceCollection AddOptions(this IServiceCollection 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; }
通過上面的原始碼可以發現,Options相關類是在AddOptions中注入的,具體的配置項在Configure中注入。
如果不指定Configure的Name,也會有個預設的Name=Microsoft.Extensions.Options.Options.DefaultName
那麼我們具體的配置項存到哪裡去了呢,在ConfigureNamedOptions這個類中,在Configer函式呼叫時,只是把相關的配置委託存了起來:
public ConfigureNamedOptions(string name,Action<TOptions> action) { Name = name; Action = action; }
OptionsManager
private readonly ConcurrentDictionary<string,Lazy<TOptions>> _cache = new ConcurrentDictionary<string,Lazy<TOptions>>(StringComparer.Ordinal); public TOptions Value => Get(Options.DefaultName); public virtual TOptions Get(string name) { name = name ?? Options.DefaultName; return _cache.GetOrAdd(name,() => _factory.Create(name)); }
OptionsManager實現相對較簡單,在查詢時需要執行Name,如果為空就用預設的Name,如果快取沒有,就用Factory建立一個,否則就讀快取中的選項。
IOptions和IOptionsSnapshot的實現類都是OptionsManager,只是生命週期不同。
OptionsFactory
那麼OptionsFactory又是如何建立Options的呢?我們看一下他的建構函式,建構函式將所有Configure和PostConfigure的初始化委託都通過建構函式儲存在內部變數中
public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups,IEnumerable<IPostConfigureOptions<TOptions>> postConfigures) { _setups = setups; _postConfigures = postConfigures; }
接下來看Create(有刪改,與本次研究無關的程式碼沒有貼出來):
public TOptions Create(string name) { //首先建立對應Options的例項 TOptions val = Activator.CreateInstance<TOptions>(); //迴圈所有的配置項,依次執行,如果對同一個Options配置了多次,最後一次的賦值生效 foreach (IConfigureOptions<TOptions> setup in _setups) { var configureNamedOptions = setup as IConfigureNamedOptions<TOptions>; if (configureNamedOptions != null) { //Configure中會判斷傳入Name的值與本身的Name值是否相同,不同則不執行Action //這解釋了我們一開始的示例中,注入了三個UserOptions,但是在IOptionsSnapshot.Value中獲取到的是第一個沒有名字的 //因為Value會呼叫OptionsManager.Get(Options.DefaultName),進而呼叫Factory的Create(Options.DefaultName) configureNamedOptions.Configure(name,val); } else if (name == Options.DefaultName) { setup.Configure(val); } } //PostConfigure沒啥可多說了,名字判斷邏輯與Configure一樣 foreach (var postConfigure in _postConfigures) { postConfigure.PostConfigure(name,val); } return val; }
NamedConfigureFromConfigurationOptions
IConfiguration配置Options的方式略有不同
對應Configure擴充套件方法最終呼叫的程式碼在Microsoft.Extensions.DependencyInjection.OptionsConfigurationServiceCollectionExtensions這個類中
public static IServiceCollection Configure<TOptions>(this IServiceCollection services,IConfiguration config,Action<BinderOptions> configureBinder) where TOptions : class { services.AddOptions(); services.AddSingleton((IOptionsChangeTokenSource<TOptions>)new ConfigurationChangeTokenSource<TOptions>(name,config)); return services.AddSingleton((IConfigureOptions<TOptions>)new NamedConfigureFromConfigurationOptions<TOptions>(name,config,configureBinder)); }
擴充套件方法裡又注入了一個IOptionsChangeTokenSource,這個類的作用是提供一個配置檔案變動監聽的Token
同時將IConfigureOptions實現類註冊成了NamedConfigureFromConfigurationOptions
NamedConfigureFromConfigurationOptions繼承了ConfigureNamedOptions,在建構函式中用IConfiguration.Bind實現了生成Options的委託
public NamedConfigureFromConfigurationOptions(string name,Action<BinderOptions> configureBinder) : base(name,(Action<TOptions>)delegate(TOptions options) { config.Bind(options,configureBinder); })
所以在Factory的Create函式中,會呼叫IConfiguration的Bind函式
由於IOptionsSnapshot生命週期是Scope,在配置檔案變動後新的Scope中會獲取最新的Options
ValidateOptions
OptionsBuilder還包含了一個Validate函式,該函式要求傳入一個Func<TOptions,bool>的委託,會注入一個單例的ValidateOptions物件。
在OptionsFactory構建Options的時候會驗證Options的有效性,驗證失敗會丟擲OptionsValidationException異常
對於ValidateOptions和PostConfigureOptions都是構建Options例項時需要用到的主要模組,不過使用和內部實現都較為簡單,應用場景也不是很多,本文就不對這兩個類多做介紹了
結論
在Configure擴充套件函式中會首先呼叫AddOptions函式
IOptions,IOptionsMonitor都是在AddOptions函式中注入的
Configure配置的選項配置委託最終會儲存到ConfigureNamedOptions或NamedConfigureFromConfigurationOptions
IOptions和IOptionsSnapshot的實現類為OptionsManager
OptionsManager通過OptionsFactory建立Options的例項,並會以Name作為鍵存到字典中快取例項
OptionsFactory會通過反射建立Options的例項,並呼叫ConfigureNamedOptions中的委託給例項賦值
現在只剩下最後一個問題了,OptionsMonitor是如何動態更新選項的呢?
其實前面的講解中已經提到了一個關鍵的介面IOptionsChangeTokenSource,這個介面提供一個IChangeToken,通過ChangeToken監聽這個Token就可以監聽到檔案的變動,我們來看下OptionsMonitor是否是這樣做的吧!
//建構函式 public OptionsMonitor(IOptionsFactory<TOptions> factory,IEnumerable<IOptionsChangeTokenSource<TOptions>> sources,IOptionsMonitorCache<TOptions> cache) { _factory = factory; _sources = sources; _cache = cache; //迴圈屬於TOptions的所有IChangeToken foreach (IOptionsChangeTokenSource<TOptions> source in _sources) { ChangeToken.OnChange(() => source.GetChangeToken(),delegate(string name) { //清除快取 name = name ?? Options.DefaultName; _cache.TryRemove(name); },source.Name); } } public virtual TOptions Get(string name) { name = name ?? Options.DefaultName; return _cache.GetOrAdd(name,() => _factory.Create(name)); }
果然是這樣的吧!
到此這篇關於.Net Core 中選項Options的具體實現的文章就介紹到這了,更多相關.Net Core Options內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!