在《读取配置数据》([上篇],[下篇])上面一节中,我们通过实例的方式演示了几种典型的配置读取方式,接下来我们从设计的维度来重写认识配置模型。配置的编程模型涉及到三个核心对象,分别通过三个对应的接口(IConfiguration、IConfigurationSource和IConfigurationBuilder)来表示。如果从设计层面来审视背后的配置模型,还缺少另一个名通过IConfigurationProvider接口表示的核心对象。总的来说,配置模型由这四个核心对象组成,但是要彻底了解这四个核心对象之间的关系,我们先得来聊聊配置的几种数据结构。

一、配置数据结构及其转换

相同的数据具有不同的表现形式和承载方式,同时体现出不同的数据结构。对于配置来说,它在被应用程序消费过程中是以IConfiguration对象的形式来体现的,该对象在逻辑上具有一个树形化层次结构,所以将它称之为配置树,并将这棵树视为配置的“逻辑结构”。配置具有多种原始来源,可以是内存对象、物理文件、数据库或者其他自定义的存储介质。如果采用物理文件来存储配置数据,我们还可以选择不同的文件格式,常见的文件类型包括XML、JSON和INI三种,所以配置的原始数据结构是多种多样的。配置模型的最终目的在于提取原始的配置数据并将其转换成一个IConfiguration对象。话句话说,配置模型的使命就在于按照下图所示的方式将配置数据从原始的结构转换成树形层次结构。

配置从原始结构向逻辑结构的转换不是一蹴而就的,在它们之间具有一种“中间结构”。原始的配置数据被读取出来之后会先统一转换成这种中间结构的数据,那么这种中间结构到底是一种怎样的数据结构呢?一棵配置树通过其叶子结点承载所有的原子配置数据, 这棵树的结构和承载的数据完全可以利用一个简单的数据字典来表达。具体来说,我们只需要将所有叶子节点在配置树中的路径作为Key,将叶子结点承载的配置数据作为Value即可。所谓的“中间结构”指的就是这样的数据字典,我们不妨将其称为“配置字典”。所以配置模型会按照图6-9所示的方式将具有不同原始结构的配置数据统一转换成基于字典的配置字典,最终再完成针对逻辑结构的转换。

对于配置模型的四个核心对象来说,IConfiguration对象是对配置树的体现,其他三个核心对象(IConfigurationSource、IConfigurationBuilder和IConfigurationProvider)在配置的结构转换过程中扮演着不同的角色,至于它们究竟起到怎样的作用,我们将在接下来的内容中对它们作专门的介绍。

二、IConfiguration

配置在应用程序中总是以一个IConfiguration对象的形式供我们使用。一个IConfiguration对象具有树形层次化结构的意思并不是说对应的类型具有对应的数据成员定义,而是说它提供的API在逻辑上体现出树形化层次结构,所以我们才说配置树是一种逻辑结构。如下所示的是IConfiguration接口的完整定义,所谓的层次化逻辑结构就体现在它的成员定义上。

public interface IConfiguration
{
IEnumerable<IConfigurationSection> GetChildren();
IConfigurationSection GetSection(string key);
IChangeToken GetReloadToken(); string this[string key] { get; set; }
}

一个IConfiguration对象表示配置树的某个配置节点。对于组成整棵树的所有配置节点来说,表示根节点的IConfiguration对象与表示其它配置节点的IConfiguration对象是不同的,所以配置模型采用不同的接口来表示它们。根节点所在的IConfiguration对象体现为一个IConfigurationRoot对象,除此之外的其他节点对象则被通过一个IConfigurationSection对象表示,IConfigurationRoot和IConfigurationSection接口都是IConfiguration的继承者。下图为我们展示了由一个IConfigurationRoot对象和一组 IConfigurationSection对象构成的配置树。

如下所示的是接口IConfigurationRoot的定义,它具有的唯一方法Reload实现对配置数据的重新加载。IConfigurationRoot对象表示的配置树的根,所以也代表了整棵配置树,如果它被重新加载了,意味着整棵配置树承载的所有配置数据均被重新加载了。

public interface IConfigurationRoot : IConfiguration
{
void Reload();
}

