[ASP.NET Core 3框架揭秘] 配置[4]:将配置绑定为对象
虽然应用程序可以直接利用通过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]:自定义配置源