依賴注入不僅是支撐整個ASP.NET Core框架的基石,也是開發ASP.NET Core應用采用的基本編程模式,所以依賴注入十分重要。依賴注入使我們可以將依賴的功能定義成服務,最終以一種松耦合的形式注入消費該功能的組件或者服務中。除了采用依賴注入的形式消費承載某種功能的服務,還可以采用相同的方式消費承載配置數據的Options對象。
一、將配置綁定為Options對象
Options模式是一種采用依賴注入的方式來提供Options對象的編程方式,但這並不意味着我們會直接利用依賴注入框架來提供Options對象本身,因為利用依賴注入框架獲取的是一個能夠提供Options對象的IOptions<TOptions>對象,泛型參數TOptions表示的正是Options對象的類型。下面的演示實例利用IOptions<TOptions>服務來提供我們需要的Options對象,該對象由一個承載配置數據的IConfiguration對象綁定而成。簡單起見,我們依然沿用《[ASP.NET Core 3框架揭秘] 配置[4]:將配置綁定為對象》定義的Profile作為基礎的Options類型,下面先回顧相關類型的定義。
public class Profile : IEquatable<Profile> { public Gender Gender { get; set; } public int Age { get; set; } public ContactInfo ContactInfo { get; set; } public Profile() { } public Profile(Gender gender, int age, string emailAddress, string phoneNo) { Gender = gender; Age = age; ContactInfo = new ContactInfo { EmailAddress = emailAddress, PhoneNo = phoneNo }; } public bool Equals(Profile other) { return other == null? false : Gender == other.Gender &&Age == other.Age && ContactInfo.Equals(other.ContactInfo); } } public class ContactInfo : IEquatable<ContactInfo> { public string EmailAddress { get; set; } public string PhoneNo { get; set; } public bool Equals(ContactInfo other)=> other == null ? false : EmailAddress == other.EmailAddress && PhoneNo == other.PhoneNo; } public enum Gender { Male, Female }
下面通過一個簡單的控制台應用來演示Options編程模式。在演示程序中定義了上面這些類型之后,我們創建承載一個Profile對象的配置文件profile.json。如下所示的代碼片段就是這個JSON文件的內容,它提供了構成一個完整Profile對象的所有數據。為了使該文件能夠在編譯后自動復制到輸出目錄,我們需要將Copy to Output Directory屬性設置為Copy Always。
{ "gender" : "Male", "age" : "18", "contactInfo": { "emailAddress": "foobar@outlook.com", "phoneNo" : "123456789" } }
下面編寫代碼來演示如何采用Options模式獲取由配置文件提供的數據綁定生成的Profile對象。我們調用AddJsonFile擴展方法將針對JSON配置文件(profile.json)的配置源注冊到創建的ConfigurationBuilder對象上,並利用它創建對應的IConfigurataion對象。
class Program { static void Main() { var configuration = new ConfigurationBuilder() .AddJsonFile("profile.json") .Build(); var profile = new ServiceCollection() .AddOptions() .Configure<Profile>(configuration) .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}"); } }
上面創建一個ServiceCollection對象,在調用AddOptions擴展方法注冊Options編程模式的核心服務后,可以將創建的IConfiguration對象作為參數調用Configure<Profile>擴展方法。Configure<TOptions>擴展方法相當於將提供的IConfiguration對象與指定的TOptions類型做了一個映射,在需要提供對應TOptions對象時,IConfiguration對象承載的配置數據會被提取出來並綁定生成返回的TOptions對象。
在調用IServiceCollection的BuildServiceProvider擴展方法得到作為依賴注入容器的IServiceProvider對象之后,可以直接調用其GetRequiredService<T>擴展方法來提供IOptions<Profile>對象,該對象的Value屬性返回的就是指定IConfiguration對象綁定生成的Profile對象。我們將這個Profile對象承載的相關數據直接打印在控制台上,輸出結果如下圖所示,由此可以看出,通過Options模式得到的Profile對象承載的數據完全來源於配置文件。
二、提供具名的Options
針對同一個Options類型,通過IOptions<TOptions>服務在整個應用范圍內只能提供一個單一的Options對象,但是在很多情況下我們需要利用多個同類型的Options對象來承載不同的配置。就演示實例中用來表示個人信息的Profile類型來說,應用程序中可能會使用它來表示不同用戶的信息,如張三、李四和王五。為了解決這個問題,我們可以在添加IConfiguration對象與Options類型映射關系時賦予它們一個唯一標識,這個標識最終會被用來提取對應的Options對象。這種具名的Options對象由IOptionsSnapshot<TOptions>接口表示的服務提供。
同樣,針對前面的演示實例,假設的應用需要采用Options模式提取承載不同用戶信息的Profile對象,具體應該如何實現?由於采用JSON格式的配置文件來提供原始的用戶信息,所以需要將針對多個用戶的信息定義在profile.json文件中。我們通過如下形式提供了兩個用戶(foo和bar)的基本信息。
{ "foo": { "gender": "Male", "age": "18", "contactInfo": { "emailAddress": "foo@outlook.com", "phoneNo": "123" } }, "bar": { "gender": "Female", "age": "25", "contactInfo": { "emailAddress": "bar@outlook.com", "phoneNo": "456" } } }
具名Options的注冊和提取體現在如下所示的代碼片段中。在調用IServiceCollection接口的Configure<TOptions>擴展方法時,我們將注冊的映射關系命名為foo和bar,提供原始配置數據的IConfiguration對象也由原來的ConfigurationRoot對象變成它的兩個子配置節。
class Program { static void Main() { var configuration = new ConfigurationBuilder() .AddJsonFile("profile.json") .Build(); var serviceProvider = new ServiceCollection() .AddOptions() .Configure<Profile>("foo", configuration.GetSection("foo")) .Configure<Profile>("bar", configuration.GetSection("bar")) .BuildServiceProvider(); var optionsAccessor = serviceProvider.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"); } } }
為了使用指定的用戶名來提取對應的Profile對象,可以利用作為依賴注入容器的IServiceProvider對象得到IOptionsSnapshot<TOptions>服務,並將用戶名作為參數調用其Get方法得到對應的Profile對象。程序運行后,針對兩個不同用戶的基本信息將以下圖所示的形式輸出到控制台上。
三、配置源的同步
通過《配置數據與數據源的實時同步》的介紹可知,配置模型不僅支持對配置源的監控,還可以在檢測到更新之后及時加載新的配置數據,並通過一個IChangeToken對象對外發送通知。對於前面演示的兩個實例來說,提供的Options對象都是由配置文件提供的數據綁定生成的,如果新的配置數據被重新加載之后能夠提供與之匹配的Options對象,那么這將是最理想的編程模式,可以通過IOptionsMonitor<TOptions>服務來實現。
前面演示的第一個實例利用JSON文件定義了一個單一Profile對象的信息,下面對它做相應的修改來演示如何監控這個JSON文件,並在監測到文件改變之后及時提取新的配置信息生成新的Profile對象。如下面的代碼片段所示,調用AddJsonFile擴展方法注冊對應配置源時應將該方法的參數reloadOnChange設置為True,從而開啟對對應配置文件的監控功能。
class Program { static void Main() { var configuration = new ConfigurationBuilder() .AddJsonFile(path: "profile.json", optional: false, reloadOnChange: true) .Build(); new ServiceCollection() .AddOptions() .Configure<Profile>(configuration) .BuildServiceProvider() .GetRequiredService<IOptionsMonitor<Profile>>() .OnChange(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"); }); Console.Read(); } }
在得到作為依賴注入容器的IServiceProvider對象之后,可以利用它得到IOptionsMonitor<TOptions>服務,該對象會接收到配置系統發出的關於配置被重新加載的通知,並在收到通知后重新生成Options對象。我們調用IOptionsMonitor<TOptions>對象的OnChange方法注冊了一個類型為Action<TOptions>的委托對象,該委托對象會在接收到Options變化時自動執行,而作為輸入的正是重新生成的Options對象。由於注冊的委托對象會將新Profile對象的相關屬性打印在控制台上,所以程序啟動后針對配置文件的任何修改都會導致新的數據被打印在控制台上。例如,我們先后修改了年齡(25)和性別(Female),新的數據將按照下圖所示的形式反映在控制台上。
具名Options同樣可以采用類似的編程模式來實現配置的同步。在前面演示的提供具名Options的第二個實例的基礎上,我們對程序做了如下修改。與之前不同的是,在利用IServiceProvider對象得到IOptionsMonitor<TOptions>服務之后,可以調用其OnChange方法注冊的回調是一個Action<TOptions, String>對象,該委托對象的第二個參數表示的正是在注冊IConfiguration對象與Options類型應用關系時指定的名稱。
class Program { static void Main() { var configuration = new ConfigurationBuilder() .AddJsonFile(path: "profile.json", optional: false, reloadOnChange: true) .Build(); new ServiceCollection() .AddOptions() .Configure<Profile>("foo", configuration.GetSection("foo")) .Configure<Profile>("bar", configuration.GetSection("bar")) .BuildServiceProvider() .GetRequiredService<IOptionsMonitor<Profile>>() .OnChange((profile, name) => { Console.WriteLine($"Name: {name}"); 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"); }); Console.Read(); } }
由於通過調用OnChange方法注冊的委托對象會將Options的名稱和承載的數據打印在控制台上,所以控制台上輸出的內容總是與配置文件的內容同步。例如,在程序啟動后,我們分別修改了用戶foo的年齡(25)和用戶bar的性別(Male),新的內容將以圖7-4所示的形式及時呈現在控制台上。
[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]: 與配置系統的整合