表示非根配置节点的IConfigurationSection接口具有如下三个属性,只读属性Key用来唯一标识多个具有相同父节点的ConfigurationSection对象,而Path则表示当前配置节点在配置树中的路径,它后组成当前路径的所有IConfigurationSection对象的Key组成,并采用冒号(“:”)作为分隔符。Path和Key的组合体现了当前配置节在整个配置树中的位置。

public interface IConfigurationSection : IConfiguration
{
string Path { get; }
string Key { get; }
string Value { get; set; }
}

IConfigurationSection的Value属性表示配置节点承载的配置数据。在大部分情况下,只有配置树的叶子结点对应的IConfigurationSection对象才具有值,非叶子节点对应的IConfigurationSection对象实际上仅仅表示存放所有子配置节点的逻辑容器,它们的Value一般返回Null。值得一体的是,这个Value属性并不是只读的,而是可读可写的,但我们写入的值一般不会被持久化,一旦配置树被重新加载,该值将会丢失。

在对IConfigurationRoot和IConfigurationSection具有基本了解情况下我们回过头来看看定义在接口IConfiguration中的成员。它的GetChildren方法返回的IConfigurationSection集合表示它的所有子配置节,另一个方法GetSection则根据指定的Key得到一个具体的子配置节。当GetSection方法执行的时候,指定的参数将会与当前IConfigurationSection的Path进行组合以确定目标配置节点所在的路径,所以如果在调用该方法的时候指定一个相对于当前配置节的路径,我们是可以得到子节点以下的某个配置节。

var source = new Dictionary<string, string>
{
["A:B:C"] = "ABC"
}; var root = new ConfigurationBuilder()
.AddInMemoryCollection(source)
.Build(); var section1 = root.GetSection("A:B:C"); //A:B:C
var section2 = root.GetSection("A:B").GetSection("C"); //A:C->C
var section3 = root.GetSection("A").GetSection("B:C"); //A->B:C Debug.Assert(section1.Value == "ABC");
Debug.Assert(section2.Value == "ABC");
Debug.Assert(section3.Value == "ABC"); Debug.Assert(!ReferenceEquals(section1, section2));
Debug.Assert(!ReferenceEquals(section1, section3));
Debug.Assert(null != root.GetSection("D"));

如上面的代码片段所示,我们以不同的方式调用GetSection方法得到的都是路径为“A:B:C”的IConfigurationSection对象。上面这段代码还体现了另一个有趣的现象,虽然这三个IConfigurationSection对象均指向配置树的同一个节点,但是它们却并非同一个对象。换句话说,当我们调用GetSection方法的时候,不论配置树中是否存在一个与指定路径匹配的配置节,它总是会创建新的IConfigurationSection对象。

IConfiguration还具有一个索引,我们可以指定子配置节的Key或者相对当前配置节点的路径得到对应IConfigurationSection的值。当这个索引执行的时候,它会按照与GetSection方法完全一致的逻辑得到一个IConfigurationSection对象,并返回其Value属性。如果配置树中不具有匹配的配置节,该索引会返回Null而不会抛出异常。

三、IConfigurationProvider

在《读取配置数据[上篇]》介绍IConfigurationSource对象时,我们说它对原始配置源的体现。虽然每种不同类型的配置源都具有一个对应的IConfigurationSource实现,但是针对原始数据的读取并不由它来提供,而是委托一个与之对应的IConfigurationProvider对象来完成。在上面介绍的配置结构转换过程中,针对不同配置源类型的IConfigurationProvider按照如下图所示的方式实现配置从原始结构向物理结构的转换。

由于IConfigurationProvider对象的目的在于将配置从原始结构转换成配置字典,所以我们会发现定义在IConfigurationProvider接口中的方法大都体现为针对字典对象的相关操作。配置数据的加载通过调用IConfigurationProvider的Load方法来完成。我们可以调用TryGet方法获取由指定的Key所标识的配置项的值。从数据持久化的角度来讲,IConfigurationProvider基本上都是只读的,也就是说它只负责从持久化资源中读取配置数据,而不负责持久化更新后的配置数据,所以它提供的Set方法设置的配置数据一般只会保存在内存中,不过通过实现该方法时对提供的值进行持久化也未尝不可。

