1. 程式人生 > >[ASP.NET Core 3框架揭祕] Options[3]: Options模型[上篇]

[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>相關的介面和型別主要體現在下圖中。

下面以上圖為基礎介紹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物件的初始化。

三、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物件的建立和初始化的實現原理,以及涉及的一些相關的介面和型別,下圖基本上反映了這些介面與型別的關係。

[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]: 與配置系統的整合