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,分别是IOptionsIOptionsSnapshotIOptionsMonitor。先大概了解一下这三种方法的区别:

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()方法。该方法实现了从IConfigurationTOptions实例的赋值。

此处合并了NamedConfigureFromConfigurationOptionsConfigureNamedOptions的代码。

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 永远都是最新的。在IConfigurationProviderIConfigurationRoot中也维护了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获取IConfigurationIChangeToken。通过监听更改通知,在配置源发生改变时,删除缓存,重新绑定强类型配置,并加入到缓存中。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,后台线程轮询标志位

结语

至此,从IConfigurationTOptions强类型的映射已经完成。

Asp.NetCore源码学习[1-2]:配置[Option]的更多相关文章

  1. Asp.NetCore源码学习[2-1]:配置[Configuration]

    Asp.NetCore源码学习[2-1]:配置[Configuration] 在Asp. NetCore中,配置系统支持不同的配置源(文件.环境变量等),虽然有多种的配置源,但是最终提供给系统使用的只 ...

  2. Asp.NetCore源码学习[2-1]:日志

    Asp.NetCore源码学习[2-1]:日志 在一个系统中,日志是不可或缺的部分.对于.net而言有许多成熟的日志框架,包括Log4Net.NLog.Serilog 等等.你可以在系统中直接使用这些 ...

  3. 05.ElementUI源码学习:项目发布配置(github pages&npm package)

    0x00.前言 书接上文.项目第一个组件已经封装好,说明文档也已编写好.下面需要将说明文档发布到外网上,以此来展示和推广项目,使用 Github Pages功能实现.同时将组件发布之 npm 上,方便 ...

  4. Vue2.0源码学习(4) - 合并配置

    合并配置 通过之前的源码学习,我们已经了解到了new Vue主要有两种场景,第一种就是在外部主动调用new Vue创建一个实例,第二个就是代码内部创建子组件的时候自行创建一个new Vue实例.但是无 ...

  5. 【spring源码学习】spring配置的事务方式是REQUIRED,但业务层抛出TransactionRequiredException异常问题

    (1)spring抛出异常的点:org.springframework.orm.jpa.EntityManagerFactoryUtils public static DataAccessExcept ...

  6. 源码学习之ASP.NET MVC Application Using Entity Framework

    源码学习的重要性,再一次让人信服. ASP.NET MVC Application Using Entity Framework Code First 做MVC已经有段时间了,但看了一些CodePle ...

  7. geoserver源码学习与扩展——跨域访问配置

    在 geoserver源码学习与扩展——restAPI访问 博客中提到了geoserver的跨域参数设置,本文详细讲一下geoserver的跨域访问配置. geoserver的跨域访问依赖java-p ...

  8. 源码学习系列之SpringBoot自动配置(篇一)

    源码学习系列之SpringBoot自动配置源码学习(篇一) ok,本博客尝试跟一下Springboot的自动配置源码,做一下笔记记录,自动配置是Springboot的一个很关键的特性,也容易被忽略的属 ...

  9. 源码学习系列之SpringBoot自动配置(篇二)

    源码学习系列之SpringBoot自动配置(篇二)之HttpEncodingAutoConfiguration 源码分析 继上一篇博客源码学习系列之SpringBoot自动配置(篇一)之后,本博客继续 ...

随机推荐

  1. springboot序

    springboot序 1.写在前面 (1) 前段时间把文章分了下类(说的是专栏,谈不上),分了三类:springboot.springcloud.mpp数据库greenplum,后来给springc ...

  2. 【区分】Typescript 中 interface 和 type

    在接触 ts 相关代码的过程中,总能看到 interface 和 type 的身影.只记得,曾经遇到 type 时不懂查阅过,记得他们很像,相同的功能用哪一个都可以实现.但最近总看到他们,就想深入的了 ...

  3. 关于ArcGIS api for JavaScript的一些内容

    2018-3-19 ArcGIS api for JavaScript 在3.4之后就已经抛弃了过时的写法,采用了AMD的写法,AMD规范即异步模块加载机制,这种规范让JS就像其它面向对象语言(比如J ...

  4. Android CHM文件阅读器

    CHM格式是1998年微软推出的基于HTML文件特性的帮助文件系统.以替代早先的winHelp帮助系统,它也是一种超文本标识语言.在Windows 98中把CHM类型文件称作“已编译的HTML帮助文件 ...

  5. 解决Oracle.DataAccess.Client.OracleConnection”的类型初始值设定项引发异常。

    解决Oracle.DataAccess.Client.OracleConnection”的类型初始值设定项引发异常. 这个问题他们说是oracle的版本问题 但是好像不是...(我感觉VS版本问题,我 ...

  6. java练习---1

    //程序员:罗元昊 2017.9.6public class Ap{ public static void main(String[] args){ System.out.println(" ...

  7. 摘录C#代码片段

    1.保存数据到指定文件String response = "";//数据 byte[] bytes = response.getBytes();//转化Byte string fi ...

  8. 动态规划_Apple Catching_POJ-2385

    It and ) in his field, each full of apples. Bessie cannot reach the apples when they are on the tree ...

  9. bug探索常识

    1.什么是Bug探索测试? 探索测试是将测试设计和测试执行整合在一起,形成的一种测试方法. 2.探索性测试怎么做? 需要认真思考和分析结果,并且在探索测试的过程中做记录. 3.探索性测试的好处? 可以 ...

  10. NDK jni mk文件 so文件 巴啦啦 初体验

    概念JNI(Java Native Interface,Java本地接口),实现了Java和其他语言的交互(主要是C/C++),如:Java程序通过JNI调用C/C++编写的在Windows上运行的D ...