雖然應用程序可以直接利用通過IConfigurationBuilder對象創建的IConfiguration對象來提取配置數據,但是我們更傾向於將其轉換成一個POCO對象,以面向對象的方式來使用配置,我們將這個轉換過程稱為配置綁定。配置綁定可以通過如下幾個針對IConfiguration的擴展方法來實現,這些擴展方法都定義在NuGet包“Microsoft.Extensions.Configuration.Binder”中。
一、ConfigurationBinder
public static class ConfigurationBinder { public static void Bind(this IConfiguration configuration, object instance); public static void Bind(this IConfiguration configuration, object instance, Action<BinderOptions> configureOptions); public static void Bind(this IConfiguration configuration, string key, object instance); public static T Get<T>(this IConfiguration configuration); public static T Get<T>(this IConfiguration configuration, Action<BinderOptions> configureOptions); public static object Get(this IConfiguration configuration, Type type); public static object Get(this IConfiguration configuration, Type type, Action<BinderOptions> configureOptions); } public class BinderOptions { public bool BindNonPublicProperties { get; set; } }
Bind方法將指定的IConfiguration對象(對應於configuration參數)綁定一個預先創建的對象(對應於instance參數),如果參數綁定的只是當前IConfiguration對象的某個子配置節,我們需要通過參數sectionKey指定對應子配置節的相對路徑。Get和Get<T>方法則直接將指定的IConfiguration對象轉換成指定類型的POCO對象。
旨在生成POCO對象的配置綁定實現在IConfiguration接口的擴展方法Bind上。配置綁定的目標類型可以是一個簡單的基元類型,也可以是一個自定義數據類型,還可以是一個數組、集合或者字典類型。通過前面的介紹我們知道IConfigurationProvider對象將原始的配置數據讀取出來后會將其轉換成Key和Value均為字符串的數據字典,那么針對這些完全不同的目標類型,原始的配置數據如何通過數據字典的形式來體現呢?
二、綁定配置項的值
我們知道配置模型采用字符串鍵值對的形式來承載基礎配置數據,我們將這組鍵值對稱為配置字典,扁平的字典因為采用路徑化的Key使配置項在邏輯上具有了層次結構。IConfigurationBuilder對象將配置的層次化結構體現在由它創建的IConfigurationRoot對象上,我們將IConfigurationRoot對象視為一棵配置樹。所謂的配置綁定體現為如何將映射為配置樹上某個節點的IConfiguration對象(可以是IConfigurationRoot對象或者IConfigurationSection對象)轉換成一個對應的POCO對象。
對於針對IConfiguration對象的配置綁定來說,最簡單的莫過於針對葉子節點的IConfigurationSection對象的綁定。表示配置樹葉子節點的IConfigurationSection對象承載着原子配置項的值,而且這個值是一個字符串,那么針對它的配置綁定最終體現為如何將這個字符串轉換成指定的目標類型,這樣的操作體現在IConfiguration如下兩個擴展方法GetValue上。
public static class ConfigurationBinder { public static T GetValue<T>(IConfiguration configuration, string sectionKey); public static T GetValue<T>(IConfiguration configuration, string sectionKey, T defaultValue); public static object GetValue(IConfiguration configuration, Type type, string sectionKey); public static object GetValue(IConfiguration configuration, Type type, string sectionKey, object defaultValue); }
對於給出的這四個重載,其中兩個方法定義了一個表示默認值的defaultValue參數,如果對應配置節的值為Null或者空字符串,指定的默認值將作為方法的返回值。對於其他的方法重載,它們實際上將Null或者Default(T)作為隱式默認值。上述這些GetValue方法被執行的時候,它們會將配置節名稱(對應sectionKey參數)作為參數調用指定IConfiguation對象的GetSection方法得到表示對應配置節的IConfigurationSection對象,它的Value屬性被提取出來並按照如下的邏輯轉換成目標類型:
- 如果目標類型為object,直接返回原始值(字符串或者Null)。
- 如果目標類型不是Nullable<T>,那么針對目標類型的TypeConverter將被用來做類型轉換。
- 如果目標類型為Nullable<T>,那么在原始值不為Null或者空字符串的情況下會將基礎類型T作為新的目標類型進行轉換,否則直接返回Null。
為了驗證上述這些類型轉化規則,我們編寫了如下的測試程序。如下面的代碼片段所示,我們利用注冊的MemoryConfigurationSource添加了三個配置項,對應的值分別為Null、空字符串和“123”,然后調用GetValue方法分別對它們進行類型轉換,轉換的目標類型分別是Object、Int32和Nullable<Int32>,上述的轉換規則體現在對應的調試斷言中。
public class Program { public static void Main() { var source = new Dictionary<string, string> { ["foo"] = null, ["bar"] = "", ["baz"] = "123" }; var root = new ConfigurationBuilder() .AddInMemoryCollection(source) .Build(); //針對object Debug.Assert(root.GetValue<object>("foo") == null); Debug.Assert("".Equals(root.GetValue<object>("bar"))); Debug.Assert("123".Equals(root.GetValue<object>("baz"))); //針對普通類型 Debug.Assert(root.GetValue<int>("foo") == 0); Debug.Assert(root.GetValue<int>("baz") == 123); //針對Nullable<T> Debug.Assert(root.GetValue<int?>("foo") == null); Debug.Assert(root.GetValue<int?>("bar") == null); } }
三、自定義TypeConverter
按照前面介紹的類型轉換規則,如果目標類型支持源自字符串的類型轉換,那么我們就能夠將配置項的原始值綁定為該類型的對象,而讓某個類型支持某種類型轉換規則的途徑就是為之注冊相應的TypeConverter。如下面的代碼片段所示,我們定義了一個表示二維坐標的Point對象,並為它注冊了一個類型為PointTypeConverter的TypeConverter,PointTypeConverter通過實現的ConvertFrom方法將坐標的字符串表達式(比如“(123,456)”)轉換成一個Point對象。
[TypeConverter(typeof(PointTypeConverter))] public class Point { public double X { get; set; } public double Y { get; set; } } public class PointTypeConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) => sourceType == typeof(string); public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { string[] split = value.ToString().Split(','); double x = double.Parse(split[0].Trim().TrimStart('(')); double y = double.Parse(split[1].Trim().TrimEnd(')')); return new Point { X = x, Y = y }; } }
由於定義的Point類型支持源自字符串的類型轉換,所以如果配置項的原始值(字符串)具有與之兼容的格式,我們將能按照如下的方式將它綁定為一個Point對象。(S608)
public class Program { public static void Main() { var source = new Dictionary<string, string> { ["point"] = "(123,456)" }; var root = new ConfigurationBuilder() .AddInMemoryCollection(source) .Build(); var point = root.GetValue<Point>("point"); Debug.Assert(point.X == 123); Debug.Assert(point.Y == 456); } }
四、綁定復合數據類型
這里所謂的復合類型表示一個具有屬性數據成員的自定義類型。如果通過一顆樹來表示一個復合對象,那么葉子節點承載所有的數據,並且葉子節點的數據類型均為基元類型。如果通過數據字典來提供一個復雜對象所有的原始數據,那么這個字典中只需要包含葉子節點對應的值即可。至於如何通過一個字典對象體現復合對象的結構,我們只需要將葉子節點所在的路徑作為字典元素的Key就可以了。
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 : Gende == 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) { return other == null ? false : EmailAddress == other.EmailAddress && PhoneNo == other.PhoneNo; } } public enum Gender { Male, Female }
如上面的代碼片段所示,我們定義了一個表示個人基本信息的Profile類,它的三個屬性(Gender、Age和ContactInfo)分別表示性別、年齡和聯系方式。由於配置綁定會調用默認無參構造函數來創建綁定的目標對象,所以我們需要為Profile類型定義一個默認構造函數。表示聯系信息的ContactInfo類型具有兩個屬性(EmailAddress和PhoneNo),它們分別表示電子郵箱地址和電話號碼。一個完整的Profile對象可以通過如下圖所示的樹來體現。
如果需要通過配置的形式來表示一個完整的Profile對象,我們只需要將四個葉子節點(性別、年齡、電子郵箱地址和電話號碼)對應的數據由配置來提供即可。對於承載配置數據的數據字典,我們需要按照如下表所示的方式將這四個葉子節點的路徑作為字典元素的Key。
Key
|
Value |
Gender | Male |
Age | 18 |
ContactInfo:Email | foobar@outlook.com |
ContactInfo:PhoneNo | 123456789 |
我們通過如下的程序來驗證針對復合數據類型的配置綁定。我們創建了一個ConfigurationBuilder對象並為它添加了一個MemoryConfigurationSource對象,它按照如上表所示的結構提供了原始的配置數據。在調用Build方法構建出IConfiguration對象之后,我們直接調用擴展方法Get<T>將它轉換成一個Profile對象。
public class Program { public static void Main() { var source = new Dictionary<string, string> { ["gender"] = "Male", ["age"] = "18", ["contactInfo:emailAddress"] = "foobar@outlook.com", ["contactInfo:phoneNo"] = "123456789" }; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(source) .Build(); var profile = configuration.Get<Profile>(); Debug.Assert(profile.Equals( new Profile(Gender.Male, 18, "foobar@outlook.com", "123456789"))); } }
五、綁定集合對象
如果配置綁定的目標類型是一個集合(包括數組),那么當前IConfiguration對象的每一個子配置節將綁定為集合的元素。假設我們需要將一個IConfiguration對象綁定為一個元素類型為Profile的集合,它表示的配置樹應該具有如下圖所示的結構。
既然我們能夠正確將集合對象通過一個合法的配置樹體現出來,那么我們就可以將它轉換成配置字典。對於通過下表所示的這個包含三個元素的Profile集合,我們可以采用如下表所示的結構來定義對應的配置字典。
Key
|
Value |
foo:Gender | Male |
foo:Age | 18 |
foo:ContactInfo:EmailAddress | foo@outlook.com |
foo:ContactInfo:PhoneNo | 123 |
bar:Gender | Male |
bar:Age | 25 |
bar:ContactInfo:EmailAddress | bar@outlook.com |
bar:ContactInfo:PhoneNo | 456 |
baz:Gender | Female |
baz:Age | 40 |
baz:ContactInfo:EmailAddress | baz@outlook.com |
baz:ContactInfo:PhoneNo | 789 |
我們依然通過一個簡單的實例來演示針對集合的配置綁定。如下面的代碼片段所示,我們創建了一個ConfigurationBuilder對象並為它注冊了一個MemoryConfigurationSource對象,它按照如s上表所示的結構提供了原始的配置數據。在得到這個ConfigurationBuilder對象創建的IConfiguration對象之后,我們兩次調用其Get<T>方法將它分別綁定為一個IEnumerable<Profile>對象和一個Profile[] 數組。由於IConfigurationProvider通過GetChildKeys方法提供的Key是經過排序的,所以在綁定生成的集合或者數組中的元素的順序與配置源是不相同的,如下的調試斷言也體現了這一點。
public class Program { public static void Main() { var source = new Dictionary<string, string> { ["foo:gender"] = "Male", ["foo:age"] = "18", ["foo:contactInfo:emailAddress"] = "foo@outlook.com", ["foo:contactInfo:phoneNo"] = "123", ["bar:gender"] = "Male", ["bar:age"] = "25", ["bar:contactInfo:emailAddress"] = "bar@outlook.com", ["bar:contactInfo:phoneNo"] = "456", ["baz:gender"] = "Female", ["baz:age"] = "36", ["baz:contactInfo:emailAddress"] = "baz@outlook.com", ["baz:contactInfo:phoneNo"] = "789" }; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(source) .Build(); var profiles = new Profile[] { new Profile(Gender.Male,18,"foo@outlook.com","123"), new Profile(Gender.Male,25,"bar@outlook.com","456"), new Profile(Gender.Female,36,"baz@outlook.com","789"), }; var collection = configuration.Get<IEnumerable<Profile>>(); Debug.Assert(collection.Any(it => it.Equals(profiles[0]))); Debug.Assert(collection.Any(it => it.Equals(profiles[1]))); Debug.Assert(collection.Any(it => it.Equals(profiles[2]))); var array = configuration.Get<Profile[]>(); Debug.Assert(array[0].Equals(profiles[1])); Debug.Assert(array[1].Equals(profiles[2])); Debug.Assert(array[2].Equals(profiles[0])); } }
在針對集合類型的配置綁定過程中,如果某個配置節綁定失敗,該配置節將被忽略並選擇下一個配置節繼續進行綁定。但是如果目標類型為數組,最終綁定生成的數組長度與子配置節的個數總是一致的,綁定失敗的元素將被設置為Null。比如我們將上面的程序作了如下的改寫,保存原始配置的字典對象包含兩個元素,第一個元素的性別從“Male”改為“男”,毫無疑問這個值是不可能轉換成Gender枚舉對象的,所以針對這個Profile的配置綁定會失敗。如果將目標類型設置為IEnumerable<Profile>,那么最終生成的集合只會有兩個元素,倘若目標類型切換成Profile數組,數組的長度依然為3,但是第一個元素是Null。
public class Program { public static void Main() { var source = new Dictionary<string, string> { ["foo:gender"] = "男", ["foo:age"] = "18", ["foo:contactInfo:emailAddress"] = "foo@outlook.com", ["foo:contactInfo:phoneNo"] = "123", ["bar:gender"] = "Male", ["bar:age"] = "25", ["bar:contactInfo:emailAddress"] = "bar@outlook.com", ["bar:contactInfo:phoneNo"] = "456", ["baz:gender"] = "Female", ["baz:age"] = "36", ["baz:contactInfo:emailAddress"] = "baz@outlook.com", ["baz:contactInfo:phoneNo"] = "789" }; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(source) .Build(); var collection = configuration.Get<IEnumerable<Profile>>(); Debug.Assert(collection.Count() == 2); var array = configuration.Get<Profile[]>(); Debug.Assert(array.Length == 3); Debug.Assert(array[2] == null); //由於配置節按照Key進行排序,綁定失敗的配置節為最后一個 } }
六、綁定字典
能夠通過配置綁定生成的字典是一個實現了IDictionary<string,T>的類型,也就是說配置模型沒有對字典的Value類型作任何要求,但是字典對象的Key必須是一個字符串(或者枚舉)。如果采用配置樹的形式來表示這么一個字典對象,我們會發現它與針對集合的配置樹在結構上幾乎是一樣的。唯一的區別是集合元素的索引直接變成了字典元素的Key。
也就是說上圖所示的這棵配置樹同樣可以表示成一個具有三個元素的Dictionary<string, Profile>對象 ,它們對應的Key分別是“Foo”、“Bar”和“Baz”,所以我們可以按照如下的方式將承載相同數據的IConfiguration對象綁定為一個IDictionary<string,T>對象。(S612)
public class Program { public static void Main() { var source = new Dictionary<string, string> { ["foo:gender"] = "Male", ["foo:age"] = "18", ["foo:contactInfo:emailAddress"] = "foo@outlook.com", ["foo:contactInfo:phoneNo"] = "123", ["bar:gender"] = "Male", ["bar:age"] = "25", ["bar:contactInfo:emailAddress"] = "bar@outlook.com", ["bar:contactInfo:phoneNo"] = "456", ["baz:gender"] = "Female", ["baz:age"] = "36", ["baz:contactInfo:emailAddress"] = "baz@outlook.com", ["baz:contactInfo:phoneNo"] = "789" }; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(source) .Build(); var profiles = configuration.Get<IDictionary<string, Profile>>(); Debug.Assert(profiles["foo"].Equals( new Profile(Gender.Male, 18, "foo@outlook.com", "123"))); Debug.Assert(profiles["bar"].Equals( new Profile(Gender.Male, 25, "bar@outlook.com", "456"))); Debug.Assert(profiles["baz"].Equals( new Profile(Gender.Female, 36, "baz@outlook.com", "789"))); } }
[ASP.NET Core 3框架揭秘] 配置[1]:讀取配置數據[上篇]
[ASP.NET Core 3框架揭秘] 配置[2]:讀取配置數據[下篇]
[ASP.NET Core 3框架揭秘] 配置[3]:配置模型總體設計
[ASP.NET Core 3框架揭秘] 配置[4]:將配置綁定為對象
[ASP.NET Core 3框架揭秘] 配置[5]:配置數據與數據源的實時同步
[ASP.NET Core 3框架揭秘] 配置[6]:多樣化的配置源[上篇]
[ASP.NET Core 3框架揭秘] 配置[7]:多樣化的配置源[中篇]
[ASP.NET Core 3框架揭秘] 配置[8]:多樣化的配置源[下篇]
[ASP.NET Core 3框架揭秘] 配置[9]:自定義配置源