专栏首页DotNet Core圈圈.Net Core中的Options使用以及源码解析

.Net Core中的Options使用以及源码解析

在.Net Core中引入了Options这一使用配置方式,通常来讲我们会把所需要的配置通过IConfiguration对象配置成一个普通的类,并且习惯上我们会把这个类的名字后缀加上Options。所以我们在使用某一个中间件,或者使用第三方类库时,经常会看到配置对应Options的代码,例如关于Cookie的中间件就会配置CookiePolicyOptions这一个对象。

使用Options

在.Net Core中使用Options主要分为两个步骤:

  • 向容器中注入TOptions的配置。目的是告诉容器当我获取这个TOptions时,这个TOptions包含的一些字段如何写入,所以我们需要传入一个Action<TOptions>。注意:默认情况下,这个TOptions需要一个无参的构造函数。
  • 从容器中获取TOptions对象。在获取的时候有三种获取方式:IOptions<TOptions>,IOptionsMonitor<TOptions>,IOptionsSnapshot<TOptions>。

配置TOptions

在配置TOptions的时候,你会发现所有的方法都是泛型的,每一个Options类型都有一套独立的管理系统。入口是Configure方法,它有多个重载,但最终都会调用这个方法

public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions)

当不传递name时,默认使用Microsoft.Extensions.Options.DefaultName,等于string.Empty

namespace Microsoft.Extensions.Options
{
    /// <summary>
    /// Helper class.
    /// </summary>
    public static class Options
    {
        public static readonly string DefaultName = string.Empty;

    }
}

有的时候我们会看到在调用Configure时并没有传递Action<TOptions>,而是直接传递了一个IConfiguration,那是因为在内部帮我们转化了一下,最终传递的还是一个Action<TOptions>

options => ConfigurationBinder.Bind(config, options)

另外,我们可以看到ConfigureAll这个方法,这个内部也是调用了Configure方法,只不过把name设置成null,后续在创建TOptions时,会把name为nul的Action<TOptions>应用于所有实例。

最后还有一个PostConfigure方法,它和Configure方法使用方式一模一样,也是在创建TOptions时调用。只不过先后顺序不一样,PostConfigure在Configure之后调用。

现在我们来看实际的用法:

            services.Configure<EmailOption>(op => op.Title = "Default Name");
            services.Configure<EmailOption>("FromMemory", op => op.Title= "FromMemory");
            services.Configure<EmailOption>("FromConfiguration", Configuration.GetSection("Email"));
            services.AddOptions<EmailOption>("AddOption").Configure(op => op.Title = "AddOption Title");

            services.Configure<EmailOption>(null, op => op.From = "Same With ConfigureAll");
            //services.ConfigureAll<EmailOption>(op => op.From = "ConfigureAll");

            services.PostConfigure<EmailOption>(null, op => op.Body = "Same With PostConfigureAll");
            //services.PostConfigureAll<EmailOption>(op => op.Body = "PostConfigurationAll");

EmailOption是一个很简单的类:

public class EmailOption
    {
        public string Title { get; set; }

        public string Body { get; set; }

        public string From { get; set; }
    }

在上面所示的用法,多了一个AddOptions的用法。这种方式会创建了一个OptionsBuilder,用来辅助配置TOptions对象,其内部实现是和Configure,PostConfigure方法一样的。

使用Options

既然我们告诉了容器TOption是如何配置的,那么在使用的时候只需要通过注入的方式获取就行了。总共用三种获取方式:

  • IOptions<TOptions>:这种方式只能获取默认名称的那个TOptions,且不能监控配置源出现变化的情况。调用时访问它的Value属性即可。
  • IOptionsMonitor<TOptions>:这种方式可以获取所有名称的TOptions,且可以监控配置源出现变化的情况。调用它的TOptions Get(string name)方法即可获取TOptions
  • IOptionsSnapshot<TOptions>:此接口继承于IOptions<TOptions>,这种方式也可以获取所有名称的TOptions和监控配置源出现变化的情况。调用它的TOptions Get(string name)方法即可获取TOptions。但是它的实现和第二种完全不一样,后面会详细解释。