public interface IConfigurationProvider
{
void Load();
void Set(string key, string value);
bool TryGet(string key, out string value); IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath);
IChangeToken GetReloadToken();
}

IConfigurationProvider的GetChildKeys方法用于获取某个指定配置节点(对应于parentPath参数)的所有子节点的Key。当IConfiguration的GetChildren方法被调用时,注册的所有IConfigurationSource对应的IConfigurationProvider的GetChildKeys方法会被调用。这个方法的第一个参数earlierKeys代表的Key来源于其他IConfigurationProvider,当解析出当前IConfigurationProvider提供的Key后,该方法需要对它们合并到earlierKeys集合中,合并后结果将作为方法的返回值。值得一提的是,返回的Key的集合是经过排序的。

每种类型的配置源都具有对应的IConfigurationProvider实现,它们一般不会直接实现接口IConfigurationProvider,而会选择继承另一个名为ConfigurationProvider的抽象类。这个抽象类的定义其实很简单,从如下的代码片段可以看出它仅仅是对一个IDictionary<string, string>对象(Key不区分大小写)的封装,其Set和TryGetValue方法最终操作的都是这个字典对象。

public abstract class ConfigurationProvider : IConfigurationProvider
{
protected IDictionary<string, string> Data { get; set; }
protected ConfigurationProvider()=> Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath)
{
var prefix = parentPath == null ? string.Empty : $"{parentPath}:" ;
return Data
.Where(it => it.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
.Select(it => Segment(it.Key, prefix.Length))
.Concat(earlierKeys)
.OrderBy(it => it);
}
public virtual void Load() {}
public void Set(string key, string value) => Data[key] = value;
public bool TryGet(string key, out string value) => Data.TryGetValue(key, out value); private static string Segment(string key, int prefixLength)
{
var indexOf = key.IndexOf(":", prefixLength, StringComparison.OrdinalIgnoreCase);
return indexOf < 0
? key.Substring(prefixLength)
: key.Substring(prefixLength, indexOf - prefixLength);
}
...
}

抽象类ConfigurationProvider实现了Load方法并将其定义成虚方法,这个方法并没有提供具体的实现,所以它的派生类可以通过重写这个方法从相应的数据源中读取配置数据,并对通过Data属性的设置完成对配置字典的初始化。

四、IConfigurationSource

IConfiurationSource在配置模型中代表配置源,它被注册到IConfigurationBuilder上为后者创建的IConfiguration提供原始的配置数据。由于针对原始配置数据的读取实现在相应的IConfigurationProvider中,所以IConfigurationSource所起的作用在于提供相应的IConfigurationProvider。如下面的代码片段所示,IConfigurationSource接口具有一个唯一的Build方法根据指定的IConfigurationBuilder对象提供对应的IConfigurationProvider。

public interface IConfigurationSource
{
IConfigurationProvider Build(IConfigurationBuilder builder);
}

五、IConfigurationBuilder

IConfigurationBulder在整个配置模型中处于一个核心地位,代表原始配置源的IConfigurationSource也注册到它上面,它的作用就在于利用后者提供的原始数据创建出供应用程序使用的IConfiguration对象。如下面的代码片段所示,IConfigurationBulder接口定义了两个方法,其中Add方法用于注册IConfigurationSource对象,最终的IConfiguration对象则通过Build方法创建,后者返回一个代表整棵配置树的IConfigurationRoot对象。注册的IConfigurationSource被保存在通过Sources属性表示的集合中,而另一个属性Properties则以字典的形式存放任意的自定义属性。

public interface IConfigurationBuilder
{
IEnumerable<IConfigurationSource> Sources { get; }
Dictionary<string, object> Properties { get; } IConfigurationBuilder Add(IConfigurationSource source);
IConfigurationRoot Build();
}

配置系统提供了一个名为ConfigurationBulder的类作为IConfigurationBulder接口的默认实现。定义在它上面的Build方法体现了配置系统读取原始配置数据并生成配置树的默认机制。ConfigurationBulder类的Build方法返回一个类型为ConfigurationRoot的对象,对于通过该对象表示配置树来说,每个非根配置节点均是一个类型为ConfigurationSection的对象。

