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


通過前面演示的幾個實例(配置選項的正確使用方式[上篇]配置選項的正確使用方式[下篇]),我們已經對基於Options的編程方式有了一定程度的了解,下面從設計的角度介紹Options模型。我們演示的實例已經涉及Options模型的3個重要的接口,它們分別是IOptions<TOptions>和IOptionsSnapshot<TOptions>,最終的Options對象正是利用它們來提供的。在Options模型中,這兩個接口具有同一個實現類型OptionsManager<TOptions>。Options模型的核心接口和類型定義在NuGet包“Microsoft.Extensions.Options”中。

一、OptionsManager<TOptions>

在Options模式的編程中,我們會利用作為依賴注入容器的IServiceProvider對象來提供IOptions<TOptions>服務或者IOptionsSnapshot<TOptions>服務,實際上,最終得到的服務實例都是一個OptionsManager<TOptions>對象。在Options模型中,OptionsManager<TOptions>相關的接口和類型主要體現在下圖中。

7-7

下面以上圖為基礎介紹OptionsManager<TOptions>對象是如何提供Options對象的。如下面的代碼片段所示,IOptions<TOptions>接口和IOptionsSnapshot<TOptions>接口的泛型參數的TOptions類型要求具有一個默認的構造函數,也就是說,Options對象可以在無須指定參數的情況下直接采用new關鍵字進行實例化,實際上,Options最初就是采用這種方式創建的。

public interface IOptions<out TOptions> where TOptions: class, new()
{
    TOptions Value { get; }
}

public interface IOptionsSnapshot<out TOptions> : IOptions<TOptions>  where TOptions: class, new()
{
    TOptions Get(string name);
}

IOptions<TOptions>接口通過Value屬性提供對應的Options對象,繼承它的IOptionsSnapshot<TOptions>接口則利用其Get方法根據指定的名稱提供對應的Options對象。OptionsManager<TOptions>針對這兩個接口成員的實現依賴其他兩個對象,分別通過IOptionsFactory<TOptions>接口和IOptionsMonitorCache<TOptions>接口表示,這也是Options模型的兩個核心成員。

作為Options對象的工廠,IOptionsFactory<TOptions>對象負責創建Options對象並對其進行初始化。出於性能方面的考慮,由IOptionsFactory<TOptions>工廠創建的Options對象會被緩存起來,針對Options對象的緩存就由IOptionsMonitorCache<TOptions>對象負責。下面會對IOptionsFactory<TOptions>和IOptionsMonitorCache<TOptions>進行單獨講解,在此之前需要先了解OptionsManager<TOptions>類型是如何定義的。

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>();  

    public OptionsManager(IOptionsFactory<TOptions> factory)  => _factory = factory;
    public TOptions Value => this.Get(Options.DefaultName);    
    public TOptions Get(string name)  => _cache.GetOrAdd(name, () => _factory.Create(name));
}

public static class Options
{
    public static readonly string DefaultName = string.Empty;  
}

OptionsManager<TOptions>對象提供Options對象的邏輯基本上體現在上面給出的代碼中。在創建一個OptionsManager<TOptions>對象時需要提供一個IOptionsFactory<TOptions>工廠,而它自己還會創建一個OptionsCache<TOptions>(該類型實現了IOptionsMonitorCache<TOptions>接口)對象來緩存Options對象,也就是說,Options對象實際上是被OptionsManager<TOptions>對象以“獨占”的方式緩存起來的,后續內容還會提到這個設計細節。

從編程的角度來講,IOptions<TOptions>接口和IOptionsSnapshot<TOptions>接口分別體現了非具名與具名的Options提供方式,但是對於同時實現這兩個接口的OptionsManager<TOptions>來說,提供的Options都是具名的,唯一的不同之處在於以IOptions<TOptions>接口名義提供Options對象時會采用一個空字符串作為名稱。默認Options名稱可以通過靜態類型Options的只讀字段DefaultName來獲取。

OptionsManager<TOptions>針對Options對象的提供(具名或者非具名)最終體現在其實現的Get方法上。由於Options對象緩存在自己創建的OptionsCache<TOptions>對象上,所以它只需要將指定的Options名稱作為參數調用其GetOrAdd方法就能獲取對應的Options對象。如果Options對象尚未被緩存,它會利用作為參數傳入的Func<TOptions>委托對象來創建新的Options對象,從前面給出的代碼可以看出,這個委托對象最終會利用IOptionsFactory<TOptions>工廠來創建Options對象。

