Asp.NetCore原始碼學習[1-2]:配置[Option]
Asp.NetCore原始碼學習[1-2]:配置[Option]
在上一篇文章中,我們知道了可以通過
IConfiguration
訪問到注入的ConfigurationRoot
,但是這樣只能通過索引器IConfiguration["配置名"]
訪問配置。這篇文章將一下如何將IConfiguration
對映到強型別。
本系列原始碼地址
一、使用強型別訪問Configuration
的用法
指定需要配置的強型別MyOptions
和對應的IConfiguration
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
。先大概瞭解一下這三種方法的區別:
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;
}
1. 通過IOptions
訪問強型別配置
與其有關的注入只有三個:
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
例項。
1.1 OptionsManager
的實現
通過IOptionsFactory<>
建立TOptions
例項,並使用OptionsCache<>
充當快取。OptionsCache<>
實際上是通過ConcurrentDictionary
實現了IOptionsMonitorCache
介面的快取實現,相關程式碼沒有展示。
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));
}
}
1.2 IOptionsFactory
的實現
首先通過Activator
建立TOptions
的例項,然後通過IConfigureNamedOptions.Configure()
方法配置例項。該工廠類依賴於注入的一系列IConfigureOptions
,在Di中注入的實現為NamedConfigureFromConfigurationOptions
,其通過委託儲存了配置源和繫結的方法
/// 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>();
}
}
1.3 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
例項不會更新。
2. 通過IOptionsSnapshot
訪問強型別配置
該方法和第一種相同,唯一不同的是,在注入DI系統的時候,其生命週期為scoped,每次請求重新建立OptionsManager<>
。這樣每次獲取TOptions
例項時,會新建例項並從IConfiguration
重新獲取資料對其賦值,那麼TOptions
例項的值自然就是最新的。
3. 通過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
的目的
3.1 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();
}
}
3.2 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;
}
}
4. 測試程式碼
本篇文章中,由於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
強型別的對映已經完成