通過前面演示的幾個實例(配置選項的正確使用方式[上篇]、配置選項的正確使用方式[下篇]),我們已經對基於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]: 與配置系統的整合