四、直接初始化Options對象
前面演示的幾個實例具有一個共同的特征,即都采用配置系統來提供綁定Options對象的原始數據,實際上,Options框架具有一個完全獨立的模型,可以稱為Options模型。這個獨立的Options模型本身並不依賴於配置系統,讓配置系統來提供配置數據僅僅是通過Options模型的一個擴展點實現的。在很多情況下,可能並不需要將應用的配置選項定義在配置文件中,在應用啟動時直接初始化可能是一種更方便快捷的方式。
class Program { static void Main() { var profile = new ServiceCollection() .AddOptions() .Configure<Profile>(it => { it.Gender = Gender.Male; it.Age = 18; it.ContactInfo = new ContactInfo { PhoneNo = "123456789", EmailAddress = "foobar@outlook.com" }; }) .BuildServiceProvider() .GetRequiredService<IOptions<Profile>>() .Value; Console.WriteLine($"Gender: {profile.Gender}"); Console.WriteLine($"Age: {profile.Age}"); Console.WriteLine($"Email Address: {profile.ContactInfo.EmailAddress}"); Console.WriteLine($"Phone No: {profile.ContactInfo.PhoneNo}\n"); } }
我們依然沿用前面演示的應用場景,現在摒棄配置文件,轉而采用編程的方式直接對用戶信息進行初始化,所以需要對程序做如上改寫。在調用IServiceCollection接口的Configure<Profile>擴展方法時,不需要再指定一個IConfiguration對象,而是利用一個Action<Profile>類型的委托對作為參數的Profile對象進行初始化。程序運行后會在控制台上產生下圖所示的輸出結果。
具名Options同樣可以采用類似的方式進行初始化。如果需要根據指定的名稱對Options進行初始化,那么調用方法時就需要指定一個Action<TOptions,String>類型的委托對象,該委托對象的第二個參數表示Options的名稱。在如下所示的代碼片段中,我們通過類似的方式設置了兩個用戶(foo和bar)的信息,然后利用IOptionsSnapshot<TOptions>服務將它們分別提取出來。
class Program { static void Main() { var optionsAccessor = new ServiceCollection() .AddOptions() .Configure<Profile>("foo", it => { it.Gender = Gender.Male; it.Age = 18; it.ContactInfo = new ContactInfo { PhoneNo = "123", EmailAddress = "foo@outlook.com" }; }) .Configure<Profile>("bar", it => { it.Gender = Gender.Female; it.Age = 25; it.ContactInfo = new ContactInfo { PhoneNo = "456", EmailAddress = "bar@outlook.com" }; }) .BuildServiceProvider() .GetRequiredService<IOptionsSnapshot<Profile>>(); Print(optionsAccessor.Get("foo")); Print(optionsAccessor.Get("bar")); static void Print(Profile profile) { Console.WriteLine($"Gender: {profile.Gender}"); Console.WriteLine($"Age: {profile.Age}"); Console.WriteLine($"Email Address: {profile.ContactInfo.EmailAddress}"); Console.WriteLine($"Phone No: {profile.ContactInfo.PhoneNo}\n"); }; } }
該程序運行后會在控制台上產生下圖所示的輸出結果。在前面的演示中,我們利用依賴注入框架提供IOptions<TOptions>服務、IOptionsSnapshot<TOptions>服務和IOptionsMonitor<TOptions>服務,然后進一步利用它們來提供對應的Options對象。既然作為依賴注入容器的IServiceProvider對象能夠提供這3個對象,我們就能夠將它們注入消費Options對象的類型中。所謂的Options模式就是通過注入這3個服務來提供對應Options對象的編程模式。
五、根據依賴服務的Options設置
在很多情況下需要針對某個依賴的服務動態地初始化Options的設置,比較典型的就是根據當前的承載環境(開發、預發和產品)對Options做動態設置。《上篇》演示了一系列針對時間日期輸出格式的配置,下面沿用這個場景演示如何根據當前的承載環境設置對應的Options。將DateTimeFormatOptions的定義進行簡化,只保留如下所示的表示日期和時間格式的兩個屬性。
public class DateTimeFormatOptions { public string DatePattern { get; set; } public string TimePattern { get; set; } public override string ToString() => $"Date: {DatePattern}; Time: {TimePattern}"; }
如下所示的代碼片段是整個演示實例的完整定義。我們利用第6章介紹的配置系統來設置當前的承載環境,具體采用的是基於命令行參數的配置源。.NET Core的承載系統通過IHostEnvironment接口表示承載環境,具體實現類型為HostingEnvironment。如下面的代碼片段所示,我們利用獲取的環境名稱創建了一個HostingEnvironment對象,並針對IHostEnvironment接口采用Singleton生命周期做了相應的注冊。
class Program { public static void Main(string[] args) { var environment = new ConfigurationBuilder() .AddCommandLine(args) .Build()["env"]; var services = new ServiceCollection(); services .AddSingleton<IHostEnvironment>(new HostingEnvironment { EnvironmentName = environment }) .AddOptions<DateTimeFormatOptions>().Configure<IHostEnvironment>( (options, env) => { if (env.IsDevelopment()) { options.DatePattern = "dddd, MMMM d, yyyy"; options.TimePattern = "M/d/yyyy"; } else { options.DatePattern = "M/d/yyyy"; options.TimePattern = "h:mm tt"; } }); var options = services .BuildServiceProvider() .GetRequiredService<IOptions<DateTimeFormatOptions>>().Value; Console.WriteLine(options); } }
上面調用IServiceCollection接口的AddOptions<DateTimeFormatOptions>擴展方法完成了針對Options模型核心服務的注冊和針對DateTimeFormatOptions的設置。該方法返回的是一個封裝了IServiceCollection集合的OptionsBuilder<DateTimeFormatOptions>對象,可以調用其Configure<IHostEnvironment>方法利用提供的Action<DateTimeFormatOptions, IHostEnvironment>委托對象針對依賴的IHostEnvironment服務對DateTimeFormatOptions做相應的設置。具體來說,我們針對開發環境和非開發環境設置了不同的日期時間格式。如果采用命令行的方式啟動這個應用程序,並利用命令行參數設置不同的環境名稱,就可以在控制台上看到下圖所示的針對DateTimeFormatOptions的不同設置。
六、驗證Options的有效性
由於配置選項是整個應用的全局設置,為了盡可能避免錯誤的設置造成的影響,最好能夠對內容進行有效性驗證。接下來我們將上面的程序做了如下改動,從而演示如何對設置的日期和時間格式做最后的有效性驗證。
class Program { public static void Main(string[] args) { var config = new ConfigurationBuilder() .AddCommandLine(args) .Build(); var datePattern = config["date"]; var timePattern = config["time"]; var services = new ServiceCollection(); services.AddOptions<DateTimeFormatOptions>() .Configure(options => { options.DatePattern = datePattern; options.TimePattern = timePattern; }) .Validate(options => Validate(options.DatePattern) && Validate(options.TimePattern),"Invalid Date or Time pattern."); try { var options = services .BuildServiceProvider() .GetRequiredService<IOptions<DateTimeFormatOptions>>().Value; Console.WriteLine(options); } catch (OptionsValidationException ex) { Console.WriteLine(ex.Message); } static bool Validate(string format) { var time = new DateTime(1981, 8, 24,2,2,2); var formatted = time.ToString(format); return DateTimeOffset.TryParseExact(formatted, format, null, DateTimeStyles.None, out var value) && (value.Date == time.Date || value.TimeOfDay == time.TimeOfDay); } } }
上述演示實例借助配置系統以命令行的形式提供了日期和時間格式化字符串。在創建了OptionsBuilder<DateTimeFormatOptions>對象並對DateTimeFormatOptions做了相應設置之后,我們調用Validate<DateTimeFormatOptions>方法利用提供的Func<DateTimeFormatOptions,bool>委托對象對最終的設置進行驗證。運行該程序並按照下圖所示的方式指定不同的格式化字符串,系統會根據我們指定的規則來驗證其有效性。
[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]: 與配置系統的整合