.NET Core采用的全新配置系统[2]: 配置模型设计详解
在《.NET Core采用的全新配置系统[1]: 读取配置数据》中,我们通过实例的方式演示了几种典型的配置读取方式,其主要目的在于使读者朋友们从编程的角度对.NET Core的这个全新的配置系统具有一个大体上的认识,接下来我们从设计的维度来重写认识它。通过上面演示的实例我们知道,配置的编程模型涉及到三个核心对象,它们分别是Configuration、ConfigurationSource和ConfigurationBuilder。如果从设计层面来审视这个配置系统,还缺少另一个名为ConfigurationProvider的核心对象,总得来说,.NET Core的这个配置模型由这四个核心对象组成。要彻底了解这四个核心对象之间的关系,我们先得来聊聊配置的几种数据结构。 [ 本文已经同步到《ASP.NET Core框架揭秘》之中]
目录
一、配置数据结构及其转换
二、Configuration
三、ConfigurationProvider
四、ConfigurationSource
五、ConfigurationBuilder
一、配置数据结构及其转换
相同的数据具有不同的表现和承载方式,同时体现出不同的数据结构。对于配置来说,它在被消费过程中是以Configuration对象的形式来体现的,该对象在逻辑上具有一个树形化层次结构,所以我们可以称之为配置树,并将这棵树视为配置的“逻辑结构”。
配置具有多种原始来源,可以是内存对象、物理文件、数据库或者其他自定义的存储介质,如果采用物理文件来存储配置数据,我们还可以选择不同的文件格式,常见的文件类型包括XML、JSON和INI三种,所以配置的原始数据结构是不确定的。配置模型的最终目的在于提取原始的配置数据并将其转换成一个Configuration对象,话句话说,整个配置模型的使命就在于按照下图所示的方式将配置数据从原始的结构转换成树形层次结构。
对于配置模型来说,配置从原始结构向逻辑结构的转换不是一蹴而就的,在它们之间具有一种“中间结构”。话句话说,原始的配置数据被读取出来之后会先统一转换成这种中间结构的数据,那么这种中间结构到底是一种怎样的数据结构呢?在《.NET Core采用的全新配置系统[1]: 读取配置数据》我们说过,一棵配置树通过其叶子结点承载所有的原子配置数据, 这棵树的结构和承载的数据完全可以利用一个简单的数据字典来表达。具体来说,我们只需要将所有叶子节点在配置树种的路径作为Key,将叶子结点承载的配置数据作为Value即可。所谓的“中间结构”指的就是这样的数据字典,我们不妨将其称为“物理结构”。所以配置模型会按照下图所示的方式将具有不同原始结构的配置数据统一转换成基于字典的物理结构,最终再完成针对逻辑结构的转换。
对于配置模型的四个核心对象,Configuration对配置树的体现,其他三个(ConfigurationSource、ConfigurationBuilder和ConfigurationProvider)在配置的结构转换过程中扮演着不同的角色,至于它们究竟起到怎样的作用,我们将在接下来的内容中对它们作专门的介绍。
二、Configuration
配置在应用程序中总是以一个Configuration对象的形式供我们使用,我们所说的Configuration是对所有实现了IConfiguration接口的所有类型一起对应对象的统称。一个Configuration对象具有树形层次化结构的意思并不是说对应的类型具有对应的数据成员(字段或者属性)定义,而是说它提供的API在逻辑上体现出树形化层次结构,所以我们才说配置树是一种逻辑结构。如下所示的是IConfiguration接口的完整定义,所谓的层次化逻辑结构就体现在它的成员定义上。
1: public interface IConfiguration
2: {
3: IEnumerable<IConfigurationSection> GetChildren();
4: IConfigurationSection GetSection(string key);
5: IChangeToken GetReloadToken();
6:
7: string this[string key] { get; set; }
8: }
一个Configuration对象表示配置树的某个配置节点。对于组成整棵树的所有配置节点来说,表示根节点的Configuration对象与表示其它配置节点的Configuration对象是不同的,所以配置模型采用不同的接口来表示它们。具体来说,根节点所在的Configuration对象被称为ConfigurationRoot,除此之外的其他Configuration对象则被称为ConfigurationSection,配置模型分别定义了接口IConfigurationRoot和IConfigurationSection来表示它们,这两个接口都是IConfiguration的继承者。下图为我们展示了由一个ConfigurationRoot对象和一组 ConfigurationSection对象构成的配置树。
如下所示的是接口IConfigurationRoot的定义,可见该接口仅仅唯一的方法Reload实现对配置数据的重新加载。ConfigurationRoot对象表示的配置树的根,也可以是它根本就是对整棵配置树的体现,如果如果它被重新加载了,意味着整棵配置树承载的所有配置数据均被重新加载了。
1: public interface IConfigurationRoot : IConfiguration
2: {
3: void Reload();
4: }
表示非根配置节点的IConfigurationSection接口具有如下三个属性,只读属性Key用来唯一标识多个具有相同父节点的ConfigurationSection对象,而Path则表示当前配置节点在配置树中的路径,该路径由ConfigurationSection的Key组成,并采用冒号(“:”)作为分隔符。Path和Key的组合体现了当前配置节在整个配置树中的位置。
1: public interface IConfigurationSection : IConfiguration
2: {
3: string Path { get; }
4: string Key { get; }
5: string Value { get; set; }
6: }
IConfigurationSection的Value属性表示配置节点承载的配置数据。在大部分情况下,只有配置树的叶子节点对应的ConfigurationSection对象才具有值,非叶子节点对应的ConfigurationSection对象实际上仅仅表示存放所有子配置节点的逻辑容器,它们的Value一般返回Null。值得一体的是,这个Value属性并不是只读的,而是可读可写的,但是我们写入的值一般不会被持久化,所以以来配置树被重新加载,写入的值将会丢失。
在对ConfigurationRoot和ConfigurationSection具有基本了解情况下我们回过头来看看定义在接口IConfiguration中的成员。它的GetChildren方法返回的ConfigurationSection集合表示率属于它的所有自配置节点,另一个方法GetSection则根据指定的Key得到一个具体的子配置节点。当GetSection方法执行的时候,指定的参数将会与当前ConfigurationSection的Path进行组合以确定目标配置节点所在的路径,所以如果在调用该方法的时候指定一个相对于当前配置节的路径,我们是可以得到子节点以下的某个配置节。
1: Dictionary<string, string> source = new Dictionary<string, string>
2: {
3: ["A:B:C"] = "ABC"
4: };
5: IConfiguration root = new ConfigurationBuilder()
6: .Add(new MemoryConfigurationSource { InitialData = source })
7: .Build();
8:
9: IConfigurationSection section1 = root.GetSection("A:B:C");
10: IConfigurationSection section2 = root.GetSection("A:B").GetSection("C");
11: IConfigurationSection section3 = root.GetSection("A").GetSection("B:C");
12:
13: Debug.Assert(section1.Value == "ABC");
14: Debug.Assert(section2.Value == "ABC");
15: Debug.Assert(section3.Value == "ABC");
16:
17: Debug.Assert(!ReferenceEquals(section1, section2));
18: Debug.Assert(!ReferenceEquals(section1, section3));
19: Debug.Assert(null != root.GetSection("D"));
如上面的代码片段所示,我们以不同的方式调用GetSection方法得到的都是路径为“A:B:C”的ConfigurationSection。上面这段代码还体现了另一个有趣的现象,虽然这三个ConfigurationSection对象均指向配置树的同一个节点,但是它们却并非同一个对象。换句话说,当我们调用GetSection方法的时候,不论配置树种是否存在一个与指定路径匹配的配置节,它总是会创建一个ConfigurationSection对象。
IConfiguration还具有一个索引,我们可以指定子配置节的Key或者相对当前配置节点的路径得到对应ConfigurationSection的值。当这个索引执行的时候,它会按照与GetSection方法完全一致的逻辑得到一个ConfigurationSection对象,并返回其Value属性。如果配置树中不具有匹配的配置节,该索引会返回Null而不会抛出异常。
三、ConfigurationProvider
在第一节介绍ConfigurationSource对象时,我们说它对原始配置源的体现。虽然每种不同类型的配置源都具有一个对应的ConfigurationSource类型,但是针对原始数据的读取并不由ConfigurationSource来提供,而是委托一个对应的ConfigurationProvider对象来完成。在上面介绍的配置结构转换过程中,针对不同配置源类型的ConfigurationProvider按照如下图所示的方式实现配置从原始结构向物理结构的转换。
ConfigurationProvider是对所有实现了IConfigurationProvider接口的所有类型以及对应对象的统称。由于ConfigurationProvider的目的在于将配置从原始结构转换成物理结构,配置数据的物理结构体现为一个简单的二维数据字典,所以我们会发现定义在IConfigurationProvider接口中的方法大都体现为针对字典对象的相关操作。
1: public interface IConfigurationProvider
2: {
3: void Load();
4:
5: bool TryGet(string key, out string value);
6: void Set(string key, string value);
7: IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath)
8: }
配置数据的加载通过调用ConfigurationProvider的Load方法来完成。我们可以调用TryGet方法获取由指定的Key所标识的配置项的值。从数据持久化的角度来讲,ConfigurationProvider基本上都是只读的,也就是说ConfigurationProvider只负责从持久化资源中读取配置数据,而不负责更新保存在持久化资源的配置数据,所以它提供的Set方法设置的配置数据一般只会保存在内存中。ConfigurationProvider的GetChildKeys方法用于获取某个指定配置节点的所有子节点的Key。
每种类型的配置源都具有对应的ConfigurationProvider类型,这些类型一般不会直接实现接口IConfigurationProvider,而会选择继承另一个名为ConfigurationProvider的抽象类。这个抽象类的定义其实很简单,从如下的代码片段可以看出它仅仅是对一个IDictionary<string, string>对象(Key不区分大小写)的封装,其Set和TryGetValue方法最终操作的都是这个字典对象。它实现了Load方法并将其定义成虚方法,具体的ConfigurationProvider可以通过重写这个方法从相应的数据源中读取配置数据并对这个字典对象进行初始化。
1: public abstract class ConfigurationProvider : IConfigurationProvider
2: {
3: protected IDictionary<string, string> Data { get; set; }
4:
5: public IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath)
6: {
7: //省略实现
8: }
9:
10: public virtual void Load()
11: {}
12:
13: public void Set(string key, string value)
14: {
15: this.Data[key] = value;
16: }
17:
18: public bool TryGet(string key, out string value)
19: {
20: return this.Data.TryGetValue(key, out value);
21: }
22: //其他成员
23: }
四、ConfigurationSource
ConfiurationSource在配置模型中代表配置源,它通过注册到ConfigurationBuilder上为后者创建的Configuration提供原始的配置数据。由于针对原始配置数据的读取实现在相应的ConfigurationProvider之中,所以ConfigurationSource所起的作用在于提供相应的ConfigurationProvider。ConfigurationSource是对所有实现了IConfigurationSource接口的所有类型及其对象的统称,如下面的代码片段所示,该接口具有一个唯一的Build方法根据指定的ConfigurationBuilder对象提供对应的ConfigurationProvider。
1: public interface IConfigurationSource
2: {
3: IConfigurationProvider Build(IConfigurationBuilder builder);
4: }
五、ConfigurationBuilder
ConfigurationBulder在整个配置模型中处于一个核心地位,它是Configuration的创建者,代表原始配置源的ConfigurationSource也注册到它上面。ConfigurationBulder是对所有实现了IConfigurationBulder接口的所有类型及其对应对象的统称。如下面的代码片段所示,IConfigurationBulder接口定义了两个方法,其中Add方法用于注册ConfigurationSource,最终的Configuration则通过Build方法创建,后者返回一个代表整棵配置的数的ConfigurationRoot对象。注册的ConfigurationSource被保存在通过Sources属性表示的集合中,而另一个属性Properties则以字典的形式存放任意的自定义属性。
1: public interface IConfigurationBuilder
2: {
3: IEnumerable<IConfigurationSource> Sources { get; }
4: Dictionary<string, object> Properties { get; }
5:
6: IConfigurationBuilder Add(IConfigurationSource source);
7: IConfigurationRoot Build();
8: }
配置系统提供了一个名为ConfigurationBulder[1]的类作为IConfigurationBulder接口的默认实现者。定义在它上面的Build方法体现了配置系统读取原始配置数据并生成配置树的默认机制,这是我们接下来重点讲述的内容。ConfigurationBulder类的Build方法返回一个类型为ConfigurationRoot的对象,对于一个通过该对象表示配置树来说,每个非根配置节点均是一个类型为ConfigurationSection的对象,这两个类型(ConfigurationRoot和ConfigurationSection)自然是IConfigurationRoot和IConfigurationSection接口的实现者。
ConfigurationRoot代表着一颗完整的配置树,但是不论是这个对象本身,还是表示这棵树非根配置节点的ConfigurationSection对象,它们自身都没有维护任何的数据。这句话好似显得自相矛盾,但实则不然,因为所谓的配置树仅仅是API在逻辑上所体现的数据结构,并不是具体的配置数据也是按照这样的结构进行存储的。由于这两个对象均不作任何的数据封装,针对它们的数据提取请求最终都会交给一组ConfigurationProvider来完成,后者自然就是注册到ConfigurationBuilder上的这组ConfigurationSource所提供的ConfigurationProvider。
本节内容从设计和实现原理的角度对配置模型进行了详细的介绍。总的来说,配置模型涉及到四个核心对象,包括承载配置逻辑结构的Configuration对象和它的创建者ConfigurationBuilder,以及与配置源相关的ConfigurationSource和ConfigurationProvider。这四个核心对象之间的关系简单而清晰,完全可以通过一句话来概括:ConfigurationBuilder利用注册的ConfigurationSource来提供的ConfigurationProvider读取原始配置数据并创建出相应的Configuration对象。下图所示的UML展示了配置模型涉及的主要接口/类型以及它们之间的关系。
[1] 本小节提到的ConfigurationBuilder大部分情况下指代的是ConfigurationBuilder这个类型或者该类型的对象,而不是泛指所有实现了IConfigurationBulder接口的类型及其对应对象,。后面提到的ConfigurationRoot和ConfigurationSection也是这样,请读者朋友注意区分。
.NET Core采用的全新配置系统[2]: 配置模型设计详解的更多相关文章
- .NET Core采用的全新配置系统[10]: 配置的同步机制是如何实现的?
配置的同步涉及到两个方面:第一,对原始的配置文件实施监控并在其发生变化之后从新加载配置:第二,配置重新加载之后及时通知应用程序进而使后者能够使用最新的配置.要了解配置同步机制的实现原理,先得从认识一个 ...
- .NET Core采用的全新配置系统[1]: 读取配置数据
提到“配置”二字,我想绝大部分.NET开发人员脑海中会立马浮现出两个特殊文件的身影,那就是我们再熟悉不过的app.config和web.config,多年以来我们已经习惯了将结构化的配置定义在这两个文 ...
- .NET Core采用的全新配置系统[3]: “Options模式”下的配置是如何绑定为Options对象
配置的原子结构就是单纯的键值对,并且键和值都是字符串,但是在真正的项目开发中我们一般不会单纯地以键值对的形式来使用配置.值得推荐的做法就是采用<.NET Core采用的全新配置系统[1]: 读取 ...
- .NET Core采用的全新配置系统[9]: 为什么针对XML的支持不够好?如何改进?
物理文件是我们最常用到的原始配置的载体,最佳的配置文件格式主要由三种,它们分别是JSON.XML和INI,对应的配置源类型分别是JsonConfigurationSource.XmlConfigura ...
- [r]Ubuntu Linux系统下apt-get命令详解
Ubuntu Linux系统下apt-get命令详解(via|via) 常用的APT命令参数: apt-cache search package 搜索包 apt-cache show package ...
- GPIO 配置之ODR, BSRR, BRR 详解
STM32 GPIO 配置之ODR, BSRR, BRR 详解 用stm32 的配置GPIO 来控制LED 显示状态,可用ODR,BSRR,BRR 直接来控制引脚输出状态. ODR寄存器可读可写:既能 ...
- STM32 GPIO 配置之ODR, BSRR, BRR 详解
STM32 GPIO 配置之ODR, BSRR, BRR 详解 用stm32 的配置GPIO 来控制LED 显示状态,可用ODR,BSRR,BRR 直接来控制引脚输出状态. ODR寄存器可读可写:既能 ...
- L012-linux系统文件属性知识深入详解小结
L012-linux系统文件属性知识深入详解小结 最近的学习重点不在这上面,所以更新的比较慢,再加上母亲住院,感情问题,一系列吧,愿快点度过这黑色的4月份,希望我能在5月份阳光起来,加油! 回归正题 ...
- L011系统文件属性知识进阶详解小节
L011系统文件属性知识进阶详解小节 这节课的内容相对来说较少,一上午加中午就听完了,现在总结一下,最后会有一个相关的面试题. 首先先附上一张图: 今天学习主要跟①和②有关,①为Inode 号 ②为文 ...
随机推荐
- “不给力啊,老湿!”:RSA加密与破解
作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 加密和解密是自古就有技术了.经常看到侦探电影的桥段,勇敢又机智的主角,拿着一长串毫 ...
- rnandroid环境搭建
react-native 环境搭建具体步骤这个大家已经玩烂了,这个主要是记录下来自己做win7系统遇到的坑 1.com.android.ddmlib.installexception 遇到这个问题,在 ...
- angular实现统一的消息服务
后台API返回的消息怎么显示更优雅,怎么处理才更简洁?看看这个效果怎么样? 自定义指令和服务实现 自定义指令和服务实现消息自动显示在页面的顶部,3秒之后消失 1. 显示消息 这种显示消息的方式是不是有 ...
- Emoji选项列表
一.需要的前提文件 从网上下载Emoji的表情包,当然是png的图片,因为WPF不支持彩色的Emoji,所以,做列表的时候,需要用图片. 随着压缩包一起的还有一个Emoji.xml文件,文件的层级结构 ...
- 11、Struts2 的文件上传和下载
文件上传 表单准备 要想使用 HTML 表单上传一个或多个文件 须把 HTML 表单的 enctype 属性设置为 multipart/form-data 须把 HTML 表单的method 属性设置 ...
- 用游标实现查询当前服务器所有数据库所有表的SQL
declare @name varchar(100) DECLARE My_Cursor CURSOR --定义游标 FOR (SELECT Name FROM Master..SysDatabase ...
- 我这么玩Web Api(一):帮助页面或用户手册(Microsoft and Swashbuckle Help Page)
前言 你需要为客户编写Api调用手册?你需要测试你的Api接口?你需要和前端进行接口对接?那么这篇文章应该可以帮到你.本文将介绍创建Web Api 帮助文档页面的两种方式,Microsoft Help ...
- [原] KVM 虚拟化原理探究(6)— 块设备IO虚拟化
KVM 虚拟化原理探究(6)- 块设备IO虚拟化 标签(空格分隔): KVM [toc] 块设备IO虚拟化简介 上一篇文章讲到了网络IO虚拟化,作为另外一个重要的虚拟化资源,块设备IO的虚拟化也是同样 ...
- FullCalendar应用——整合农历节气和节日
FullCalendar用来做日程管理功能非常强大,但是唯一不足的地方是没有将中国农历历法加进去,今天我将结合实例和大家分享如何将中国农历中的节气和节日整合到FullCalendar中,从而增强其实用 ...
- WPF CheckBox 样式
<Style x:Key="FocusVisual"> <Setter Property="Control.Template"> < ...