本篇文章从设计和实现原理的角度对配置模型进行了详细的介绍。总的来说,配置模型涉及到四个核心对象,包括承载配置逻辑结构的IConfiguration对象和它的创建者IConfigurationBuilder,以及与配置源相关的IConfigurationSource和IConfigurationProvider。这四个核心对象之间的关系简单而清晰,完全可以通过一句话来概括:IConfigurationBuilder利用注册在它上面的所有IConfigurationSource提供的IConfigurationProvider读取原始配置数据并创建出相应的IConfiguration对象。下图所示的UML展示了配置模型涉及的主要接口/类型以及它们之间的关系。

[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框架揭秘] 配置[3]:配置模型总体设计的更多相关文章

  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框架揭秘] 配置[7]:多样化的配置源[中篇]

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

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

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

  8. [ASP.NET Core 3框架揭秘] 配置[4]:将配置绑定为对象

    虽然应用程序可以直接利用通过IConfigurationBuilder对象创建的IConfiguration对象来提取配置数据,但是我们更倾向于将其转换成一个POCO对象,以面向对象的方式来使用配置, ...

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

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

随机推荐

  1. python3 之 匿名函数

    一.语法: lambda 参数:方法(或三元运算) #最多支持3元运算 二.实例1:基础 #函数1: a = lambda x:x*x print(a(2)) #函数2: def myfun(x): ...

  2. 教你用Java web实现多条件过滤功能

    生活中,当你闲暇之余浏览资讯的时候,当你搜索资料但繁杂信息夹杂时候,你就会想,如何更为准确的定位需求信息.今天就为你带来: 分页查询 需求分析:在列表页面中,显示指定条数的数据,通过翻页按钮完成首页/ ...

  3. DNS资源记录的七类

    在Microsoft产品系列中,ADDS是一个很出色的设计平台,说到AD,那么我们就不得不提起他的合作伙伴--DNS,相信大家都知道,DNS在AD中的重要地位,就如男人和女人一样,要想有所作为,他们2 ...

  4. Java 大黑话讲解设计模式 -- UML类图

    目录 1.啥是UML类图? 2.UML类图有啥用? 3.正式理解UML类图 4.使用idea画第一个UML类图 5.类之间的关系图[必须牢记] 6.类之间的关系 6.1.依赖 6.2.泛化 6.3.实 ...

  5. 软件测试必须掌握的抓包工具Wireshark,你会了么?

    作为软件测试工程师,大家在工作中肯定经常会用到各种抓包工具来辅助测试,比如浏览器自带的抓包工具-F12,方便又快捷:比如时下特别流行的Fiddler工具,使用各种web和APP测试的各种场景的抓包分析 ...

  6. springboot2中使用dubbo的三重境界

    在springboot中使用dubbo,本来是件挺简单的事情,但现实的世界就是如此的复杂,今天我用一个亲身经历的跳坑和填坑的事来讲在spring boot中使用高版本dubbo(当当的魔改版)的三重境 ...

  7. django框架简介及自定义简易版框架

    web应用与web框架本质 概念 什么是web应用程序呢? Web应用程序就一种可以通过互联网来访问资源的应用程序, 用户可以只需要用一个浏览器而不需要安装其他程序就可以访问自己需要的资源. 应用软件 ...

  8. sina中的附件图片处理

    这样写就会频繁的创建和销毁对象 因为setPhotos这个方法调用频繁 如果在里面直接用for循环创建9个UIImageView如果因为cell重用 比如在上一个cell中本来就有UIImageVie ...

  9. luogu P3572 [POI2014]PTA-Little Bird |单调队列

    从1开始,跳到比当前矮的不消耗体力,否则消耗一点体力,每次询问有一个步伐限制,求每次最少耗费多少体力 #include<cstdio> #include<cstring> #i ...

  10. Python中 * 与 **, *args 与 **kwargs的用法

    * 用于传递位置参数(positional argument) ** 用于传递关键字参数(keyword argument) 首先,先通过一个简单的例子来介绍 * 的用法: def add_funct ...