下面我们来看一下具体的使用方法:

    public class HomeController : Controller
    {
        IOptions<EmailOption> _options;
        IOptionsMonitor<EmailOption> _optionsMonitor;
        IOptionsSnapshot<EmailOption> _optionsSnapshot;

        public HomeController(IOptions<EmailOption> options, IOptionsMonitor<EmailOption> optionsMonitor, IOptionsSnapshot<EmailOption> optionsSnapshot)
        {
            _options = options;
            _optionsMonitor = optionsMonitor;
            _optionsSnapshot = optionsSnapshot;
        }

        public IActionResult Demo()
        {
            EmailOption defaultEmailOption = _options.Value;

            EmailOption defaultEmailOption1 = _optionsMonitor.CurrentValue;//_optionsMonitor.Get(Microsoft.Extensions.Options.Options.DefaultName);
            EmailOption fromMemoryEmailOption1 = _optionsMonitor.Get("FromMemory");
            EmailOption fromConfigurationEmailOption1 = _optionsMonitor.Get("FromConfiguration");

            EmailOption defaultEmailOption2 = _optionsSnapshot.Value;//_optionsSnapshot.Get(Microsoft.Extensions.Options.Options.DefaultName);
            EmailOption fromMemoryEmailOption2 = _optionsSnapshot.Get("FromMemory");
            EmailOption fromConfigurationEmailOption2 = _optionsSnapshot.Get("FromConfiguration");
            return View();
        }
    }

注意:如果是基于IConfiguration的TOptions需要进行监控,必须此IConfiguration是可监控的。

源码解析

我们在配置Options的时候,其实会向容器内部注入IConfigureOptions<TOptions>或者IConfigureNamedOptions<TOptions>以及IPostConfigureOptions<TOptions>这几种对象。随后负责创建TOptions的工厂类 IOptionsFactory<TOptions>,也以注入的形式获取这几个对象来创建需要的TOptions。其中IConfigureNamedOptions<TOptions>继承于IConfigureOptions<TOptions>。相关的UML图如下:

IOptionsFactory<TOptions>的实现类是OptionsFactory<TOptions>,Create(string name)的核心代码如下:

        public TOptions Create(string name)
        {
            var options = new TOptions();
            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;
        }

到这里,我们知道了如何通过提供的配置信息,去产生一个TOptions对象。接下来我们看看 IOptions<TOptions>,IOptionsSnapshot<TOptions>,IOptionsMonitor<TOptions>是如何实现的,以及它们是如何实现配置源的动态更新。

每当我们调用Configure方法的时候,系统都会调用AddOptions,其内容如下:

        public static IServiceCollection AddOptions(this IServiceCollection services)
        {
            if (services == null)
            {
                throw new ArgumentNullException(nameof(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;
        }

其中IOptionsFactory就不必说,它就是用来产生对象的,这是它唯一的用处。而IOptions<>,IOptionsSnapshot<>的实现类都是OptionsManager。OptionsManager在创建时会注入IOptionsFactory,同时内部还有一个OptionsCache根据name保存产生的对象。注意:这里的OptionsCache并不是注入到容器里的那个实例。它的代码如下:

    public class OptionsManager<TOptions> : IOptions<TOptions>, IOptionsSnapshot<TOptions> where TOptions : class, new()
    {
        private readonly IOptionsFactory<TOptions> _factory;
        private readonly OptionsCache<TOptions> _cache = new OptionsCache<TOptions>(); // Note: this is a private cache

        /// <summary>
        /// Initializes a new instance with the specified options configurations.
        /// </summary>
        /// <param name="factory">The factory to use to create options.</param>
        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;

            // Store the options in our instance cache
            return _cache.GetOrAdd(name, () => _factory.Create(name));
        }
    }
  • IOptions在注册到容器时是以单例的形式,所以以这种方式产生的对象会被全局缓存起来(缓存在OptionsManager的内部OptionsCache里),也不会被更新,并且它只能获取默认名称的TOptions,但是它效率更高。
  • IOptionsSnapshot在注册到容器时是以Scoped的形式,所以这种方式产生的对象不会全局缓存,每一次请求都会创建新的对象,能觉察到配置源的改变。又因为它也有一个内部的OptionsCache,所以能做到同一请求周期内是不会改变的。

而IOptionsMonitor是以单例的形式注入到容器中,并且IOptionsMonitorCache也是单例的形式注入到容器中,这个IOptionsMonitorCache后续会在创建OptionsMonitor的时候注入进去,所以OptionsMonitor的缓存也是全局唯一的。但是我们之前已经说过,这个也是能觉察到配置源更新的,那又是如何实现的呢?那是因为还会注入一个IOptionsChangeTokenSource类型,它会觉察到配置源的改变,一旦发生改变就会告知OptionsMonitor从缓存中移除相应的对象。关于移除缓存的核心代码如下:

    public class OptionsMonitor<TOptions> : IOptionsMonitor<TOptions> where TOptions : class, new()
    {
        private readonly IOptionsMonitorCache<TOptions> _cache;
        private readonly IOptionsFactory<TOptions> _factory;
        private readonly IEnumerable<IOptionsChangeTokenSource<TOptions>> _sources;/// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="factory">The factory to use to create options.</param>
        /// <param name="sources">The sources used to listen for changes to the options instance.</param>
        /// <param name="cache">The cache used to store options.</param>
        public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
        {
            _factory = factory;
            _sources = sources;
            _cache = cache;

            foreach (var source in _sources)
            {
                ChangeToken.OnChange<string>(
                    () => source.GetChangeToken(),
                    (name) => InvokeChanged(name),
                    source.Name);
            }
        }

        private void InvokeChanged(string name)
        {
            name = name ?? Options.DefaultName;
            _cache.TryRemove(name);
            ...
        }
    }

IOptionsChangeTokenSource需要在配置Options的时候进行配置,如果我们配置的时候调用的IConfiguration的重载,那么他会自动注入一个ConfigurationChangeTokenSource,核心代码如下:

        public static IServiceCollection Configure<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));
        }

