ASP.NET Core的配置(3): 將配置綁定為對象[上篇]
出于編程上的便利,我們通常不會直接利用ConfigurationBuilder創建的Configuration對象讀取某個單一配置項的值,而是傾向于將一組相關的配置綁定為一個對象,我們將后者稱為Options對象。我們在本章第一節通過簡單的實例演示了如何利用Options模型實現了配置數據向Options對象的綁定,現在我們對Options模型背后的實現原理進行詳細介紹。
目錄
一、ConfigurationBinder
二、綁定簡單對象
三、綁定復雜對象
四、綁定集合對象
五、綁定字典對象
一、ConfigurationBinder
配置在邏輯上體現為一個具有層次化的配置樹,對于一個Options對象來說,如果我們將其數據成員視為其子節點,那么Options對象同樣具有一個層次化屬性結構,所以Options對象和配置在數據結構層面并沒有本質的差異。如果Options類型的數據成員定義與配置的結構具有一一匹配關系,那么將后者綁定為一個對應類型的Options對象是一件很容易的事情,我們本節重點介紹的ConfigurationBinder就是利用這樣的原理實現了結構化配置向數據對象的自動綁定。
ConfigurationBinder是一個定義在程序集“Microsoft.Extensions.Configuration.Binder” 中的靜態類型,程序集的名稱同樣也是所在NuGet包的名稱,它提供的針對配置的綁定功能體現在它所定義的Bind和一系列Get方法中。如下面的代碼片段所示,這些都是針對IConfiguration接口的擴展方法。
1: public static class ConfigurationBinder
2: {
3: public static void Bind(this IConfiguration configuration, object instance);
4:
5: public static object Get(this IConfiguration configuration, Type type);
6: public static object Get(this IConfiguration configuration, Type type, string key);
7: public static T Get<T>(this IConfiguration configuration);
8: public static T Get<T>(this IConfiguration configuration, T defaultValue);
9: public static T Get<T>(this IConfiguration configuration, string key);
10: public static T Get<T>(this IConfiguration configuration, string key, T defaultValue);
11: }
我們可以調用Bind方法將一個Configuration對象綁定為一個預先創建的對象,而Get方法則直接根據指定類型(通過參數type或者方法的泛型參數類型決定)的對應數據對象并將Configuration對象承載的配置數據綁定在該對象上。如果調用具有參數key的Get方法,綁定的配置來源于由這個Key代表的子配置節。
ConfigurationBinder綁定的目標類型可以是一個簡單的基元類型,也可以是一個復雜的自定義數據類型,還可以是一個集合或者字典類型。通過上面的介紹我們知道配置的物理結構體現為一個二維數據字典,那么對于綁定生成的不同類型的數據對象,這些原始的數據如何通過一組字符串類型的鍵值對來表現呢?
二、綁定簡單數據類型
由于一個原子配置項總是體現為一個KeyValuePair <string,string >對象,所以配置綁定的原始數據類型是字符串。這里所謂的簡單數據類型和復雜數據類型只有一個界定標準,那就是是否支持源自字符串類型的數據轉換。簡單類型對象可以直接通過一個字符串轉換而來,復雜類型對象則不能。
如果綁定的目標類型為簡單類型,在進行配置綁定的時候自需要將配置項的值(體現為ConfigurationSection的Value屬性)轉換成對應的數據類型就可以了。由于所有基元類型(比如Int32、Double等)都是簡單類型,所以我們可以直接按照如下的方式綁定它們的值。
1: IConfiguration configuration = new ConfigurationBuilder()
2: .Add(new MemoryConfigurationProvider(new Dictionary<string, string>
3: {
4: ["foo"] = "abc",
5: ["bar"] = "123",
6: ["baz"] = "3.14"
7: })).Build();
8:
9: Debug.Assert(configuration.GetSection("foo").Get<string>() == "abc");
10: Debug.Assert(configuration.Get<int>("bar") == 123);
11: Debug.Assert(configuration.Get<double>("bar") == 3.14);
我們自定義的數據類型在默認情況下不屬于簡單類型,但是我們可以為它指定一個TypeConverter,如果后者支持源自字符串的類型轉換,那么該類型自然也就成為了一個簡單類型。如下面的代碼片段所示,我們定義了一個表示二維坐標點的Point類型,并通過標注TypeConverterAttribute特性應用了一個之處源自字符串類型轉換的TypeConverter(PointTypeConverter)。在進行配置綁定的時候,如果原始配置項具有匹配的格式,則可以直接將其綁定為一個Point對象。
1: [TypeConverter(typeof(PointTypeConverter))]
2: public class Point
3: {
4: public double X { get; set; }
5: public double Y { get; set; }
6: }
7:
8: public class PointTypeConverter : TypeConverter
9: {
10: public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
11: {
12: string[] split = value.ToString().Split(',');
13: return new Point
14: {
15: X = double.Parse(split[0].TrimStart('(')),
16: Y = double.Parse(split[1].TrimStart(')'))
17: };
18: }
19: }
20:
21: IConfiguration configuration = new ConfigurationBuilder().Add(new MemoryConfigurationProvider(new Dictionary<string, string>
22: {
23: ["point"] = "(1.2,3.4)"
24: })).Build();
25: Debug.Assert(configuration.Get<Point>("point").X == 1.2);
26: Debug.Assert(configuration.Get<Point>("point").Y == 3.4);
三、綁定復雜數據類型
如果通過一顆樹來表示一個復雜對象,那么真正的數據是通過葉子節點來承載的,并且葉子節點對應的數據類型均為簡單類型。如果通過一個二維數據字典來提供一個復雜對象所有的原始數據,那么這個字典中只需要包含葉子節點對應的值即可。至于如何通過一個字典對象體現復雜對象的結構,我們只需要將葉子節點所在的路徑作為字典元素的Key就可以了。
1: public class Profile
2: {
3: public Gender Gender { get; set; }
4: public int Age { get; set; }
5: public ContactInfo ContactInfo { get; set; }
6: }
7:
8: public class ContactInfo
9: {
10: public string Email { get; set; }
11: public string PhoneNo { get; set; }
12: }
13:
14: public enum Gender
15: {
16: Male,
17: Female
18: }
如上面的代碼片段所示,我們定義了一個表示個人基本信息的Profile類,定義其中的三個屬性(Gender、Age和ContactInfo)分別表示性別、年齡和聯系方式。表示聯系信息的ContactInfo對象具有兩個屬性(Email和PhoneNo)分別表示電子郵箱地址和電話號碼。一個完整的Profile對象可以通過如右圖所示的樹來體現。
如果需要通過配置的形式來表示一個完整的Profile對象,我們只需要將四個葉子節點(性別、年齡、電子郵箱地址和電話號碼)對應的數據定義在配置之中即可。對于承載配置數據的數據字典中,我們需要按照如左邊表格所示的方式將這四個葉子節點的路徑作為字典元素的Key。
我們通過一個簡單示例來演示針對復雜對象的配置綁定。我們創建一個ASP .NET Core控制臺應用中,并在project.json文件中添加針對NuGet包“Microsoft.Extensions.Configuration.Binder”的依賴。我們在作為程序入口的Main方法中定義了如下所示的程序。
1: public class Program
2: {
3: public static void Main(string[] args)
4: {
5: IConfiguration configuration = new ConfigurationBuilder().Add(new MemoryConfigurationProvider(new Dictionary<string, string>
6: {
7: ["Profile:Gender"] = "Male",
8: ["Profile:Age"] = "18",
9: ["Profile:ContactInfo:Email"] = "foobar@outlook.com",
10: ["Profile:ContactInfo:PhoneNo"]= "123456789",
11: })).Build();
12:
13: Profile profile = configuration.Get<Profile>("Profile");
14: Console.WriteLine("{0,-10}:{1}", "Gender", profile.Gender);
15: Console.WriteLine("{0,-10}:{1}", "Age", profile.Age);
16: Console.WriteLine("{0,-10}:{1}", "Email", profile.ContactInfo.Email);
17: Console.WriteLine("{0,-10}:{1}", "PhoneNo", profile.ContactInfo.PhoneNo);
18: }
19: }
如上面的代碼片段所示,我們創建了一個ConfigurationBuilder對象并為之添加了一個MemoryConfigurationProvider,后者按照如表2所示的結構提供了原始的配置數據。我們利用這個ConfigurationBuilder對象創建的Configuration對象并調用這個ConfigurationSection的Get方法將Key為“Profile”的配置節綁定為一個Profile對象。為了驗證配置綁定是否成功,我們最終將這個綁定的Profile對象的相關信息打印出來。該程序執行之后會在控制臺上產生如下所示的輸出結果。
1: Gender :Male
2: Age :18
3: Email :foobar@outlook.com
4: PhoneNo :123456789
四、綁定集合對象
配置綁定從本質上講就是承載相同信息的數據在不同結構之間的轉換,說得更加具體一點就是數據從基于數據字典的物理結構轉換成基于樹的邏輯結構。要理解針對集合的配置綁定,需要首先了解一個集合對象在邏輯上體現怎樣一棵樹。對于一個集合對象來說,組成它的元素自然視為集合的子節點,所以一個包含三個Profile對象的集合可以由左圖所示的樹來體現。
但是如8所示的這棵樹并不是一個合法的配置樹。對于這棵樹來說,表示集合元素的配置節都采用“Profile”作為Key,這導致了所有葉子節點的路徑并不是唯一的。由于路徑不唯一,我們自然不能將它作為一個字典對象的Key,那么構成這個集合的原子數據將無法通過一個數據字典來承載。為了解決這個問題,我們將集合元素的索引(0、1、2、 …)對應的配置節的Key,所以右圖所示的才是真正配置樹的結構。
既然我們能夠正確將集合對象通過一個合法的配置樹體現出來,那么我們就可以直接利用一個字典對象來提供構成這個集合對象的所有原子數據。數據字典中的每一個元素對應著配置樹中的某個葉子結點,后者的路徑直接作為字典元素的Key, 下面的表格清晰地體現了這個數據字典的結構。
我們依然通過一個簡單的實例來演示針對集合的配置綁定。如下面的代碼片段所示,我們創建了一個ConfigurationBuilder對象并為之添加了一個MemoryConfigurationProvider,后者按照如表3所示的結構提供了原始的配置數據。我們利用這個ConfigurationBuilder對象創建的Configuration對象并調用這個ConfigurationSection的Get方法將Key為“Profiles”的配置節綁定為一個List<Profile>對象。
1: public class Program
2: {
3: public static void Main(string[] args)
4: {
5: IConfiguration configuration = new ConfigurationBuilder().Add(new MemoryConfigurationProvider(new Dictionary<string, string>
6: {
7: ["Profiles:0:Gender"] = "Male",
8: ["Profiles:0:Age"] = "18",
9: ["Profiles:0:ContactInfo:Email"] = "foo@gmail.com",
10: ["Profiles:0:ContactInfo:PhoneNo"] = "123",
11:
12: ["Profiles:1:Gender"] = "Male",
13: ["Profiles:1:Age"] = "25",
14: ["Profiles:1:ContactInfo:Email"] = "bar@gmail.com",
15: ["Profiles:1:ContactInfo:PhoneNo"] = "456",
16:
17: ["Profiles:2:Gender"] = "Female",
18: ["Profiles:2:Age"] = "40",
19: ["Profiles:2:ContactInfo:Email"] = "baz@gmail.com",
20: ["Profiles:2:ContactInfo:PhoneNo"] = "789",
21: })).Build();
22:
23: IEnumerable<Profile> profiles = configuration.Get<List<Profile>>("Profiles");
24: foreach (Profile profile in profiles)
25: {
26:
27: Console.WriteLine("{0,-10}:{1}", "Gender", profile.Gender);
28: Console.WriteLine("{0,-10}:{1}", "Age", profile.Age);
29: Console.WriteLine("{0,-10}:{1}", "Email", profile.ContactInfo.Email);
30: Console.WriteLine("{0,-10}:{1}\n", "PhoneNo", profile.ContactInfo.PhoneNo);
31: }
32: }
33: }
為了驗證配置綁定是否成功,我們最終將這個綁定的List<Profile>對象的每個元素的相關信息打印出來,該程序執行之后會在控制臺上產生如下所示的輸出結果。
1: Gender :Male
2: Age :18
3: Email :foo@gmail.com
4: PhoneNo :123
5:
6: Gender :Male
7: Age :25
8: Email :bar@gmail.com
9: PhoneNo :456
10:
11: Gender :Female
12: Age :40
13: Email :baz@gmail.com
14: PhoneNo :789
五、綁定字典
字典可以視為元素類型為鍵值對的集合,兩者在配置樹上的表示非常相似,它們之間的唯一不同之處在于前者采用索引作為集合元素所在配置節的Key,后者直接將鍵值對的Key直接作為配置節的Key。舉個簡單的例子,我們通過一個Dictionary<string, Profile >對象來表示多個用戶的個人信息,并且將用戶名作為這個字典的Key,那么這個字典對象的配置樹將具有如右圖所示的結構(“Foo”、“Bar”和“Baz”表示用戶名的Key)。
表示集合與字典的配置樹在結構上基本類似,所以反映在基于數據字典的物理結構上也大同小異。對于右圖表示的Dictionary<string, Profile>對象,構造該對象的所有原子配置數據可以通過包含如下元素的數據字典來提供。
我們依然通過一個簡單的實例來演示針對字典的配置綁定。如下面的代碼片段所示,我們創建了一個ConfigurationBuilder對象并為之添加了一個MemoryConfigurationProvider,后者按照如左邊表格所示的結構提供了原始的配置數據。我們利用這個ConfigurationBuilder對象創建的Configuration對象并調用這個ConfigurationSection的Get方法將Key為“Profiles”的配置節綁定為一個Dictionary<string, Profile>對象。
1: public class Program
2: {
3: public static void Main(string[] args)
4: {
5: IConfiguration configuration = new ConfigurationBuilder().Add(new MemoryConfigurationProvider(new Dictionary<string, string>
6: {
7: ["Profile:Foo:Gender"] = "Male",
8: ["Profile:Foo:Age"] = "18",
9: ["Profile:Foo:ContactInfo:Email"] = "foo@gmail.com",
10: ["Profile:Foo:ContactInfo:PhoneNo"] = "123",
11:
12: ["Profile:Bar:Gender"] = "Male",
13: ["Profile:Bar:Age"] = "25",
14: ["Profile:Bar:ContactInfo:Email"] = "bar@gmail.com",
15: ["Profile:Bar:ContactInfo:PhoneNo"] = "456",
16:
17: ["Profile:Baz:Gender"] = "Female",
18: ["Profile:Baz:Age"] = "40",
19: ["Profile:Baz:ContactInfo:Email"] = "baz@gmail.com",
20: ["Profile:Baz:ContactInfo:PhoneNo"] = "789",
21: })).Build();
22:
23: Dictionary<string, Profile> profiles = configuration.Get<Dictionary<string, Profile>>("Profile");
24: foreach (var item in profiles)
25: {
26:
27: Console.WriteLine("{0,-10}:{1}", "Name", item.Key );
28: Console.WriteLine("{0,-10}:{1}", "Gender", item.Value.Gender);
29: Console.WriteLine("{0,-10}:{1}", "Age", item.Value.Age);
30: Console.WriteLine("{0,-10}:{1}", "Email", item.Value.ContactInfo.Email);
31: Console.WriteLine("{0,-10}:{1}\n", "PhoneNo", item.Value.ContactInfo.PhoneNo);
32: }
33: }
34: }
為了驗證配置綁定是否成功,我們最終將這個綁定的Dictionary<string, Profile>對象的每個元素的相關信息打印出來,該程序執行之后會在控制臺上產生如下所示的輸出結果。
1: Name :Foo
2: Gender :Male
3: Age :18
4: Email :foo@gmail.com
5: PhoneNo :123
6:
7: Name :Bar
8: Gender :Male
9: Age :25
10: Email :bar@gmail.com
11: PhoneNo :456
12:
13: Name :Baz
14: Gender :Female
15: Age :40
16: Email :baz@gmail.com
17: PhoneNo :789