本文主要介绍Options组件的原理和源码解析,但是主要介绍常用的一些用法,有一些不常用的模式,本文可能会跳过,因为内容太多.
在了解之前,需要掌握配置组件如何集成如Json配置文件等Provider,如有疑惑,请参考.net 5.0 配置文件组件之JsonProvider源码解析
1、调用代码
class Program
{
static void Main(string[] args)
{
var workDir = $"{Environment.CurrentDirectory}";
var builder = new ConfigurationBuilder()
.SetBasePath(workDir)
.AddJsonFile($"test.json", optional: true, reloadOnChange: true);
var root = (ConfigurationRoot)builder.Build();
var services = new ServiceCollection();
services.Configure<MySqlDbOptions>(root.GetSection("MySqlDbOptions"));
var provider=services.BuildServiceProvider();
var mySqlDbOptions = provider.GetRequiredService<IOptions<MySqlDbOptions>>().Value;
Console.ReadKey();
}
}
class MySqlDbOptions
{
public string ConnectionString { get; set; }
}
通过配置文件并集成JsonProvider,可以得到一个ConfigurationRoot实例,并且通过FileWatcher实现了和参数reloadOnChange配置文件监听,所以当手动改变json配置文件对应的ConfigurationRoot实例持有的Data数据源会发生改变.ok,开始介绍正文.
2、源码分析
(1)、Microsoft.Extensions.Options.ConfigurationExtensions组件部分
首先调用了 services.Configure<MySqlDbOptions>(root.GetSection("MySqlDbOptions"));看看这里发生了什么,源码如下:
[RequiresUnreferencedCode(OptionsBuilderConfigurationExtensions.TrimmingRequiredUnreferencedCodeMessage)]
public static IServiceCollection Configure<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>(this IServiceCollection services, string name, IConfiguration config) where TOptions : class
=> services.Configure<TOptions>(name, config, _ => { });
[RequiresUnreferencedCode(OptionsBuilderConfigurationExtensions.TrimmingRequiredUnreferencedCodeMessage)]
public static IServiceCollection Configure<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>(this IServiceCollection services, string name, IConfiguration config, Action<BinderOptions> configureBinder)
where TOptions : class
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}
services.AddOptions();
services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));
return services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
}
到这里IOptionsChangeTokenSource<TOptions>先不介绍,注入了NamedConfigureFromConfigurationOptions<TOptions>类型以IConfigureOptions<TOptions>接口注入,并传入了配置的名称,这里如果不指定默认未空字符串,并传入ConfigurationRoot实例,然后传入一个Action<BinderOptions> configureBinder配置绑定回调,因为使用Options组件就是为了将ConfigurationRoot实例持有的Data字典按照传入的条件传通过Microsoft.Extensions.Configuration.Binder组件(下面会介绍)绑定到传入的Options实例参数,并通过DI拿到配置实例,所以这里传入这个回调就是为了扩展,方便通过特定的业务逻辑进行参数转换.
接着NamedConfigureFromConfigurationOptions<TOptions>类型的构造函数,代码如下:
[RequiresUnreferencedCode(OptionsBuilderConfigurationExtensions.TrimmingRequiredUnreferencedCodeMessage)]
public NamedConfigureFromConfigurationOptions(string name, IConfiguration config)
: this(name, config, _ => { })
{ }
/// <summary>
/// Constructor that takes the <see cref="IConfiguration"/> instance to bind against.
/// </summary>
/// <param name="name">The name of the options instance.</param>
/// <param name="config">The <see cref="IConfiguration"/> instance.</param>
/// <param name="configureBinder">Used to configure the <see cref="BinderOptions"/>.</param>
[RequiresUnreferencedCode(OptionsBuilderConfigurationExtensions.TrimmingRequiredUnreferencedCodeMessage)]
public NamedConfigureFromConfigurationOptions(string name, IConfiguration config, Action<BinderOptions> configureBinder)
: base(name, options => BindFromOptions(options, config, configureBinder))
{
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}
}
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
Justification = "The only call to this method is the constructor which is already annotated as RequiresUnreferencedCode.")]
private static void BindFromOptions(TOptions options, IConfiguration config, Action<BinderOptions> configureBinder) => config.Bind(options, configureBinder);
到这里看不出什么,接着看this调用,代码如下:
/// <summary>
/// Constructor.
/// </summary>
/// <param name="name">The name of the options.</param>
/// <param name="action">The action to register.</param>
public ConfigureNamedOptions(string name, Action<TOptions> action)
{
Name = name;
Action = action;
}
ok,这里就明白了,将Options的名称,这里是空,和回调写入ConfigureNamedOptions实例,这里注意了,看下回调
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
Justification = "The only call to this method is the constructor which is already annotated as RequiresUnreferencedCode.")]
private static void BindFromOptions(TOptions options, IConfiguration config, Action<BinderOptions> configureBinder) => config.Bind(options, configureBinder);
这个回调会触发Microsoft.Extensions.Configuration.Binder组件的方法,先不介绍.到这里Microsoft.Extensions.Options.ConfigurationExtensions组件部分介绍完毕了.
(2)、Microsoft.Extensions.Options组件
(1)、完成了配置注入,那么如何像调用代码那样,通过IOptions<>拿到对应的配置,代码如下:
public static IServiceCollection AddOptions(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(UnnamedOptionsManager<>)));
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;
}
这里给了答案,当通过Ioptions<>释出配置实力的时候会释出UnnamedOptionsManager<>实例,接着就是调用.Value方法,代码如下:
public TOptions Value
{
get
{
if (_value is TOptions value)
{
return value;
}
lock (_syncObj ?? Interlocked.CompareExchange(ref _syncObj, new object(), null) ?? _syncObj)
{
return _value ??= _factory.Create(Options.DefaultName);
}
}
}
接着做了一下线程相关的处理加了锁,调用IOptionsFactory<TOptions>实例的Create方法,这里因为没有指定配置的名称,这里为空.注入时的Options名称也为空.接着看OptionsFactory<>实例的构造函数,这里看IEnumerable<IConfigureOptions<TOptions>> setups,这就是在Microsoft.Extensions.Options.ConfigurationExtensions组件部分注入的NamedConfigureFromConfigurationOptions<TOptions>,说明IOptionsFactory<TOptions>实例可以拿到ConfigureNamedOptions<TOptions>实例,这意味可以拿到传入的Options名称和BindFromOptions回调并可以调用Microsoft.Extensions.Configuration.Binder组件就行参数的绑定.
public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures, IEnumerable<IValidateOptions<TOptions>> validations)
{
// The default DI container uses arrays under the covers. Take advantage of this knowledge
// by checking for an array and enumerate over that, so we don't need to allocate an enumerator.
// When it isn't already an array, convert it to one, but don't use System.Linq to avoid pulling Linq in to
// small trimmed applications.
_setups = setups as IConfigureOptions<TOptions>[] ?? new List<IConfigureOptions<TOptions>>(setups).ToArray();
_postConfigures = postConfigures as IPostConfigureOptions<TOptions>[] ?? new List<IPostConfigureOptions<TOptions>>(postConfigures).ToArray();
_validations = validations as IValidateOptions<TOptions>[] ?? new List<IValidateOptions<TOptions>>(validations).ToArray();
}
这里IEnumerable<IPostConfigureOptions<TOptions>>和IEnumerable<IValidateOptions<TOptions>>说明参数可以指定生命周期,和检验功能,本文暂不做介绍.接着看Create方法.
public TOptions Create(string name)
{
TOptions options = CreateInstance(name);
foreach (IConfigureOptions<TOptions> setup in _setups)
{
if (setup is IConfigureNamedOptions<TOptions> namedSetup)
{
namedSetup.Configure(name, options);
}
else if (name == Options.DefaultName)
{
setup.Configure(options);
}
}
foreach (IPostConfigureOptions<TOptions> post in _postConfigures)
{
post.PostConfigure(name, options);
}
if (_validations.Length > 0)
{
var failures = new List<string>();
foreach (IValidateOptions<TOptions> validate in _validations)
{
ValidateOptionsResult result = validate.Validate(name, options);
if (result is not null && result.Failed)
{
failures.AddRange(result.Failures);
}
}
if (failures.Count > 0)
{
throw new OptionsValidationException(name, typeof(TOptions), failures);
}
}
return options;
}
/// <summary>
/// Creates a new instance of options type
/// </summary>
protected virtual TOptions CreateInstance(string name)
{
return Activator.CreateInstance<TOptions>();
}
这里首先调用Activator反射创建Options实例,接着执行namedSetup.Configure(name, options);方法,这里调用ConfigureNamedOptions的Configure方法,其本质就是如下代码:
public virtual void Configure(string name, TOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
// Null name is used to configure all named options.
if (Name == null || name == Name)
{
Action?.Invoke(options);
}
}
ok,很清晰执行了BindFromOptions回调,进入Microsoft.Extensions.Configuration.Binder组件,到这里Microsoft.Extensions.Options组件结束
(3)、Microsoft.Extensions.Configuration.Binder组件
(2)、通过ConfigureNamedOptions的Configure方法将反射创建的Options实例和传入的BinderOptions配置回调和IConfiguration实例传入Microsoft.Extensions.Configuration.Binder组件.并调用Bind方法,如下
public static void Bind(this IConfiguration configuration, object instance, Action<BinderOptions> configureOptions)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
if (instance != null)
{
var options = new BinderOptions();
configureOptions?.Invoke(options);
BindInstance(instance.GetType(), instance, configuration, options);
}
}
这里执行了BinderOptions自定义回调,来控制绑定行为.接着看BindInstance方法,先看一段如下:
if (type == typeof(IConfigurationSection))
{
return config;
}
var section = config as IConfigurationSection;
string configValue = section?.Value;
object convertedValue;
Exception error;
if (configValue != null && TryConvertValue(type, configValue, section.Path, out convertedValue, out error))
{
if (error != null)
{
throw error;
}
// Leaf nodes are always reinitialized
return convertedValue;
}
如果绑定的类型派生自IConfigurationSection,啥也不做,直接返回,接着IConfigurationSection的Value属性代码如下:
public string Value
{
get
{
return _root[Path];
}
set
{
_root[Path] = value;
}
}
这里_root本质就是配置文件解析完成之后得到的Data,并且传入了Path,Path就是调用代码如下:
services.Configure<MySqlDbOptions>(root.GetSection("MySqlDbOptions"));
中的MySqlDbOptions,这个是应为调用root.GetSection方法的本质就是创建一个ConfigurationSection对象实例,如下
public IConfigurationSection GetSection(string key)
=> new ConfigurationSection(this, key);
这里的key就是Path,如下代码:
public ConfigurationSection(IConfigurationRoot root, string path)
{
if (root == null)
{
throw new ArgumentNullException(nameof(root));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
_root = root;
_path = path;
}
到这里就很清晰了,应为要绑定的是配置实体,所以传入MySqlDbOptions字符串必然返回null.因为调用System.Text.Json序列化配置文件时,并不会将顶级节点,写入,原因是他没有具体的配置值.所以接着看代码:
if (configValue != null && TryConvertValue(type, configValue, section.Path, out convertedValue, out error))
{
if (error != null)
{
throw error;
}
// Leaf nodes are always reinitialized
return convertedValue;
}
所以绑定Options实例的时候这个判断走不进去,但是这段代码也很清晰,说明当调用IConfigurationSection的Value属性读到值时,遍历到带值得节点时,会走TryConvertValue方法转换值,并返回.接着看代码,如下:
if (config != null && config.GetChildren().Any())
{
// If we don't have an instance, try to create one
if (instance == null)
{
// We are already done if binding to a new collection instance worked
instance = AttemptBindToCollectionInterfaces(type, config, options);
if (instance != null)
{
return instance;
}
instance = CreateInstance(type);
}
// See if its a Dictionary
Type collectionInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type);
if (collectionInterface != null)
{
BindDictionary(instance, collectionInterface, config, options);
}
else if (type.IsArray)
{
instance = BindArray((Array)instance, config, options);
}
else
{
// See if its an ICollection
collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type);
if (collectionInterface != null)
{
BindCollection(instance, collectionInterface, config, options);
}
// Something else
else
{
BindNonScalar(config, instance, options);
}
}
}
return instance;
开始遍历子节点了,接下去就通过反射和类型判断做值的绑定,实例绑定最终走的BindNonScalar方法,并循环调用BindInstance方法,绑定完所有的匹配的属性值,之后返回Options实例.
应为内容较多,这里不在详细介绍了.自行阅读源码.
(4)、IOptions的问题
应为UnnamedOptionsManager的单例注入,且获取Value的代码如下:
public TOptions Value
{
get
{
if (_value is TOptions value)
{
return value;
}
lock (_syncObj ?? Interlocked.CompareExchange(ref _syncObj, new object(), null) ?? _syncObj)
{
return _value ??= _factory.Create(Options.DefaultName);
}
}
}
这意味着每个Options的实例在第一次创建完毕之后,就不会在被创建.导致,通过IOptions释出Options实例时,无法监听到配置文件的改变,所以IOptions的用途就有限制了,那如何解决这个问题
(5)、通过IOptionsMonitor来解决IOptions无法监听配置变化的问题
(4)中应为单例和判断的问题,导致通过IOptions释出的配置项无法监听到配置的修改.下面来介绍IOptionsMonitor如何解决这个问题,调用代码如下:
var workDir = $"{Environment.CurrentDirectory}";
var builder = new ConfigurationBuilder()
.SetBasePath(workDir)
.AddJsonFile($"test.json", optional: true, reloadOnChange: true);
var root = (ConfigurationRoot)builder.Build();
var services = new ServiceCollection();
services.Configure<MySqlDbOptions>(root.GetSection("MySqlDbOptions"));
var provider = services.BuildServiceProvider();
var mySqlDbOptions = provider.GetRequiredService<IOptionsMonitor<MySqlDbOptions>>();
Console.WriteLine($"当前配置值:" + mySqlDbOptions.CurrentValue.ConnectionName);
Console.WriteLine("变更配置,输入任意字符继续");
Console.ReadLine();
Console.WriteLine("变更后的配置值" + mySqlDbOptions.CurrentValue.ConnectionName);
Console.WriteLine("变更配置,输入任意字符继续");
Console.ReadLine();
Console.WriteLine("变更后的配置值" + mySqlDbOptions.CurrentValue.ConnectionName);
Console.ReadKey();
ok,看CurrentValue属性
public TOptions CurrentValue
{
get => Get(Options.DefaultName);
}
/// <summary>
/// Returns a configured <typeparamref name="TOptions"/> instance with the given <paramref name="name"/>.
/// </summary>
public virtual TOptions Get(string name)
{
name = name ?? Options.DefaultName;
return _cache.GetOrAdd(name, () => _factory.Create(name));
}
很清晰,将创建Options的实例方法持久化到字典中.所以当调用同一Options实例的CurrentValue属性时,不会重复调用_factory.Create方法而是直接返回第一次创建的Options实例.显然到这里并不能实现配置的监听.继续看源码,如下代码:
public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
{
_factory = factory;
_cache = cache;
void RegisterSource(IOptionsChangeTokenSource<TOptions> source)
{
IDisposable registration = ChangeToken.OnChange(
() => source.GetChangeToken(),
(name) => InvokeChanged(name),
source.Name);
_registrations.Add(registration);
}
// The default DI container uses arrays under the covers. Take advantage of this knowledge
// by checking for an array and enumerate over that, so we don't need to allocate an enumerator.
if (sources is IOptionsChangeTokenSource<TOptions>[] sourcesArray)
{
foreach (IOptionsChangeTokenSource<TOptions> source in sourcesArray)
{
RegisterSource(source);
}
}
else
{
foreach (IOptionsChangeTokenSource<TOptions> source in sources)
{
RegisterSource(source);
}
}
}
查看构造函数的代码发现了ChangeToken.OnChange,如不明白这个的原理请参考C#下 观察者模式的另一种实现方式IChangeToken和ChangeToken.OnChange
,用到观察者了配合在Microsoft.Extensions.Options.ConfigurationExtensions组件中注入的如下代码:
services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));
实现了配置监听,具体原理继续查看源码,首先令牌生产者一直查看源码,发现其是ConfigurationRoot实例创建,如下:
public IChangeToken GetReloadToken() => _changeToken;
接看着Root实例的构造函数:
public ConfigurationRoot(IList<IConfigurationProvider> providers)
{
if (providers == null)
{
throw new ArgumentNullException(nameof(providers));
}
_providers = providers;
_changeTokenRegistrations = new List<IDisposable>(providers.Count);
foreach (IConfigurationProvider p in providers)
{
p.Load();
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
}
}
果然,在每次Load完一个配置源之后(这里拿Json作为配置源讲解),订阅了每个配置源的Provider的reloadToken实例,而配置源通过FileSystemWatcher检测到文件发生改变时,会调用Provider实例的Load方法重新读取配置源,并给Root实例的Data属性重新赋值,之后调用OnReload方法如下代码:
protected void OnReload()
{
ConfigurationReloadToken previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken());
previousToken.OnReload();
}
触发令牌执行ConfigurationRoot注入的RaiseChanged回调,其代码如下:
private void RaiseChanged()
{
ConfigurationReloadToken previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
previousToken.OnReload();
}
而OptionsMonitor订阅了ConfigurationRoot实例的Reload令牌,这里就触发了Monitor实例的InvokeChanged方法,如下:
private void InvokeChanged(string name)
{
name = name ?? Options.DefaultName;
_cache.TryRemove(name);
TOptions options = Get(name);
if (_onChange != null)
{
_onChange.Invoke(options, name);
}
}
到这里清晰了,移除了缓存的实例,按照新的配置源重新生成了实例.所以当第二次调用IOptionsMonitor实例的CurrentValue时,会拿到新的配置值.但是这里当FileSystemWatcher检测到配置变化时,重新Load配置源时,会有延时如下代码:
if (Source.ReloadOnChange && Source.FileProvider != null)
{
_changeTokenRegistration = ChangeToken.OnChange(
() => Source.FileProvider.Watch(Source.Path),
() =>
{
Thread.Sleep(Source.ReloadDelay);
Load(reload: true);
});
}
线程短暂的按照配置值休息了一会,所以通过IMonitorOptions拿到的配置值并不是实时的,这个参数值是可配置的.