跟我一起學.NetCore之選項(Options)核心型別簡介

Code綜藝圈發表於2020-08-24

前言

.NetCore中提供的選項框架,我把其理解為配置組,主要是將服務中可供配置的項提取出來,封裝成一個型別;從而服務可根據應用場景進行相關配置項的設定來滿足需求,其中使用了依賴注入的形式,使得更加簡單、便捷;另外和配置(Configuration)系統的無縫結合,使得服務更加靈活;而對於Options我們經常在註冊服務中用到,相信只要接觸過.NetCore中的小夥伴都知道,在註冊服務的時候,經常在引數中進行Options的配置(如下圖),可以直接的說:沒有Options的服務不是好服務~~~

img

正文

Options模型中主要有三個核心介面型別:IOption、IOptionsSnapshot、IOptionsMonitor這裡的TOptions就是指標對服務提取出來的配置項封裝的型別,以下分別看看三個核心型別定義了什麼?**
**

  • IOption

    namespace Microsoft.Extensions.Options
    {
        // 這裡TOptions 做了一個約束,必須有無參建構函式
        public interface IOptions<out TOptions> where TOptions : class, new()
        {
            // 這裡是通過屬性的方式定義TOptions    
            TOptions Value
            {
                get;
            }
        }
    }
    
  • IOptionsSnapshot

    namespace Microsoft.Extensions.Options
    {   // 這裡TOptions 做了一個約束,必須有無參建構函式
        public interface IOptionsSnapshot<out TOptions> : IOptions<TOptions> where TOptions : class, new()
        {
            // 通過名字獲取 TOptions
            TOptions Get(string name);
        }
    }
    
  • IOptionsMonitor

    namespace Microsoft.Extensions.Options
    {
        public interface IOptionsMonitor<out TOptions>
        {
            // 通過屬性獲取TOptions    
            TOptions CurrentValue
            {
                get;
            }
            // 通過名稱獲取TOptions
            TOptions Get(string name);
            // 這是用於監聽改變的,如果資料設定項改變,就會發出通知
            IDisposable OnChange(Action<TOptions, string> listener);
        }
    }
    
    

通過以上三種型別的定義,大概應該知道TOptions有對應的名字,根據對應的名字建立或獲取TOptions,可能會問,IOption中是通過屬性獲取的,沒有指定名字啊,其實是有的,只是名字預設為空,所以稱之為預設Option;而對於IOptionsMonitor一看便知,它提供了監聽改變的功能,所以後續如果需要監聽改變,就可以用這個型別介面;除此,微軟為三個核心型別提供了預設實現,IOptions和IOptionsSnapshot的預設實現為OptionsManager,IOptionsMonitor的預設實現為OptionsMonitor,來、走進他們的世界,看看是如何實現的(進行簡單的註釋):

OptionsManager

// 實現了IOptions<TOptions> 和IOptionsSnapshot<TOptions>, 同時也約束了TOptions
public class OptionsManager<TOptions>  :IOptions<TOptions>,  IOptionsSnapshot<TOptions> where TOptions : class, new()
{
    // 用於專門提供TOptions例項的,同時也對TOpions進行相關初始化
    private readonly IOptionsFactory<TOptions> _factory;
    // 提高效能,將對應的TOptions例項進行快取
    private readonly OptionsCache<TOptions>  _cache =  new OptionsCache<TOptions>();  
    // 建構函式,通過依賴注入的形式,將factory進行注入
    public OptionsManager(IOptionsFactory<TOptions> factory)
{
        _factory = factory;
    }
    // 實現IOptions<TOptions>通過屬性獲取TOptions例項
    public TOptions Value
    {
        get
        {
          // 這裡通過一個預設的名字獲取,只是這個名字預設為空,所以還是有名字的
          return Get(Options.DefaultName);
        }
    } 
    // 實現IOptionsSnapshot<TOptions>通過名字獲取TOptions 
    public virtual TOptions Get(string name)
{
       name = name ?? Options.DefaultName;
       // 如果快取中沒有,就通過傳入的Action進行建立並加入到快取中
       return _cache.GetOrAdd(name, () => _factory.Create(name));
    }
}
// 定義的 TOptions預設名稱
public static class Options
{
    public static readonly string DefaultName = string.Empty;  
}

