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