.NET Core采用的全新配置系统[2]: 配置模型设计详解

简介:

在《.NET Core采用的全新配置系统[1]: 读取配置数据》中,我们通过实例的方式演示了几种典型的配置读取方式,其主要目的在于使读者朋友们从编程的角度对.NET Core的这个全新的配置系统具有一个大体上的认识,接下来我们从设计的维度来重写认识它。通过上面演示的实例我们知道,配置的编程模型涉及到三个核心对象,它们分别是ConfigurationConfigurationSourceConfigurationBuilder。如果从设计层面来审视这个配置系统,还缺少另一个名为ConfigurationProvider的核心对象,总得来说,.NET Core的这个配置模型由这四个核心对象组成。要彻底了解这四个核心对象之间的关系,我们先得来聊聊配置的几种数据结构。 [ 本文已经同步到《ASP.NET Core框架揭秘》之中]

目录
一、配置数据结构及其转换
二、Configuration
三、ConfigurationProvider
四、ConfigurationSource
五、ConfigurationBuilder

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

相同的数据具有不同的表现和承载方式,同时体现出不同的数据结构。对于配置来说,它在被消费过程中是以Configuration对象的形式来体现的,该对象在逻辑上具有一个树形化层次结构,所以我们可以称之为配置树,并将这棵树视为配置的“逻辑结构”。

配置具有多种原始来源,可以是内存对象、物理文件、数据库或者其他自定义的存储介质,如果采用物理文件来存储配置数据,我们还可以选择不同的文件格式,常见的文件类型包括XML、JSON和INI三种,所以配置的原始数据结构是不确定的。配置模型的最终目的在于提取原始的配置数据并将其转换成一个Configuration对象,话句话说,整个配置模型的使命就在于按照下图所示的方式将配置数据从原始的结构转换成树形层次结构。

3

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

4

对于配置模型的四个核心对象,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,配置模型分别定义了接口IConfigurationRootIConfigurationSection来表示它们,这两个接口都是IConfiguration的继承者。下图为我们展示了由一个ConfigurationRoot对象和一组 ConfigurationSection对象构成的配置树。

5

如下所示的是接口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按照如下图所示的方式实现配置从原始结构向物理结构的转换。

6

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展示了配置模型涉及的主要接口/类型以及它们之间的关系。

7


[1] 本小节提到的ConfigurationBuilder大部分情况下指代的是ConfigurationBuilder这个类型或者该类型的对象,而不是泛指所有实现了IConfigurationBulder接口的类型及其对应对象,。后面提到的ConfigurationRoot和ConfigurationSection也是这样,请读者朋友注意区分。


