.net 5.0 Options组件源码解析
本文主要介绍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拿到的配置值并不是实时的,这个参数值是可配置的.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构