Asp.NetCore源码学习[1-2]:配置[Option]
- 2019 年 10 月 3 日
- 筆記
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:WorkStationCodeGitHubCodeCoreAppCoreWebAppappsettings.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
强类型的映射已经完成。