OptionsMonitor

namespace Microsoft.Extensions.Options
{
    // 實現IOptionsMonitor ,對TOpitons 進行約束
    public class OptionsMonitor<TOptions> : IOptionsMonitor<TOptions>, IDisposable where TOptions : class, new()
    {
        // 根據名稱快取TOptions物件
        private readonly IOptionsMonitorCache<TOptions> _cache;
        // 用於建立TOptions物件
        private readonly IOptionsFactory<TOptions> _factory;
        // 監聽改變的核心
        private readonly IEnumerable<IOptionsChangeTokenSource<TOptions>> _sources;
        private readonly List<IDisposable> _registrations = new List<IDisposable>();
        // 改變觸發的事件
        internal event Action<TOptions, string> _onChange;
        // 建構函式
        public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
        {
            _factory = factory;
            _sources = sources;
            _cache = cache;
            // 註冊改變回撥,用於監聽
            foreach (var source in _sources)
            {
                var registration = ChangeToken.OnChange(
                      () => source.GetChangeToken(),
                      (name) => InvokeChanged(name),
                      source.Name);

                _registrations.Add(registration);
            }
        }
        // 這個方法是重點,如果發生改變,移除之前的TOptions物件,重新建立一個新TOptions
        private void InvokeChanged(string name)
        {
            name = name ?? Options.DefaultName;
            // 根據名字移除TOpitons物件
            _cache.TryRemove(name);
            // 重新根據名稱獲取物件
            var options = Get(name);
            if (_onChange != null)
            {
                _onChange.Invoke(options, name);
            }
        }
        // 獲取預設的TOptions物件
        public TOptions CurrentValue
        {
            get => Get(Options.DefaultName);
        }
        // 根據名稱獲取TOptions物件,如果沒有就利用OptionsFactory建立TOptions物件
        public virtual TOptions Get(string name)
        {
            name = name ?? Options.DefaultName;
            // 
            return _cache.GetOrAdd(name, () => _factory.Create(name));
        }

        // 註冊監聽改變的蝙蝠
        public IDisposable OnChange(Action<TOptions, string> listener)
        {
            var disposable = new ChangeTrackerDisposable(this, listener);
            _onChange += disposable.OnChange;
            return disposable;
        }

        // 取消註冊的監聽改變回撥,同時移除對應的監聽Token
        public void Dispose()
        {
            foreach (var registration in _registrations)
            {
                registration.Dispose();
            }

            _registrations.Clear();
        }
        // 內部類
        internal class ChangeTrackerDisposable : IDisposable
        {
            private readonly Action<TOptions, string> _listener;
            private readonly OptionsMonitor<TOptions> _monitor;

            public ChangeTrackerDisposable(OptionsMonitor<TOptions> monitor, Action<TOptions, string> listener)
            {
                _listener = listener;
                _monitor = monitor;
            }
            // 觸發改變時呼叫對應註冊的回撥
            public void OnChange(TOptions options, string name) => _listener.Invoke(options, name);
            // Dispose  方法進行解除註冊的監聽回撥
            public void Dispose() => _monitor._onChange -= OnChange;
        }
    }
}

通過以上程式碼段得知,IOptions和IOptionsSnapshot其實本質都是一樣的,都是命名的Options,統一由IOptionsFactory建立提供TOptions物件;而對於IOptionsMonitor, 監聽的本質是通過IOptionsChangeTokenSource這個實現,每次監聽到改變都把對應名稱的TOptions物件移除,重新建立一個新的TOptions物件,從而獲取到最新的值,其實最終改變通知的本質還是使用IChangeToken進行通知,這個可以參考之前配置的監聽改變(參考這篇:跟我一起學.NetCore之配置變更監聽);

**
**

