虽然应用程序可以直接利用通过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]:自定义配置源

[ASP.NET Core 3框架揭秘] 配置[4]:将配置绑定为对象的更多相关文章

  1. [ASP.NET Core 3框架揭秘] Options[1]: 配置选项的正确使用方式[上篇]

    依赖注入不仅是支撑整个ASP.NET Core框架的基石,也是开发ASP.NET Core应用采用的基本编程模式,所以依赖注入十分重要.依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式 ...

  2. [ASP.NET Core 3框架揭秘] Options[2]: 配置选项的正确使用方式[下篇]

    四.直接初始化Options对象 前面演示的几个实例具有一个共同的特征,即都采用配置系统来提供绑定Options对象的原始数据,实际上,Options框架具有一个完全独立的模型,可以称为Options ...

  3. [ASP.NET Core 3框架揭秘] 配置[1]:读取配置数据[上篇]

    提到"配置"二字,我想绝大部分.NET开发人员脑海中会立即浮现出两个特殊文件的身影,那就是我们再熟悉不过的app.config和web.config,多年以来我们已经习惯了将结构化 ...

  4. [ASP.NET Core 3框架揭秘] 配置[2]:读取配置数据[下篇]

    [接上篇]提到“配置”二字,我想绝大部分.NET开发人员脑海中会立即浮现出两个特殊文件的身影,那就是我们再熟悉不过的app.config和web.config,多年以来我们已经习惯了将结构化的配置定义 ...

  5. [ASP.NET Core 3框架揭秘] 配置[5]:配置数据与数据源的实时同步

    在<配置模型总体设计>介绍配置模型核心对象的时候,我们刻意回避了与配置同步相关的API,现在我们利用一个独立文章来专门讨论这个话题.配置的同步涉及到两个方面:第一,对原始的配置源实施监控并 ...

  6. [ASP.NET Core 3框架揭秘] 配置[3]:配置模型总体设计

    在<读取配置数据>([上篇],[下篇])上面一节中,我们通过实例的方式演示了几种典型的配置读取方式,接下来我们从设计的维度来重写认识配置模型.配置的编程模型涉及到三个核心对象,分别通过三个 ...

  7. [ASP.NET Core 3框架揭秘] 配置[7]:多样化的配置源[中篇]

    物理文件是我们最常用到的原始配置载体,而最佳的配置文件格式主要有三种,它们分别是JSON.XML和INI,对应的配置源类型分别是JsonConfigurationSource.XmlConfigura ...

  8. [ASP.NET Core 3框架揭秘] 配置[6]:多样化的配置源[上篇]

    .NET Core采用的这个全新的配置模型的一个主要的特点就是对多种不同配置源的支持.我们可以将内存变量.命令行参数.环境变量和物理文件作为原始配置数据的来源.如果采用物理文件作为配置源,我们可以选择 ...

  9. ASP.NET Core 6框架揭秘实例演示[08]:配置的基本编程模式

    .NET的配置支持多样化的数据源,我们可以采用内存的变量.环境变量.命令行参数.以及各种格式的配置文件作为配置的数据来源.在对配置系统进行系统介绍之前,我们通过几个简单的实例演示一下如何将具有不同来源 ...

随机推荐

  1. nmap中的详细命令

    nmap全部参数详解-A 综合性扫描端口:80http 443https 53dns 25smtp 22ssh 23telnet20.21ftp 110pop3 119nntp 143imap 179 ...

  2. Gemini.Workflow 双子工作流高级教程:数据库设计及各表作用说明

    整体数据库设计,可见这一篇:Gemini.Workflow 双子工作流高级教程:数据库-设计文档 这里对各数据表进行介绍: 工作流里的设计表并不多,核心只有以下8个: 下面按照流程的顺序来介绍一下表的 ...

  3. mysql中给查询出的结果集添加自增序号

    select (@i:=@i+1) i,emp.* from emp,(select @i:=0) it 按部门分组并按薪资总和从大到小排序求薪资总和第二高的部门名称和薪资总和:select c.en ...

  4. Bootstrap 元素居中设置

    一.Bootstrap水平居中 1. 文本:class ="text-center" 2. 图片居中:class = "center-block" 3.其他元素 ...

  5. Spring Boot2 系列教程(三十)Spring Boot 整合 Ehcache

    用惯了 Redis ,很多人已经忘记了还有另一个缓存方案 Ehcache ,是的,在 Redis 一统江湖的时代,Ehcache 渐渐有点没落了,不过,我们还是有必要了解下 Ehcache ,在有的场 ...

  6. FF.PyAdmin 接口服务/后台管理微框架 (Flask+LayUI)

    源码(有兴趣的朋友请Star一下) github: https://github.com/fufuok/FF.PyAdmin gitee: https://gitee.com/fufuok/FF.Py ...

  7. luogu P3111 [USACO14DEC]牛慢跑Cow Jog_Sliver |贪心+模拟

    有N (1 <= N <= 100,000)头奶牛在一个单人的超长跑道上慢跑,每头牛的起点位置都不同.由于是单人跑道,所有他们之间不能相互超越.当一头速度快的奶牛追上另外一头奶牛的时候,他 ...

  8. Spring Boot 搭建TCP Server

    本示例首选介绍Java原生API实现BIO通信,然后进阶实现NIO通信,最后利用Netty实现NIO通信及Netty主要模块组件介绍. Netty 是一个异步事件驱动的网络应用程序框架,用于快速开发可 ...

  9. [TimLinux] Python Django与WSGI的简介

    1. Web应用 web应用的最原始的访问流程: 客户端发送HTTP请求: 服务端接收到请求,生成一个HTML文档: 服务端将构造HTTP响应,包含:响应头(响应码.键值对).响应体(HTML文档) ...

  10. HDU-1274

    在纺织CAD系统开发过程中,经常会遇到纱线排列的问题.  该问题的描述是这样的:常用纱线的品种一般不会超过25种,所以分别可以用小写字母表示不同的纱线,例如:abc表示三根纱线的排列:重复可以用数字和 ...