.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>,並且注冊回調。

本貼相關的代碼和UML圖在https://github.com/zhurongbo111/AspNetCoreDemo/tree/master/02-Options


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM