出于编程上的便利,我们通常不会直接利用ConfigurationBuilder创建的Configuration对象读取某个单一配置项的值,而是倾向于将一组相关的配置绑定为一个对象,我们将后者称为Options对象。我们在本章第一节通过简单的实例演示了如何利用Options模型实现了配置数据向Options对象的绑定,现在我们对Options模型背后的实现原理进行详细介绍。
  一、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: }
  5如上面的代码片段所示,我们定义了一个表示个人基本信息的Profile类,定义其中的三个属性(Gender、Age和ContactInfo)分别表示性别、年龄和联系方式。表示联系信息的ContactInfo对象具有两个属性(Email和PhoneNo)分别表示电子邮箱地址和电话号码。一个完整的Profile对象可以通过如右图所示的树来体现。