最佳实践

既然有如此多的获取方式,那应该如何选择?

  1. 如果TOption不需要监控且整个程序就只有一个同类型的TOption,那么强烈建议使用IOptions<TOptions>。
  2. 如果TOption需要监控或者整个程序有多个同类型的TOption,那么只能选择IOptionsMonitor<TOptions>或者IOptionsSnapshot<TOptions>。
  3. 当IOptionsMonitor<TOptions>和IOptionsSnapshot<TOptions>都可以选择时,如果Action<TOptions>是一个比较耗时的操作,那么建议使用IOptionsMonitor<TOptions>,反之选择IOptionsSnapshot<TOptions>
  4. 如果需要对配置源的更新做出反应时(不仅仅是配置对象TOptions本身的更新),那么只能使用IOptionsMonitor<TOptions>,并且注册回调。

本文分享自微信公众号 - DotNet技术平台(DotNetCore_Mements),作者:Gary_Zhu

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-07-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 使用 Infer.NET 评价竞争对手

    Infer.NET 是开放源代码的代码库,可用于创建概率性编程系统。我往往会将普通的计算机程序视作,主要基于有指定类型的值的变量(如有值“Q”的 char 变量...

    Edison.Ma
  • 惊!.Net5真的来了,抢鲜实战!

    靴子落地,期盼已久的.Net5终于来了!在3月16号正式发布了第一个预览版本。号称一统江湖的.Net5究竟为我们带来了什么,是人性的扭曲还是道德的沦丧,下面让我...

    Edison.Ma
  • .NET Core 3.0之深入源码理解Startup的注册及运行

    开发.NET Core应用,直接映入眼帘的就是Startup类和Program类,它们是.NET Core应用程序的起点。通过使用Startup,可以配置化处理...

    Edison.Ma
  • [ASP.NET Core 3框架揭秘] Options[4]: Options模型[下篇]

    IOptionsFactory<TOptions>解决了Options的创建与初始化问题,但由于它自身是无状态的,所以Options模型对Options对象实施...

    蒋金楠
  • [ASP.NET Core 3框架揭秘] Options[3]: Options模型[上篇]

    通过前面演示的几个实例(配置选项的正确使用方式[上篇]、配置选项的正确使用方式[下篇]),我们已经对基于Options的编程方式有了一定程度的了解,下面从设计的...

    蒋金楠
  • circBank:人类环状RNA数据库

    circBank对circBase数据库中人类的环状RNA数据加以整理,根据序列信息进行了蛋白编码潜能,miRNA相互作用预测分析,并将所有结果整理成了在线数据...

    生信修炼手册
  • 华科世界第六,北邮碾压伯克利:USNews世界大学CS榜发布

    另外,还有排名第三的德克萨斯大学奥斯汀分校,第五的阿卜杜拉国王大学,以及第七的巴黎萨克雷大学。

    量子位
  • 带你轻松打开SVG动画的大门

    初学SVG的时候,感觉那一坨一坨的代码难读难懂,现在回过头仔细想想,是因为那时候看文档缺少一些具体的实例,导致学习起来很枯燥。如今SVG已经在前端各个领域都有所...

    练小习
  • Exchange 2010迁移Exchange 2013(二)本地用户移动

         续上一篇完成了Exchange 2013的共存部署后,下面就要对Exchange 2010的用户进行迁移了。用户迁移无法在Exchange 2010中...

    李珣
  • 手把手教你使用SPSS快速上手商业数据分析

    SPSS是社会统计科学软件包的简称, 其官方全称为IBM SPSS Statistics。SPSS软件包最初由SPSS Inc.于1968年推出,于2009年被...

    CDA数据分析师

扫码关注云+社区

领取腾讯云代金券