在 .NET 中,設定與選項模式其實有聯絡的(這些功能現在不僅限於 ASP.NET Core,而是作為平臺擴充套件來提供,在其他.NET 專案中都能用)。設定一般從多個來源(上一篇水文中的例子,記得否?)來讀取資料,最後以 Key - Value 的方式載入到應用程式中,然後應用程式可以讀取設定。這些來源有 JSON檔案、XML檔案等。上次老周還演示了 CSV 檔案。
而選項模式呢,說直白些就是一些簡單的類,多數情況下只定義些公共屬性,可以稱為選項類(從泛型約束而言,選項類的要求一般就是 class,也就是型別是類的就行)。這些類其根本作用也是用來設定應用程式的,只是它們以物件導向的方式把設定資訊封裝起來。也就是說,選項類可以與設定資訊做繫結。
咱們在定義選項類的時候,習慣讓類名以「Options」結尾。這也不是什麼硬規則,只不過易於理解罷了。你看到某個類以「Options」結尾,你就可以猜到它的用處——設定選項引數用的,其實也就是設定。
假設有這麼個類。
public class TestOptions { public int Key1 { get; set; } public string? Key2 { get; set; } }
看著很是簡單,就兩個屬性。
在 appsettings.json 檔案中,我們可以加上這樣一個節點:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "myConfig": { "key1": 5055, "key2": "Speaking Chinglish" } }
如你所見,「myConfig」 節點對應的那個 JObject 的屬性結構和 TestOptions 類相同。
appsettings.json 在應用程式初始化時是自動新增的,我們不需要再設定,直接改檔案就行,不用再添程式碼。IConfiguration 介面有個擴充套件方法 Bind,可以將設定資訊與某個型別物件直接繫結。
app.MapGet("/", () => { IConfigurationRoot config = (IConfigurationRoot)app.Configuration; // 找出我們要的節 IConfigurationSection myconfig = config.GetSection("myConfig"); TestOptions options = new(); // 直接繫結 myconfig.Bind(options); // 看看結果 return $"Key1 = {options.Key1}\nKey2 = {options.Key2}"; });
從 JSON 檔案讀出的設定資訊可以直接與 TestOptions 範例繫結。這樣,在程式碼執行後,能看到咱們剛剛在 JSON 檔案中設定的內容。
上面這種方法雖然能做到了繫結,但用起來還費勁。為啥不直接弄進服務容器中,來個依賴注入,豈不美哉?
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.Configure<TestOptions>(builder.Configuration.GetSection("myConfig")); var app = builder.Build();
Configure<TOptions> 是個擴充套件方法,它用來對選項類進行設定——就是為選項類的屬性賦值。選項模式不僅限於把設定資訊強型別化,它通用於應用程式內各種功能設定。如紀錄檔怎麼記錄、驗證策略、Cookie 策略等。
這裡注意在依賴注入時,我們不是直接用 TestOptions 型別,而是 IOptions<TOptions> 介面。比如,我們可以在 MVC 控制器中這樣玩:
public class HomeController : Controller { readonly TestOptions _options; // 建構函式,獲取注入的物件 public HomeController(IOptions<TestOptions> wrapper) { _options = wrapper.Value; } public IActionResult Index() { // 這裡存取選項 string s = $"Key1: {_options.Key1}\n"; s += $"Key2: {_options.Key2}"; return Content(s); } }
其實,咱們可以用的介面型別並不只有 IOptions<>,不妨看看 AddOptions 擴充套件方法的原始碼(OptionsServiceCollectionExtensions.cs)
public static IServiceCollection AddOptions(this IServiceCollection services) { ThrowHelper.ThrowIfNull(services); services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(UnnamedOptionsManager<>))); services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>))); services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>))); services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>))); services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>))); return services; }
以上原始碼解釋了為什麼依賴注入時用的是 IOptions<> 介面,在容器中註冊時用的就是這個介面嘛。其實,你可以用 IOptionsMonitor<TestOptions>。至於這幾個介面有啥區別,我們先不管,下一個世紀再告訴你。
AddOptions 方法我們一般不需要呼叫,ASP.NET Core 應用程式初始化時自動呼叫了。
為了使選項模式有更好的適用性,我們也可以通過委託來設定選項類。
builder.Services.Configure<TestOptions>(opt => { opt.Key1 = 999; opt.Key2 = "Hi, baby"; });
至此,Configure 方法的兩種用法就出來了:
1、用 IConfigure 物件,從設定資訊源中載入,並填充選項類的屬性值(這個挺像反序列化操作);
2、直接用委託。
不管是設定資訊還是委託,Configure 方法只是向服務容器新增了一組 IConfigureOptions<TOptions> 物件。它的實現型別有 ConfigureOptions<TOptions>、ConfigureNamedOptions<TOptions> 等。
我們以 ConfigureOptions<TOptions> 類為例分析一下,因為這個類比較簡單
public class ConfigureOptions<TOptions> : IConfigureOptions<TOptions> where TOptions : class { public ConfigureOptions(Action<TOptions>? action) { Action = action; } public Action<TOptions>? Action { get; } public virtual void Configure(TOptions options) { …… Action?.Invoke(options); } }
看到建構函式時,你有沒有發現點什麼?看,它是不是有個委託 Action<TOptions>,我們再回頭看看剛剛我們在服務容器呼叫的 Configure 擴充套件方法,它是不是有一個過載版本也有個 Action<TOptions> 的引數。
builder.Services.Configure<TestOptions>(opt => { opt.Key1 = 999; opt.Key2 = "Hi, baby"; });
對的,我們傳給它的委託就是傳到了 ConfigureOptions 的建構函式裡,ConfigureOptions 實現的就是 IConfigureOptions<TOptions> 介面。哦,原來是籍樣子滴。我們每呼叫一次 Configure 方法,它就向容器註冊一個 IConfigureOptions。
這只是放進了容器中罷了,並沒有馬上執行我們寫的委託。
那,它們在哪裡被呼叫呢?在工廠裡面——IOptionsFactory<TOptions>。
public interface IOptionsFactory<TOptions> where TOptions : class { TOptions Create(string name); }
別小看這貨,它可是核心角色。選項類的範例就是由它來建立的(實現 Create 方法)。你會注意到,這裡有個 name 引數,幹嗎的?這個是為選項的命名分組準備。一般可以不理會它,除非你要實現不同分組產生不太一樣的選項類範例,這樣就用得上了。
其實,如果需要,我們不妨為 TestOptions 類寫個小工廠(家庭小作坊)。
public class TestOptionsFactory : IOptionsFactory<TestOptions> { public TestOptions Create(string name) { return new TestOptions(); } }
簡單吧,然後咱用呢,不用管它咋用,你只要註冊到服務容器中就行了。
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddTransient<IOptionsFactory<TestOptions>, TestOptionsFactory>();
這裡我註冊為暫時性服務,你也可以考慮註冊為其他的,不衝突就行。
你要是有疑問,我自定義的工廠它會執行嗎?會的,不信打個斷點看看。
然後執行程式,瞧,這不,停下來了。
可,可,可是,這TNND,為什麼是預設值,我剛剛不是 Configure 用委託設定了嗎?
builder.Services.Configure<TestOptions>(opt => { opt.Key1 = 999; opt.Key2 = "Hi, baby"; });
無效?出啥故障了?等等,剛剛是不是說了,Configure 方法每呼叫一次就會註冊一個 IConfigureOptions<TOptions> 嗎?
想起來了,所以我們的工廠要改造一下,通過依賴注入把這一堆 IConfigureOptions 弄進來,然後逐個呼叫,這樣就可以為選項類的屬性賦值了。
public class TestOptionsFactory : IOptionsFactory<TestOptions> { readonly IEnumerable<IConfigureOptions<TestOptions>> _configs; public TestOptionsFactory(IEnumerable<IConfigureOptions<TestOptions>> configs) { _configs = configs; } public TestOptions Create(string name) { TestOptions opt = new(); foreach(var cfg in _configs) { cfg.Configure(opt); } return opt; } }
看,這就成了。
其實,工廠所需要的依賴物件並不只有 IConfigureOptions,還有兩個介面,需要了解一下:
1、IPostConfigureOptions:這貨史稱「後期設定」。其用法和 IConfigureOptions 一樣的。對應的服務容器擴充套件方法是 PostConfigure<TOptions>,用法和 Configure<TOptions> 擴充套件方法完全一樣的。它的用途是進行選項設定後做一些「修修補補」。比如,看看你還有哪些屬性沒賦值的,給它安排個預設值。有人會說,那我在建立選項類範例時就為每個屬性分配預設值不就行了嗎?還要啥後期設定?舉個例子:
public class ProgressOptions { public int Max { get; set; } public int Min { get; set; } public int Current { get; set; } }
就算我在工廠類中範例化時分配 Max = 100, Min = 0, Current = 50。那,如果我設定的時候是這樣的呢:
builder.Services.Configure<ProgressOptions>(o => { o.Max = 60; o.Min = 0; });
假如我沒設定 Current 屬性,預設規則是它取中值,那這時候取中值 50 顯然就不行了。而是得根據 Max 和 Min 的值來決定。這時候用後期設定就很必要了。
builder.Services.PostConfigure<ProgressOptions>(o => { o.Current = (o.Max - o.Min) / 2; });
2、IValidateOptions:這貨用來做驗證用的。這個和上面的「修修補補」不同。驗證是檢查選項類的屬性值是否符合要求。如果不符合,直接給你來個異常——回爐重造。
我們不妨看看預設的工作是怎麼實現的。
public class OptionsFactory<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> : IOptionsFactory<TOptions> where TOptions : class { private readonly IConfigureOptions<TOptions>[] _setups; private readonly IPostConfigureOptions<TOptions>[] _postConfigures; private readonly IValidateOptions<TOptions>[] _validations; public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures) : this(setups, postConfigures, validations: Array.Empty<IValidateOptions<TOptions>>()) { } public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures, IEnumerable<IValidateOptions<TOptions>> validations) { _setups = setups as IConfigureOptions<TOptions>[] ?? new List<IConfigureOptions<TOptions>>(setups).ToArray(); _postConfigures = postConfigures as IPostConfigureOptions<TOptions>[] ?? new List<IPostConfigureOptions<TOptions>>(postConfigures).ToArray(); _validations = validations as IValidateOptions<TOptions>[] ?? new List<IValidateOptions<TOptions>>(validations).ToArray(); } public TOptions Create(string name) { TOptions options = CreateInstance(name); foreach (IConfigureOptions<TOptions> setup in _setups) { if (setup is IConfigureNamedOptions<TOptions> namedSetup) { namedSetup.Configure(name, options); } else if (name == Options.DefaultName) { setup.Configure(options); } } foreach (IPostConfigureOptions<TOptions> post in _postConfigures) { post.PostConfigure(name, options); } if (_validations.Length > 0) { var failures = new List<string>(); foreach (IValidateOptions<TOptions> validate in _validations) { ValidateOptionsResult result = validate.Validate(name, options); if (result is not null && result.Failed) { failures.AddRange(result.Failures); } } if (failures.Count > 0) { throw new OptionsValidationException(name, typeof(TOptions), failures); } } return options; } /// <summary> /// 這裡是建立選項類範例的地方,為了通用化,它用了 Activator /// </summary> protected virtual TOptions CreateInstance(string name) { return Activator.CreateInstance<TOptions>(); } }
從其原始碼我們知道它們的執行順序:
N多個IConfigureOptions ---> N多個IPostConfigureOptions ---> N多個IValidateOptions。
好了,下面咱們解決最後一個疑問:選項類的範例怎麼跑到 IOptions<TOptions> 裡面的?通過前面的例子,咱們都知道,通過依賴注入存取選項類時,是通過 IOptions<> 等介面的。咋關聯起來的?還是跟 Factory 有關,不然就不會說它是核心角色。
這裡老周只說一對服務型別,其他的實現類似。在 AddOptions 擴充套件方法中,通過註冊的服務型別得知,IOptions<> 對應的是 UnnamedOptionsManager。這個類沒有公開,它的原始碼如下:
internal sealed class UnnamedOptionsManager<TOptions> : IOptions<TOptions> where TOptions : class { private readonly IOptionsFactory<TOptions> _factory; private volatile object? _syncObj; private volatile TOptions? _value; public UnnamedOptionsManager(IOptionsFactory<TOptions> factory) => _factory = factory; public TOptions Value { get { if (_value is TOptions value) { return value; } lock (_syncObj ?? Interlocked.CompareExchange(ref _syncObj, new object(), null) ?? _syncObj) { return _value ??= _factory.Create(Options.DefaultName); } } } }
現在知道它們怎麼關聯起來了吧——還是一樣的套路,用依賴注入。
經過老週上文一系列胡說八道,這些與選項模式有關的介面之間的關係就清晰了。