二、IOptionsFactory<TOptions>

顧名思義,IOptionsFactory<TOptions>接口表示創建和初始化Options對象的工廠。如下面的代碼片段所示,該接口定義了唯一的Create方法,可以根據指定的名稱創建對應的Options對象。

public interface IOptionsFactory<TOptions> where TOptions: class, new()
{
    TOptions Create(string name);
}

OptionsFactory<TOptions>OptionsFactory<TOptions>是IOptionsFactory<TOptions>接口的默認實現。OptionsFactory<TOptions>對象針對Options對象的創建主要分3個步驟來完成,筆者將這3個步驟稱為Options對象相關的“實例化”、“初始化”和“驗證”。由於Options類型總是具有一個公共默認的構造函數,所以OptionsFactory<TOptions>的實現只需要利用new關鍵字調用這個構造函數就可以創建一個空的Options對象。當Options對象被實例化之后,OptionsFactory<TOptions>對象會根據注冊的一些服務對其進行初始化。Options模型中針對Options對象初始化的工作由如下3個接口表示的服務負責。

public interface IConfigureOptions<in TOptions> where TOptions: class
{    
    void Configure(TOptions options);
}

public interface IConfigureNamedOptions<in TOptions> :  IConfigureOptions<TOptions> where TOptions : class
{
    void Configure(string name, TOptions options);
}

public interface IPostConfigureOptions<in TOptions> where TOptions : class
{    
    void PostConfigure(string name, TOptions options);
}

上述3個接口分別通過定義的Configure方法和PostConfigure方法對指定的Options對象進行初始化,其中,IConfigureNamedOptions<TOptions>和IPostConfigureOptions<TOptions>還指定了Options的名稱。由於IConfigureOptions<TOptions>接口的Configure方法沒有指定Options的名稱,意味着該方法僅僅用來初始化默認的Options對象,而這個默認的Options對象就是以空字符串命名的Options對象。從接口命名就可以看出定義其中的3個方法的執行順序:定義在IPostConfigureOptions<TOptions>中的PostConfigure方法會在IConfigureOptions<TOptions>和IConfigureNamedOptions<TOptions>的Configure方法之后執行。

當注冊的IConfigureNamedOptions<TOptions>服務和IPostConfigureOptions<TOptions>服務完成了對Options對象的初始化之后,IOptionsFactory<TOptions>對象還應該驗證最終得到的Options對象是否有效。針對Options對象有效性的驗證由IValidateOptions<TOptions>接口表示的服務對象來完成。如下面的代碼片段所示,IValidateOptions<TOptions>接口定義的唯一的方法Validate用來對指定的Options對象(參數options)進行驗證,而參數name則代表Options的名稱。

public interface IValidateOptions<TOptions> where TOptions : class
{
    ValidateOptionsResult Validate(string name, TOptions options);
}

public class ValidateOptionsResult
{
    public static readonly ValidateOptionsResult Success;
    public static readonly ValidateOptionsResult Skip;
    public static ValidateOptionsResult Fail(string failureMessage);

    public bool Succeeded { get; protected set; }
    public bool Skipped { get; protected set; }
    public bool Failed { get; protected set; }
    public string FailureMessage { get; protected set; }
}

Options的驗證結果由ValidateOptionsResult類型表示。總的來說,針對Options對象的驗證會產生3種結果,即成功、失敗和忽略,它們分別通過3個對應的屬性來表示(Succeeded、Failed和Skipped)。一個表示驗證失敗的ValidateOptionsResult對象會通過其FailureMessage屬性來描述具體的驗證錯誤。可以調用兩個靜態只讀字段Success和Skip以及靜態方法Fail得到或者創建對應的ValidateOptionsResult對象。

Options模型提供了一個名為OptionsFactory<TOptions>的類型作為IOptionsFactory<TOptions>接口的默認實現。對上述3個接口有了基本了解后,對實現在OptionsFactory<TOptions>類型中用來創建並初始化Options對象的實現邏輯比較容易理解了。下面的代碼片段基本體現了OptionsFactory<TOptions>類型的完整定義。

