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强类型的映射已经完成。
Asp.NetCore源码学习[1-2]:配置[Option]的更多相关文章
- Asp.NetCore源码学习[2-1]:配置[Configuration]
Asp.NetCore源码学习[2-1]:配置[Configuration] 在Asp. NetCore中,配置系统支持不同的配置源(文件.环境变量等),虽然有多种的配置源,但是最终提供给系统使用的只 ...
- Asp.NetCore源码学习[2-1]:日志
Asp.NetCore源码学习[2-1]:日志 在一个系统中,日志是不可或缺的部分.对于.net而言有许多成熟的日志框架,包括Log4Net.NLog.Serilog 等等.你可以在系统中直接使用这些 ...
- 05.ElementUI源码学习:项目发布配置(github pages&npm package)
0x00.前言 书接上文.项目第一个组件已经封装好,说明文档也已编写好.下面需要将说明文档发布到外网上,以此来展示和推广项目,使用 Github Pages功能实现.同时将组件发布之 npm 上,方便 ...
- Vue2.0源码学习(4) - 合并配置
合并配置 通过之前的源码学习,我们已经了解到了new Vue主要有两种场景,第一种就是在外部主动调用new Vue创建一个实例,第二个就是代码内部创建子组件的时候自行创建一个new Vue实例.但是无 ...
- 【spring源码学习】spring配置的事务方式是REQUIRED,但业务层抛出TransactionRequiredException异常问题
(1)spring抛出异常的点:org.springframework.orm.jpa.EntityManagerFactoryUtils public static DataAccessExcept ...
- 源码学习之ASP.NET MVC Application Using Entity Framework
源码学习的重要性,再一次让人信服. ASP.NET MVC Application Using Entity Framework Code First 做MVC已经有段时间了,但看了一些CodePle ...
- geoserver源码学习与扩展——跨域访问配置
在 geoserver源码学习与扩展——restAPI访问 博客中提到了geoserver的跨域参数设置,本文详细讲一下geoserver的跨域访问配置. geoserver的跨域访问依赖java-p ...
- 源码学习系列之SpringBoot自动配置(篇一)
源码学习系列之SpringBoot自动配置源码学习(篇一) ok,本博客尝试跟一下Springboot的自动配置源码,做一下笔记记录,自动配置是Springboot的一个很关键的特性,也容易被忽略的属 ...
- 源码学习系列之SpringBoot自动配置(篇二)
源码学习系列之SpringBoot自动配置(篇二)之HttpEncodingAutoConfiguration 源码分析 继上一篇博客源码学习系列之SpringBoot自动配置(篇一)之后,本博客继续 ...
随机推荐
- js数字格式化(截断格式化或四舍五入格式化)
/*** * 数字格式化(适合金融产品截断小数位后展示) * @param num * @param pattern (标准格式:#,###.## 或#.## 或#,###00.00) * @para ...
- Linux mysql开启远程访问
默认情况下远程访问会出现 Can't connect to MySQL server on '192.168.10.18′ (10061) 错误是因为,mysql的默认配置为了增强安全性,禁止了非本机 ...
- Python小故事--------Tkinter的组件描述及解析
概念 Tkinter: 是Tk图形用户界面工具包标准(ctl)的Python接口,作为一个轻量级的跨平台图形用户界面(GUI)开发工具 frame: 屏幕上的一块矩形区域,多是用来作为容器(conta ...
- linux初学者-网络管理篇
linux初学者-网络管理篇 linux学习中,网络管理是非常重要的一个内容,本篇将会介绍一些ip.网关.DNS配置的一些基本内容. 1.ip配置 1.1.ip查询 在linux系统中一般可以使用& ...
- GitLab-CI 来自动创建 Docker 镜像
1.what is gitlab-ci docker image CI/CD 自动化集成,自动化部署.简单的说就是把代码提交到gitlab管理的同时部署到指定的server,打成docker imag ...
- javascript基础学习第三天
☞ 命名法: 小驼峰命名法 和 大驼峰命名法(帕斯卡命名法) 变量命名规则:遵循小驼峰命名法 [变量名第一个字母小写后面每一个单词的首字母大写] var userNameAge; 函数命名规则:遵循帕 ...
- TestNG独立运行的几种方法.md
目录 通过main函数调用testng.xml文件,然后打成可执行jar包执行 1 假设我们已经写好了testng.xml,现在我们需要写一个主类和main函数用来调用testng.xml 2 把项目 ...
- 【Android Studio】使用 Genymotion 调试出现错误 INSTALL_FAILED_CPU_ABI_INCOMPATI
RT -- 解决方法参考: https://my.oschina.net/u/242764/blog/375909 http://blog.csdn.net/wjr2012/article/detai ...
- Java1.8新特性实战
public class JDK8_features {private ArrayList<Integer> list; @Testpublic void test(){/*** 1.La ...
- 【原创】JAVA进程突然消失的原因?
引言 值此七夕佳节,烟哥放弃了无数妹纸的邀约,坐在电脑面前码字,就是为了给读者带来新的知识,这是一件伟大的事业! 好吧,实际情况是没人约.为了化解尴尬,我决定卖力写文章,嗯,一定是我过于屌丝! 好了, ...