6.3 配置绑定
虽然应用程序可以直接通过 IConfigurationBuilder 对象创建的 IConfiguration 对象来提取配置数据,但是我们更倾向于将其转换成一个 POCO 对象,以面向对象的方式来使用配置,我们将这个转换过程称为配置绑定。配置绑定可以通过如下几个针对 IConfiguration的扩展方法来实现,这些扩展方法都定义在NuGet包“Microsoft.Extensions.Configuration.Binder”中。
Bind方法将指定的 IConfiguration对象(对应参数 configuration)绑定一个预先创建的对象(对应参数instance),如果参数绑定的只是当前IConfiguration对象的某个子配置节,就需要通过参数 sectionKey 指定对应子配置节的相对路径。Get 方法和 Get<T>方法则直接将指定的IConfiguration对象转换成指定类型的POCO对象。
旨在生成 POCO对象的配置绑定实现在 IConfiguration接口的 Bind扩展方法上。配置绑定的目标类型可以是一个简单的基元类型,也可以是一个自定义数据类型,还可以是一个数组、集合或者字典类型。通过前面的介绍可知,IConfigurationProvider对象将原始配置数据读取出来之后会将其转换成 Key 和 Value 均为字符串的数据字典,那么针对这些完全不同的目标类型,原始配置数据是如何通过数据字典的形式来体现的?
6.3.1 绑定配置项的值
配置模型采用字符串键值对的形式来承载基础配置数据,我们将这组键值对称为配置字典,扁平的字典因为采用路径化的 Key使配置项在逻辑上具有了层次结构。IConfigurationBuilder对象将配置的层次化结构体现在由它创建的 IConfigurationRoot 对象上,我们将IConfigurationRoot 对象看作一棵配置树。所谓的配置绑定体现为如何将映射在配置树上某个节点的 IConfiguration对象(可以是 IConfigurationRoot对象或者 IConfigurationSection对象)转换成一个对应的POCO对象。
对于针对 IConfiguration 对象的配置绑定来说,最简单的是针对叶子节点的IConfigurationSection对象的绑定。表示配置树叶子节点的IConfigurationSection对象承载着原子配置项的值,而且这个值是一个字符串,所以针对它的配置绑定最终体现为如何将这个字符串转换成指定的目标类型,这样的操作体现在IConfiguration如下两个GetValue扩展方法上。
对于给出的这 4个重载,其中两个方法定义了一个表示默认值的参数 defaultValue,如果对应配置节的值为 Null 或者空字符串,那么指定的默认值将作为方法的返回值。对于其他的方法重载,它们实际上是将 Null或者 Default(T)作为隐式默认值。执行上述这些 GetValue方法时,它们会将配置节名称(对应参数 sectionKey)作为参数调用指定 IConfiguration 对象的GetSection 方法得到表示对应配置节的 IConfigurationSection 对象,它的 Value 属性被提取出来并按照如下逻辑转换成目标类型。
● 如果目标类型为object,那么直接返回原始值(字符串或者Null)。
● 如果目标类型不是 Nullable<T>,那么针对目标类型的 TypeConverter 将被用来做类型转换。
● 如果目标类型为 Nullable<T>,那么在原始值不是 Null 或者空字符串的情况下会将基础类型T作为新的目标类型进行转换,否则直接返回Null。
为了验证上述这些类型转化规则,我们编写了如下测试程序。如下面的代码片段所示,我们利用注册的MemoryConfigurationSource添加了3个配置项,对应的值分别为Null、空字符串和“123”,然后调用 GetValue 方法分别对它们进行类型转换,转换的目标类型分别是 Object、Int32和Nullable<Int32>,上述类型转换规则体现在对应的调试断言中。(S607)
按照前面介绍的类型转换规则,如果目标类型支持源自字符串的类型转换,就能够将配置项的原始值绑定为该类型的对象,而让某个类型支持某种类型转换规则的途径就是为之注册相应的 TypeConverter。下面的代码片段定义了一个表示二维坐标的 Point 对象,并且为它注册了一个类型为 PointTypeConverter的 TypeConverter,PointTypeConverter通过实现的 ConvertFrom方法将坐标的字符串表达式(如“123”和“456”)转换成一个Point对象。
由于定义的Point类型支持源自字符串的类型转换,所以如果配置项的原始值(字符串)具有与之兼容的格式,就可以按照如下方式将其绑定为一个Point对象。(S608)
6.3.2 绑定复合数据类型
这里所谓的复合类型就是一个具有属性数据成员的自定义类型。如果用一棵树表示一个复合对象,那么叶子节点承载所有的数据,并且叶子节点的数据类型均为基元类型。如果用数据字典来提供一个复杂对象所有的原始数据,那么这个字典中只需要包含叶子节点对应的值即可。只要将叶子节点所在的路径作为字典元素的Key,就可以通过一个字典对象体现复合对象的结构。
上面的代码片段定义了一个表示个人基本信息的 Profile 类,它的 3 个属性(Gender、Age和 ContactInfo)分别表示性别、年龄和联系方式。由于配置绑定会调用默认无参构造函数来创建绑定的目标对象,所以需要为 Profile类定义一个默认构造函数。表示联系方式的 ContactInfo类型具有两个属性(EmailAddress和 PhoneNo),它们分别表示电子邮箱地址和电话号码。一个完整的Profile对象可以通过图6-13所示的树来体现。
图6-13 复杂对象的配置树
如果需要通过配置的形式表示一个完整的 Profile对象,只需要将 4个叶子节点(性别、年龄、电子邮箱地址和电话号码)对应的数据由配置来提供即可。对于承载配置数据的数据字典,我们需要按照表6-2列举的方式将这4个叶子节点的路径作为字典元素的Key。
表6-2 针对复杂对象的配置数据结构
可以通过下面的程序来验证针对复合数据类型的配置绑定。先创建一个 Configuration Builder对象,然后为它添加了一个 MemoryConfigurationSource对象,并按照表 6-2列举的结构提供原始配置数据。在调用Build方法构建出IConfiguration对象之后,可以直接调用Get<T>扩展方法将它转换成一个Profile对象。(S609)
6.3.3 绑定集合对象
如果配置绑定的目标类型是一个集合(包括数组),那么当前 IConfiguration 对象的每个子配置节将绑定为集合的元素。如果将一个IConfiguration对象绑定为一个元素类型为Profile的集合,那么它表示的配置树应该具有图6-14所示的结构。
图6-14 集合对象的配置树
既然能够正确地将集合对象通过一个合法的配置树体现出来,那么就可以将它转换成配置字典。而图6-14表示的这个包含3个元素的Profile集合,就可以采用表6-3列举的结构来定义对应的配置字典。
表6-3 针对集合的配置数据结构
下面通过一个简单的实例来演示针对集合的配置绑定。如下面的代码片段所示,我们创建了一个 ConfigurationBuilder对象,并为它注册了一个 MemoryConfigurationSource对象,它按照表 6-3 列举的结构提供了原始的配置数据。在得到这个 ConfigurationBuilder 对象创建的IConfiguration 对象之后,我们两次调用其 Get<T>方法将它分别绑定为一个 IEnumerable<Profile>对象和一个 Profile[]数组。由于 IConfigurationProvider 通过 GetChildKeys 方法提供的Key 是经过排序的,所以在绑定生成的集合或者数组中的元素的顺序与配置源是不相同的,如下的调试断言也体现了这一点。(S610)
在针对集合类型的配置绑定过程中,如果某个配置节绑定失败,该配置节将被忽略并选择下一个配置节继续进行绑定。但是如果目标类型为数组,最终绑定生成的数组长度与子配置节的个数总是一致的,绑定失败的元素将被设置为 Null。例如,保存原始配置的字典对象包含两个元素,如果将上面的程序进行改写,第一个元素的性别从“Male”改为“男”,那么这个值是不可能转换成Gender枚举对象的,所以针对这个Profile的配置绑定会失败。如果将目标类型设置为IEnumerable<Profile>,那么最终生成的集合只有两个元素,倘若目标类型切换成Profile数组,数组的长度依然为3,第一个元素就是Null。(S611)
6.3.4 绑定字典
能够通过配置绑定生成的字典是一个实现了 IDictionary<string,T>的类型,也就是说,配置模型对字典的 Value类型没有任何要求,但字典对象的 Key必须是一个字符串(或者枚举)。如果采用配置树的形式表示这样一个字典对象,就会发现它与针对集合的配置树在结构上几乎是一样的,唯一的区别是集合元素的索引直接变成字典元素的Key。
也就是说,图 6-14 所示的配置树同样可以表示成一个具有 3 个元素的 Dictionary<string,Profile>对象,它们对应的Key分别是Foo、Bar和Baz,所以可以按照如下方式将承载相同数据的IConfiguration对象绑定为一个IDictionary<string,T>对象。(S612)