public class OptionsFactory<TOptions> :IOptionsFactory<TOptions> where TOptions : class, new()
{
    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, null)
    { }

    public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures, IEnumerable<IValidateOptions<TOptions>> validations)
    {
        _setups = setups;
        _postConfigures = postConfigures;
        _validations = validations;
    }

    public TOptions Create(string name)
    {
        //步驟1:實例化
        var options = new TOptions();

        //步驟2-1:針對IConfigureNamedOptions<TOptions>的初始化
        foreach (var setup in _setups)
        {
            if (setup is IConfigureNamedOptions<TOptions> namedSetup)
            {
                namedSetup.Configure(name, options);
            }
            else if (name == Options.DefaultName)
            {
                setup.Configure(options);
            }
        }

        //步驟2-2:針對IPostConfigureOptions<TOptions>的初始化
        foreach (var post in _postConfigures)
        {
            post.PostConfigure(name, options);
        }

        //步驟3:有效性驗證
        var failedMessages = new List<string>();
        foreach (var validator in _validations)
        {
            var reusult = validator.Validate(name, options);
            if (reusult.Failed)
            {
                failedMessages.Add(reusult.FailureMessage);
            }
        }
        if (failedMessages.Count > 0)
        {
            throw new OptionsValidationException(name, typeof(TOptions),
                failedMessages);
        }
        return options;
    }
}

如上面的代碼片段所示,調用構造函數創建OptionsFactory<TOptions>對象時需要提供IConfigureOptions<TOptions>對象、IPostConfigureOptions<TOptions>對象和IValidateOptions<TOptions>對象。在實現的Create方法中,它首先調用默認構造函數創建一個空Options對象,再先后利用IConfigureOptions<TOptions>對象和IPostConfigureOptions<TOptions>對象對這個Options對象進行“再加工”。這一切完成之后,指定的IValidateOptions<TOptions>會被逐個提取出來對最終生成的Options對象進行驗證,如果沒有通過驗證,就會拋出一個OptionsValidationException類型的異常。圖7-8所示的UML展示了OptionsFactory<TOptions>針對Options對象的初始化。

7-8

三、ConfigureNamedOptions<TOptions>

對於上述3個用來初始化Options對象的接口,Options模型均提供了默認實現,其中,ConfigureNamedOptions<TOptions>類同時實現了IConfigureOptions<TOptions>和IConfigureNamedOptions<TOptions>接口。當我們創建這樣一個對象時,需要指定Options的名稱和一個用來初始化Options對象的Action<TOptions>委托對象。如果指定了一個非空的名稱,那么提供的委托對象將會用於初始化與該名稱相匹配的Options對象;如果指定的名稱為Null(不是空字符串),就意味着提供的初始化操作適用於所有同類的Options對象。

public class ConfigureNamedOptions<TOptions> :IConfigureNamedOptions<TOptions>,IConfigureOptions<TOptions> where TOptions : class
{
    public string Name { get; }
    public Action<TOptions> Action { get; }

    public ConfigureNamedOptions(string name, Action<TOptions> action)
    {
        Name = name;
        Action = action;
    }

    public void Configure(string name, TOptions options)
    {
        if (Name == null || name == Name)
        {
            Action?.Invoke(options);
        }
    }

    public void Configure(TOptions options)  => Configure(Options.DefaultName, options);
}

有時針對某個Options的初始化工作需要依賴另一個服務。比較典型的就是根據當前承載環境(開發、預發和產品)對某個Options對象做動態設置。為了解決這個問題,Options模型提供了一個ConfigureNamedOptions<TOptions, TDep>,其中,第二個反省參數代表依賴的服務類型。如下面的代碼片段所示,ConfigureNamedOptions<TOptions, TDep>依然是IConfigureNamedOptions<TOptions>接口的實現類型,它利用Action<TOptions, TDep>對象針對指定的依賴服務對Options做針對性初始化。

public class ConfigureNamedOptions<TOptions, TDep> : IConfigureNamedOptions<TOptions>
    where TOptions : class
    where TDep : class
{
    public string Name { get; }
    public Action<TOptions, TDep> Action { get; }
    public TDep Dependency { get; }

    public ConfigureNamedOptions(string name, TDep dependency, Action<TOptions, TDep> action)
    {
        Name = name;
        Action = action;
        Dependency = dependency;
    }

    public virtual void Configure(string name, TOptions options)
    {
        if (Name == null || name == Name)
        {
            Action?.Invoke(options, Dependency);
        }
    }

    public void Configure(TOptions options)  => Configure(Options.DefaultName, options);
}

ConfigureNamedOptions<TOptions, TDep>僅僅實現了針對單一服務的依賴,針對Options的初始化可能依賴多個服務,Options模型為此定義了如下所示的一系列類型。這些類型都實現了IConfigureNamedOptions<TOptions>接口,並采用類似於ConfigureNamedOptions<TOptions, TDep>類型的方式實現了Configure方法。

