在上一篇文章中, 我们知道了可以通过 IConfiguration 访问到注入的 ConfigurationRoot, 但是这样只能通过索引器 IConfiguration["配置名"] 访问配置. 这篇文章将一下如何将 IConfiguration 映射到强类型.
本系列源码地址 https://github.com/calanchenlins/CoreApp
一, 使用强类型访问 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 强类型的映射已经完成.
来源: https://www.cnblogs.com/Kane-Blake/p/11426415.html