作者:蒋金楠
微信公众账号:大内老A
微博: www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号 蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
相关文章
|
1月前
|
存储 开发框架 JSON
ASP.NET Core OData 9 正式发布
【10月更文挑战第8天】Microsoft 在 2024 年 8 月 30 日宣布推出 ASP.NET Core OData 9,此版本与 .NET 8 的 OData 库保持一致,改进了数据编码以符合 OData 规范,并放弃了对旧版 .NET Framework 的支持,仅支持 .NET 8 及更高版本。新版本引入了更快的 JSON 编写器 `System.Text.UTF8JsonWriter`,优化了内存使用和序列化速度。
|
17天前
|
关系型数据库 C# 数据库
.NET 8.0 开源在线考试系统(支持移动端)
【10月更文挑战第27天】以下是适用于 .NET 8.0 的开源在线考试系统(支持移动端)的简介: 1. **基于 .NET Core**:跨平台,支持多种数据库,前后端分离,适用于多操作系统。 2. **结合 Blazor**:使用 C# 开发 Web 应用,支持响应式设计,优化移动端体验。 3. **基于 .NET MAUI**:跨平台移动应用开发,一套代码多平台运行,提高开发效率。 开发时需关注界面设计、安全性与稳定性。
|
2月前
|
开发框架 监控 前端开发
在 ASP.NET Core Web API 中使用操作筛选器统一处理通用操作
【9月更文挑战第27天】操作筛选器是ASP.NET Core MVC和Web API中的一种过滤器,可在操作方法执行前后运行代码,适用于日志记录、性能监控和验证等场景。通过实现`IActionFilter`接口的`OnActionExecuting`和`OnActionExecuted`方法,可以统一处理日志、验证及异常。创建并注册自定义筛选器类,能提升代码的可维护性和复用性。
|
2月前
|
开发框架 .NET 中间件
ASP.NET Core Web 开发浅谈
本文介绍ASP.NET Core,一个轻量级、开源的跨平台框架,专为构建高性能Web应用设计。通过简单步骤,你将学会创建首个Web应用。文章还深入探讨了路由配置、依赖注入及安全性配置等常见问题,并提供了实用示例代码以助于理解与避免错误,帮助开发者更好地掌握ASP.NET Core的核心概念。
89 3
|
24天前
|
Windows
.NET 隐藏/自定义windows系统光标
【10月更文挑战第20天】在.NET中,可以使用`Cursor`类来控制光标。要隐藏光标,可将光标设置为`Cursors.None`。此外,还可以通过从文件或资源加载自定义光标来更改光标的样式。例如,在表单加载时设置`this.Cursor = Cursors.None`隐藏光标,或使用`Cursor.FromFile`方法加载自定义光标文件,也可以将光标文件添加到项目资源中并通过资源管理器加载。这些方法适用于整个表单或特定控件。
|
2月前
|
JSON 安全 数据安全/隐私保护
从0到1搭建权限管理系统系列三 .net8 JWT创建Token并使用
【9月更文挑战第22天】在.NET 8中,从零开始搭建权限管理系统并使用JWT(JSON Web Tokens)创建Token是关键步骤。JWT是一种开放标准(RFC 7519),用于安全传输信息,由头部、载荷和签名三部分组成。首先需安装`Microsoft.AspNetCore.Authentication.JwtBearer`包,并在`Program.cs`中配置JWT服务。接着,创建一个静态方法`GenerateToken`生成包含用户名和角色的Token。最后,在控制器中使用`[Authorize]`属性验证和解析Token,从而实现身份验证和授权功能。
117 3
|
28天前
|
开发框架 JavaScript 前端开发
一个适用于 ASP.NET Core 的轻量级插件框架
一个适用于 ASP.NET Core 的轻量级插件框架
|
2月前
|
开发框架 NoSQL .NET
利用分布式锁在ASP.NET Core中实现防抖
【9月更文挑战第5天】在 ASP.NET Core 中,可通过分布式锁实现防抖功能,仅处理连续相同请求中的首个请求,其余请求返回 204 No Content,直至锁释放。具体步骤包括:安装分布式锁库如 `StackExchange.Redis`;创建分布式锁服务接口及其实现;构建防抖中间件;并在 `Startup.cs` 中注册相关服务和中间件。这一机制有效避免了短时间内重复操作的问题。
|
3月前
|
设计模式 存储 前端开发
揭秘.NET架构设计模式:如何构建坚不可摧的系统?掌握这些,让你的项目无懈可击!
【8月更文挑战第28天】在软件开发中,设计模式是解决常见问题的经典方案,助力构建可维护、可扩展的系统。本文探讨了.NET中三种关键架构设计模式:MVC、依赖注入与仓储模式,并提供了示例代码。MVC通过模型、视图和控制器分离关注点;依赖注入则通过外部管理组件依赖提升复用性和可测性;仓储模式则统一数据访问接口,分离数据逻辑与业务逻辑。掌握这些模式有助于开发者优化系统架构,提升软件质量。
52 5
|
3月前
|
C# Windows 开发者
超越选择焦虑:深入解析WinForms、WPF与UWP——谁才是打造顶级.NET桌面应用的终极利器?从开发效率到视觉享受,全面解读三大框架优劣,助你精准匹配项目需求,构建完美桌面应用生态系统
【8月更文挑战第31天】.NET框架为开发者提供了多种桌面应用开发选项,包括WinForms、WPF和UWP。WinForms简单易用,适合快速开发基本应用;WPF提供强大的UI设计工具和丰富的视觉体验,支持XAML,易于实现复杂布局;UWP专为Windows 10设计,支持多设备,充分利用现代硬件特性。本文通过示例代码详细介绍这三种框架的特点,帮助读者根据项目需求做出明智选择。以下是各框架的简单示例代码,便于理解其基本用法。
140 0