public class ConfigureNamedOptions<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5> : IConfigureNamedOptions<TOptions>
    where TOptions : class
    where TDep1 : class
    where TDep2 : class
    where TDep3 : class
    where TDep4 : class
    where TDep5 : class
{
    public string Name { get; }
    public TDep1 Dependency1 { get; }
    public TDep2 Dependency2 { get; }
    public TDep3 Dependency3 { get; }
    public TDep4 Dependency4 { get; }
    public TDep5 Dependency5 { get; }
    public Action<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5> Action { get; }

    public ConfigureNamedOptions(string name, TDep1 dependency, TDep2 dependency2, TDep3 dependency3, TDep4 dependency4, TDep5 dependency5, Action<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5> action);
    public void Configure(TOptions options);
    public virtual void Configure(string name, TOptions options);
}

四、PostConfigureOptions<TOptions>

默認實現IPostConfigureOptions<TOptions>接口的是PostConfigureOptions<TOptions>類型。從給出的代碼片段可以看出它針對Options對象的初始化實現方式與ConfigureNamedOptions<TOptions>類型並沒有本質的差別。

public class PostConfigureOptions<TOptions> : IPostConfigureOptions<TOptions> where TOptions : class
{
    public string Name { get; }
    public Action<TOptions> Action { get; }

    public PostConfigureOptions(string name, Action<TOptions> action)
    {
        Name = name;
        Action = action;
    }

    public void PostConfigure(string name, TOptions options)
    {
        if (Name == null || name == Name)
        {
            Action?.Invoke(options);
        }
    }
}

Options模型同樣定義了如下這一系列針對依賴服務的IPostConfigureOptions<TOptions>接口實現。如果針對Options對象的后置初始化操作依賴於其他服務,就可以根據服務的數量選擇對應的類型。這些類型針對PostConfigure方法的實現與ConfigureNamedOptions<TOptions, TDep>類型實現Configure方法並沒有本質區別。

  • PostConfigureOptions<TOptions, TDep>。
  • PostConfigureOptions<TOptions, TDep1, TDep2>。
  • PostConfigureOptions<TOptions, TDep1, TDep2, TDep3>。
  • PostConfigureOptions<TOptions, TDep1, TDep2, TDep3, TDep4>。
  • PostConfigureOptions<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5>。

五、ValidateOptions<TOptions>

ValidateOptions<TOptions>是對IValidateOptions<TOptions>接口的默認實現。如下面的代碼片段所示,創建一個ValidateOptions<TOptions>對象時,需要提供Options的名稱和驗證錯誤消息,以及真正用於對Options進行驗證的Func<TOptions, bool>對象。

public class ValidateOptions<TOptions> : IValidateOptions<TOptions>where TOptions : class
{
    public string Name { get; }
    public string FailureMessage { get; }
    public Func<TOptions, bool> Validation { get; }
    public ValidateOptions(string name, Func<TOptions, bool> validation, string failureMessage);
    public ValidateOptionsResult Validate(string name, TOptions options);
}

對Options的驗證同樣可能具有對其他服務的依賴,比較典型的依然是針對不同的承載環境(開發、預發和產品)具有不同的驗證規則,所以IValidateOptions<TOptions>接口同樣具有如下5個針對不同依賴服務數量的實現類型。

  • ValidateOptions<TOptions, TDep>
  • ValidateOptions<TOptions, TDep1, TDep2>
  • ValidateOptions<TOptions, TDep1, TDep2, TDep3>
  • ValidateOptions<TOptions, TDep1, TDep2, TDep3, TDep4>
  • ValidateOptions<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5>

前面介紹了OptionsFactory<TOptions>類型針對Options對象的創建和初始化的實現原理,以及涉及的一些相關的接口和類型,下圖基本上反映了這些接口與類型的關系。

7-9

[ASP.NET Core 3框架揭秘] Options[1]: 配置選項的正確使用方式[上篇]
[ASP.NET Core 3框架揭秘] Options[2]: 配置選項的正確使用方式[下篇]
[ASP.NET Core 3框架揭秘] Options[3]: Options模型[上篇]
[ASP.NET Core 3框架揭秘] Options[4]: Options模型[下篇]
[ASP.NET Core 3框架揭秘] Options[5]: 依賴注入
[ASP.NET Core 3框架揭秘] Options[6]: 擴展與定制
[ASP.NET Core 3框架揭秘] Options[7]: 與配置系統的整合


免責聲明!

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



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