.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拿到的配置值并不是实时的,这个参数值是可配置的.

posted @ 2022-04-27 23:29  郑小超  阅读(169)  评论(0编辑  收藏  举报