Asp.NetCore源碼學習[1-2]:配置[Option]
在上一篇文章中,咱們知道了能夠經過
IConfiguration
訪問到注入的ConfigurationRoot
,可是這樣只能經過索引器IConfiguration["配置名"]
訪問配置。這篇文章將一下如何將IConfiguration
映射到強類型。git
Configuration
的用法指定須要配置的強類型MyOptions
和對應的IConfiguration
github
public void ConfigureServices(IServiceCollection services) { //使用Configuration配置Option services.Configure<MyOptions>(Configuration.GetSection("MyOptions")); //載入Configuration後再次進行配置 services.PostConfigure<MyOptions>(options=> { options.FilePath = "/"; }); }
在控制器中經過DI訪問強類型配置,一共有三種方法能夠訪問到強類型配置MyOptions
,分別是IOptions
、IOptionsSnapshot
、IOptionsMonitor
。先大概瞭解一下這三種方法的區別:json
public class ValuesController : ControllerBase { private readonly MyOptions _options1; private readonly MyOptions _options2; private readonly MyOptions _options3; private readonly IConfiguration _configurationRoot; public ValuesController(IConfiguration configurationRoot, IOptionsMonitor<MyOptions> options1, IOptionsSnapshot<MyOptions> options2, IOptions<MyOptions> options3 ) { //IConfiguration(ConfigurationRoot)隨着配置文件進行更新(須要IConfigurationProvider監聽配置源的更改) _configurationRoot = configurationRoot; //單例,監聽IConfiguration的IChangeToken,在配置源發生改變時,自動刪除緩存 //生成新的Option實例並綁定,加入緩存 _options1 = options1.CurrentValue; //scoped,每次請求從新生成Option實例並從IConfiguration獲取數據進行綁定 _options2 = options2.Value; //單例,從IConfiguration獲取數據進行綁定,只綁定一次 _options3 = options3.Value; } }
首先看看Configure擴展方法,方法很簡單,經過DI注入了Options須要的依賴。這裏注入了了三種訪問強類型配置的方法所需的全部依賴,接下來咱們按照這三種方法去分析源碼。緩存
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration config) where TOptions : class => services.Configure<TOptions>(Options.Options.DefaultName, config, _ => { }); public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, 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)); }
/// 爲IConfigurationSection實例註冊須要綁定的TOptions 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; }
IOptions
訪問強類型配置與其有關的注入只有三個:app
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>))); services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>))); services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
從以上代碼咱們知道,經過IOptions
訪問到的實際上是OptionsManager
實例。asp.net
OptionsManager
的實現經過IOptionsFactory<>
建立TOptions
實例,並使用OptionsCache<>
充當緩存。OptionsCache<>
其實是經過ConcurrentDictionary
實現了IOptionsMonitorCache
接口的緩存實現,相關代碼沒有展現。ide
public class OptionsManager<TOptions> : IOptions<TOptions>, IOptionsSnapshot<TOptions> where TOptions : class { private readonly IOptionsFactory<TOptions> _factory; // 單例OptionsManager的私有緩存,經過ConcurrentDictionary實現了 IOptionsMonitorCache接口 // Di中注入的單例OptionsCache<> 是給 OptionsMonitor<>使用的 private readonly OptionsCache<TOptions> _cache = new OptionsCache<TOptions>(); // Note: this is a private cache 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)); } }
IOptionsFactory
的實現首先經過Activator
建立TOptions
的實例,而後經過IConfigureNamedOptions.Configure()
方法配置實例。該工廠類依賴於注入的一系列IConfigureOptions
,在Di中注入的實現爲NamedConfigureFromConfigurationOptions
,其經過委託保存了配置源和綁定的方法post
/// Options工廠類 生命週期:Transient /// 單例OptionsManager和單例OptionsMonitor持有不一樣的工廠實例 public class OptionsFactory<TOptions> : IOptionsFactory<TOptions> where TOptions : class { private readonly IEnumerable<IConfigureOptions<TOptions>> _setups; private readonly IEnumerable<IPostConfigureOptions<TOptions>> _postConfigures; public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures) { _setups = setups; _postConfigures = postConfigures; } public TOptions Create(string name) { var options = CreateInstance(name); 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; } protected virtual TOptions CreateInstance(string name) { return Activator.CreateInstance<TOptions>(); } }
NamedConfigureFromConfigurationOptions
的實現在內部經過Action
委託,保存了IConfiguration.Bind()
方法。該方法實現了從IConfiguration
到TOptions
實例的賦值。
此處合併了NamedConfigureFromConfigurationOptions
和ConfigureNamedOptions
的代碼。性能
public class NamedConfigureFromConfigurationOptions<TOptions> : ConfigureNamedOptions<TOptions> where TOptions : class { public NamedConfigureFromConfigurationOptions(string name, IConfiguration config) : this(name, config, _ => { }) { } public NamedConfigureFromConfigurationOptions(string name, IConfiguration config, Action<BinderOptions> configureBinder) : this(name, options => config.Bind(options, configureBinder)) { } public ConfigureNamedOptions(string name, Action<TOptions> action) { Name = name; Action = action; } public string Name { get; } public Action<TOptions> Action { get; } public virtual void Configure(string name, TOptions options) { if (Name == null || name == Name) { Action?.Invoke(options); } } public void Configure(TOptions options) => Configure(string.Empty, options); }
因爲
OptionsManager<>
是單例模式,只會從IConfiguration
中獲取一次數據,在配置發生更改後,OptionsManager<>
返回的TOptions
實例不會更新。學習
IOptionsSnapshot
訪問強類型配置該方法和第一種相同,惟一不一樣的是,在注入DI系統的時候,其生命週期爲scoped,每次請求從新建立OptionsManager<>
。這樣每次獲取TOptions
實例時,會新建實例並從IConfiguration
從新獲取數據對其賦值,那麼TOptions
實例的值天然就是最新的。
IOptionsMonitor
訪問強類型配置與其有關的注入有五個:
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>))); services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>))); services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>))); services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config)); services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
第二種方法在每次請求時,都新建實例進行綁定,對性能會有影響。如何監測IConfiguration
的變化,在變化的時候進行從新獲取TOptions
實例呢?答案是經過IChangeToken
去監聽配置源的改變。從上一篇知道,當使用FileProviders
監聽文件更改時,會返回一個IChangeToken
,在FileProviders
中監聽返回的IChangeToken
能夠得知文件發生了更改並進行從新加載文件數據。因此使用IConfiguration
訪問到的ConfigurationRoot
永遠都是最新的。在IConfigurationProvider
和IConfigurationRoot
中也維護了IChangeToken
字段,這是用於向外部一層層的傳遞更改通知。下圖爲更改通知的傳遞方向:
graph LR A["FileProviders"]--IChangeToken-->B B["IConfigurationProvider"]--IChangeToken-->C["IConfigurationRoot"]
因爲NamedConfigureFromConfigurationOptions
沒有直接保存IConfiguration
字段,因此沒辦法經過它獲取IConfiguration.GetReloadToken()
。在源碼中經過注入ConfigurationChangeTokenSource
實現獲取IChangeToken
的目的
ConfigurationChangeTokenSource
的實現該類保存IConfiguration
,並實現IOptionsChangeTokenSource
接口
public class ConfigurationChangeTokenSource<TOptions> : IOptionsChangeTokenSource<TOptions> { private IConfiguration _config; public ConfigurationChangeTokenSource(IConfiguration config) : this(string.Empty, config) { } public ConfigurationChangeTokenSource(string name, IConfiguration config) { _config = config; Name = name ?? string.Empty; } public string Name { get; } public IChangeToken GetChangeToken() { return _config.GetReloadToken(); } }
OptionsMonitor
的實現該類經過IOptionsChangeTokenSource
獲取IConfiguration
的IChangeToken
。經過監聽更改通知,在配置源發生改變時,刪除緩存,從新綁定強類型配置,並加入到緩存中。IOptionsMonitor
接口還有一個OnChange()
方法,能夠註冊更改通知發生時候的回調方法,在TOptions
實例發生更改的時候,進行回調。值得一提的是,該類有一個內部類ChangeTrackerDisposable
,在註冊回調方法時,返回該類型,在須要取消回調時,經過ChangeTrackerDisposable.Dispose()
取消剛剛註冊的方法。
public class OptionsMonitor<TOptions> : IOptionsMonitor<TOptions>, IDisposable where TOptions : class { private readonly IOptionsMonitorCache<TOptions> _cache; private readonly IOptionsFactory<TOptions> _factory; private readonly IEnumerable<IOptionsChangeTokenSource<TOptions>> _sources; private readonly List<IDisposable> _registrations = new List<IDisposable>(); internal event Action<TOptions, string> _onChange; public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache) { _factory = factory; _sources = sources; _cache = cache; foreach (var source in _sources) { var registration = ChangeToken.OnChange( () => source.GetChangeToken(), (name) => InvokeChanged(name), source.Name); _registrations.Add(registration); } } private void InvokeChanged(string name) { name = name ?? Options.DefaultName; _cache.TryRemove(name); var options = Get(name); if (_onChange != null) { _onChange.Invoke(options, name); } } public TOptions CurrentValue { get => Get(Options.DefaultName); } public virtual TOptions Get(string name) { name = name ?? Options.DefaultName; return _cache.GetOrAdd(name, () => _factory.Create(name)); } public IDisposable OnChange(Action<TOptions, string> listener) { var disposable = new ChangeTrackerDisposable(this, listener); _onChange += disposable.OnChange; return disposable; } public void Dispose() { foreach (var registration in _registrations) { registration.Dispose(); } _registrations.Clear(); } internal class ChangeTrackerDisposable : IDisposable { private readonly Action<TOptions, string> _listener; private readonly OptionsMonitor<TOptions> _monitor; public ChangeTrackerDisposable(OptionsMonitor<TOptions> monitor, Action<TOptions, string> listener) { _listener = listener; _monitor = monitor; } public void OnChange(TOptions options, string name) => _listener.Invoke(options, name); public void Dispose() => _monitor._onChange -= OnChange; } }
本篇文章中,因爲Option依賴於自帶的注入系統,而本項目中Di部分尚未完成,因此,這篇文章的測試代碼直接new依賴的對象。
public class ConfigurationTest { public static void Run() { var builder = new ConfigurationBuilder(); builder.AddJsonFile(null, $@"C:\WorkStation\Code\GitHubCode\CoreApp\CoreWebApp\appsettings.json", true,true); var configuration = builder.Build(); Task.Run(() => { ChangeToken.OnChange(() => configuration.GetReloadToken(), () => { Console.WriteLine("Configuration has changed"); }); }); var optionsChangeTokenSource = new ConfigurationChangeTokenSource<MyOption>(configuration); var configureOptions = new NamedConfigureFromConfigurationOptions<MyOption>(string.Empty, configuration); var optionsFactory = new OptionsFactory<MyOption>(new List<IConfigureOptions<MyOption>>() { configureOptions },new List<IPostConfigureOptions<MyOption>>()); var optionsMonitor = new OptionsMonitor<MyOption>(optionsFactory,new List<IOptionsChangeTokenSource<MyOption>>() { optionsChangeTokenSource },new OptionsCache<MyOption>()); optionsMonitor.OnChange((option,name) => { Console.WriteLine($@"optionsMonitor Detected Configuration has changed,current Value is {option.TestOption}"); }); Thread.Sleep(600000); } }
回調會觸發兩次,這是因爲FileSystemWatcher
形成的,能夠經過設置一個後臺線程,在檢測到文件變化時,主線程將標誌位置true,後臺線程輪詢標誌位
---
至此,從IConfiguration
到TOptions
強類型的映射已經完成。