本想看完以上預設實現,就打算進行舉例演示了,不再深入看程式碼;但是相信看到這的小夥伴肯定會問:IOptionsFactory是怎麼建立出TOptions的? 重點都不說,差評_~~~~~,其實我也是這麼想的,所以繼續再看看IOptionsFactory

IOptionsFactory的預設實現是OptionsFactory;建立TOptions我理解為三步驟,建立物件->加工物件(初始化)->驗證(驗證合法性):

namespace Microsoft.Extensions.Options
{
    public interface IOptionsFactory<TOptions> where TOptions : class, new()
    {
        // 介面中定義了一個建立方法,用於建立TOptions
        TOptions Create(string name);
    }
}
namespace Microsoft.Extensions.Options
{
    // 實現IOptionsFactory介面,並約束TOptions
    public class OptionsFactory<TOptions> : IOptionsFactory<TOptions> where TOptions : class, new()
    {
        // 初始化邏輯,初始化由IConfigureOptions和IPostConfigureOptions處理
        private readonly IEnumerable<IConfigureOptions<TOptions>> _setups;
        private readonly IEnumerable<IPostConfigureOptions<TOptions>> _postConfigures;
        // 驗證邏輯
        private readonly IEnumerable<IValidateOptions<TOptions>> _validations;
        // 建構函式 
        public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures) : this(setups, postConfigures, validations: null)
        { }
        // 建構函式 
        public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures, IEnumerable<IValidateOptions<TOptions>> validations)
        {
            _setups = setups;
            _postConfigures = postConfigures;
            _validations = validations;
        }
        // 建立TOptions的核心方法,傳入名稱,如果沒名稱,預設是空
        public TOptions Create(string name)
        {
            // 1 根據傳入的TOptions建立物件,這裡用無參建構函式,所以之前需要對TOptions進行約束
            var options = new TOptions();
            // 2 初始化
            foreach (var setup in _setups)
            {
                // 根據傳入的名字是否為預設名稱選擇不同的加工方法
                if (setup is IConfigureNamedOptions<TOptions> namedSetup)
                {
                    namedSetup.Configure(name, options);
                }
                else if (name == Options.DefaultName)
                {
                    setup.Configure(options);
                }
            }
            // IPostConfigureOptions對Options加工
            foreach (var post in _postConfigures)
            {
                post.PostConfigure(name, options);
            }
            // 進行驗證, 如果不傳入驗證規則,則代表不進行驗證
            if (_validations != null)
            {
                // 存放驗證失敗的錯誤訊息
                var failures = new List<string>();
                // 遍歷驗證
                foreach (var validate in _validations)
                {
                    // 進行驗證
                    var result = validate.Validate(name, options);
                    // 如果驗證失敗
                    if (result.Failed)
                    {  
                        // 將驗證失敗錯誤資訊加入到列表中
                        failures.AddRange(result.Failures);
                    }
                }
                // 如果驗證失敗,就丟擲異常OptionsValidationException
                if (failures.Count > 0)
                {
                    throw new OptionsValidationException(name, typeof(TOptions), failures);
                }
            }
            // 返回例項物件
            return options;
        }
    }
}

對於TOptions的建立邏輯就暫時先看到這吧,如果需要再詳細瞭解具體邏輯,可以私下進行研究;

總結

哎呀,這篇先不一一舉例演示了,可能導致篇幅過長,上個WC的時間估計看不完(哈哈哈);那麼就是單純的程式碼說明嗎?不僅僅如此,這篇主要講解程式碼的同時,其實著重凸顯了IOption、IOptionsSnapshot、IOptionsMonitor三個核心型別,然後圍繞三個核心型別簡單看了內部實現、建立過程和監聽邏輯,因為在實際應用也是圍繞以上進行使用和擴充套件的,最終的目的是讓使用不再糊塗,其實這才是終極目標啦~~~~ 下一篇就專門針對Options舉例演示!!!

----------------------------------------------

一個被程式搞醜的帥小夥,關注"Code綜藝圈",識別關注跟我一起學~~~

img

相關文章