提到“配置”二字,我想绝大部分.NET开发人员脑海中会立马浮现出两个特殊文件的身影,那就是我们再熟悉不过的app.config和web.config,多年以来我们已经习惯了将结构化的配置定义在这两个文件之中。到了.NET Core的时代,很多我们习以为常的东西都发生了改变,其中也包括定义配置的方式。总的来说,新的配置系统显得更加轻量级,并且具有更好的扩展性,其最大的特点就是支持多样化的数据源。我们可以采用内存的变量作为配置的数据源,也可以直接配置定义在持久化的文件甚至数据库中。由于很多人都不曾接触过这个采用全新设计的配置系统,为了让大家对它有一个大体的认识,我们先从编程的角度来体验一下全新的配置读取方式。这个全新的配置系统为配置的读取定义了非常简单的API,这些API涉及到三个核心的对象,我们不妨称之为“配置编程模型三要素”。 [ 本文已经同步到《ASP.NET Core框架揭秘》之中]

目录
一、配置编程模型三要素
二、以键-值对的形式读取配置
三、读取结构化的配置
四、将结构化配置直接绑定为对象

一、配置编程模型三要素

就编程层面来讲,.NET Core的这个配置系统由如下图所示的三个核心对象构成。读取出来的配置信息最终会转换成一个Configuration对象供我们的程序使用。ConfigurationBuilder是Configuration对象的构建者,而ConfigurationSource则代表配置最原始的来源。

在读取配置的时候,我们根据配置的定义方式创建相应的ConfigurationSource,并将其注册到创建的ConfigurationBuilder对象上。由于提供配置的最初来源可能不止一个,所以我们可以注册多个相同或者不同类型的ConfigurationSource对象到ConfigurationBuilder上。ConfigurationBuilder这是利用注册的这些ConfigurationSource提供的原始数据最终构建出我们在程序中使用的Configuration对象。

根据本系列文章一贯采用的命名方式,我们应该知道上面介绍的Configuration、ConfigurationSource和ConfigurationBuilder均是对一类对象的统称,它们在API层面都通过相应的接口(IConfiguration、IConfigurationSource和IConfigurationBuilder)来表示,这些接口均义在NuGet包“Microsoft.Extensions.Configuration.Abstractions”中。如果我们的程序中只需要使用到这些接口,我们只需要添加针对这个NuGet包的依赖。至于这些接口的默认实现类型,则大多定义在“Microsoft.Extensions.Configuration”这个NuGet包中。

二、以键-值对的形式读取配置

虽然在大部分情况下的配置从整体来说都具有结构化的次关系,但是“原子”配置项都以最简单的“键-值对”的形式来体现,并且键和值通常都是字符串,接下来我们会通过一个简单的实例来演示如何以键值对的形式来读取配置。我们创建一个针对ASP.NET Core的控制台应用,并在project.json中按照如下的方式添加针对“Microsoft.Extensions.Configuration”这个NuGet包的依赖,配置模型就实现在这个包中。

   1: {

   2:   ...

   3:   "dependencies": {

   4:     "Microsoft.Extensions.Configuration": "1.0.0 "

   5:   },

   6: }

假设我们的应用程序需要通过配置来设定日期/时间的显示格式,为此我们将相关的配置信息定义在如下所示的这个DateTimeFormatOptions类,它的四个属性体现针对DateTime对象的四种显示格式(分别为长日期/时间和短日期/时间)。

   1: public class DateTimeFormatOptions

   2: {

   3:     public string LongDatePattern { get; set; }

   4:     public string LongTimePattern { get; set; }

   5:     public string ShortDatePattern { get; set; }

   6:     public string ShortTimePattern { get; set; }

   7:     //其他成员

   8: }

我们希望通过配置的形式来控制由DateTimeFormatOptions的四个属性体现的日期/时间显示格式,所以我们为它定义了一个构造函数。如下面的代码片段所示,该构造函数具有一个IConfiguration接口类型的参数,通过上面的介绍我们知道它是配置在应用程序中体现。键值对是配置的基本表现形式,所以Configuration对象提供了索引使我们可以根据配置项的Key得到配置项的值,下面的代码正式调用索引的方式得到对应配置信息的。

   1: public class DateTimeFormatOptions

   2: {

   3:     //其他成员

   4:     public DateTimeFormatOptions (IConfiguration config)

   5:     {

   6:         this.LongDatePattern     = config ["LongDatePattern"];

   7:         this.LongTimePattern     = config ["LongTimePattern"];

   8:         this.ShortDatePattern    = config ["ShortDatePattern"];

   9:         this.ShortTimePattern    = config ["ShortTimePattern"];

  10:     }

  11: }

要创建一个体现当前配置的DateTimeFormatOptions对象,我们必须向得到这个承载相关配置信息的Configuration对象。正如我们上面所说,Configuration对象是由ConfigurationBuilder创建的,而原始的配置信息则是通过相应的ConfigurationSource来提取的,所以创建一个Configuration对象的正确编程方式是先创建一个ConfigurationBuilder对象,然后为之注册一个或者多个ConfigurationSource对象,最后利用ConfigurationBuilder来创建我们需要的Configuration对象。

我们通过如下的程序来读取配置并将其转换成一个DateTimeFormatOptions对象。简单起见,我们采用一中类型为MemoryConfigurationSource的ConfigurationSource,它直接利用一个保存在内存中的字典对象作为最初的配置来源。如下面的代码片段所示,我们在为MemoryConfigurationSource提供的字典对象中设置了四种类型的日期/时间显示格式。

   1: Dictionary<string, string> source = new Dictionary<string, string>

   2: {

   3:     ["longDatePattern"] = "dddd, MMMM d, yyyy",

   4:     ["longTimePattern"] = "h:mm:ss tt",

   5:     ["shortDatePattern"] = "M/d/yyyy",

   6:     ["shortTimePattern"] = "h:mm tt"

   7: };

   8:  

   9: IConfiguration config = new ConfigurationBuilder()

  10:     .Add(new MemoryConfigurationSource { InitialData = source })

  11:     .Build();

  12:  

  13: DateTimeFormatOptions options = new DateTimeFormatOptions(config);

  14: Console.WriteLine($"LongDatePattern: {options.LongDatePattern}");

  15: Console.WriteLine($"LongTimePattern: {options.LongTimePattern}");

  16: Console.WriteLine($"ShortDatePattern: {options.ShortDatePattern}");

  17: Console.WriteLine($"ShortTimePattern: {options.ShortTimePattern}");

我们创建了一个ConfigurationBuilder类型的对象,并将这个MemoryConfigurationSource注册到它上面。接下来,我们直接调用ConfigurationBuilder的Build方法创建出Configuration对象,并利用后者创建了一个DateTimeFormatOptions对象。为了验证DateTimeFormatOptions对象是否与原始的配置一致,我们将它的四个属性打印在控制台上。程序运行之后,控制台上将会产生如下所示的输出结果。

   1: LongDatePattern : dddd, MMMM d, yyyy

   2: LongTimePattern : h:mm:ss tt

   3: ShortDatePattern: M/d/yyyy

   4: ShortTimePattern: h:mm tt

三、读取结构化的配置

真实项目中涉及的配置大都具有结构化的层次结构,所以Configuration对象同样具有这样的结构。结构化配置具有一个树形层次结构,我们不妨将其称之为“配置树”,一个Configuration对象最终对应着这棵配置树的某个节点,而整棵配置树自然可以由根节点对应的Configuration对象来表示。以键值对体现的“原子配置项”一般对应于配置树中不具有子节点的“叶子节点”。

接下来我们同样以实例的方式来演示如何定义并读取具有层次结构的配置。我们依然沿用上一节的应用场景,不过现在我们不仅仅需要设置日期/时间的格式,还需要设置其他数据类型的格式,比如表示货币的Decimal类型。为此我们定义了如下一个CurrencyDecimalFormatOptions类,它的属性Digits和Symbol分别表示小数位数和货币符号,一个CurrencyDecimalFormatOptions对象依然是利用一个Configuration对象来创建的。

   1: public class CurrencyDecimalFormatOptions

   2: {

   3:     public int        Digits { get; set; }

   4:     public string     Symbol { get; set; }

   5:  

   6:     public CurrencyDecimalFormatOptions (IConfiguration config)

   7:     {

   8:         this.Digits = int.Parse(config["Digits"]);

   9:         this.Symbol = config["Symbol"];

  10:     }

  11: }

我们定义了另一个名为FormatOptions的类型来表示针对不同数据类型的格式设置。如下面的代码片段所示,它的两个属性DateTime和CurrencyDecimal分别表示针对日期/时间和货币数字的格式设置。FormatOptions依然具有一个参数类型为IConfiguration接口的构造函数,它的两个属性均在此构造函数中被初始化。值得注意的是初始化这两个属性采用的是当前Configuration的“子配置节”,我们通过指定配置节名称调用GetSection方法获得这两个子配置节。

   1: public class FormatOptions

   2: {

   3:     public DateTimeFormatOptions            DateTime { get; set; }

   4:     public CurrencyDecimalFormatOptions     CurrencyDecimal { get; set; }

   5:  

   6:     public FormatOptions (IConfiguration config)

   7:     {

   8:         this.DateTime = new DateTimeFormatOptions (config.GetSection("DateTime"));

   9:         this.CurrencyDecimal = new CurrencyDecimalFormatOptions (config.GetSection("CurrencyDecimal"));

  10:     }

  11: }

FormatOptions类型体现的配置具有如下图所示的树形层次化结构。在我们上面演示的实例中,我们通过以一个MemoryConfigurationSource对象来提供原始的配置信息。由于承载原始配置信息的是一个元素类型为KeyValuePair<string, string>的集合,它在物理存储上并不具有树形化的层次结构,那么它如何能够最终提供一个结构化的Configuration对象呢?

解决方案其实很简单,对于一棵完整的配置树,具体的配置信息最终是通过叶子节点来承载的,所以MemoryConfigurationSource只需要在配置字典中保存叶子节点的数据即可。除此之外,为了描述配置树的结构,配置字典需要将对应叶子节点在配置树种的路径作为Key。所以MemoryConfigurationSource可以采用下表所示的配置字典对配置数进行“扁平化”,路径采用冒号(“:”)作为分隔符。

Key

Value

Format:DateTime:LongDatePattern

dddd, MMMM d, yyyy

Format:DateTime:LongTimePattern

h:mm:ss tt

Format:DateTime:ShortDatePattern

M/d/yyyy

Format:DateTime:ShortTimePattern

h:mm tt

Format:CurrencyDecimal:Digits

2

Format:CurrencyDecimal:Symbol

$

如下面的代码片段所示,我们按照表1所示的结构创建了一个Dictionary<string, string>对象,并利用它创建出MemoryConfigurationSource对象。在利用ConfigurationBuildr得到表示整个配置的Configuration对象之后,我们调用其GetSection方法得到名称为“Format”的配置节,并利用后者创建一个FormatOptions。

   1: Dictionary<string, string> source = new Dictionary<string, string>

   2: {

   3:     ["format:dateTime:longDatePattern"] = "dddd, MMMM d, yyyy",

   4:     ["format:dateTime:longTimePattern"] = "h:mm:ss tt",

   5:     ["format:dateTime:shortDatePattern"] = "M/d/yyyy",

   6:     ["format:dateTime:shortTimePattern"] = "h:mm tt",

   7:  

   8:     ["format:currencyDecimal:digits"] = "2",

   9:     ["format:currencyDecimal:symbol"] = "$",

  10: };

  11: IConfiguration configuration = new ConfigurationBuilder()

  12:         .Add(new MemoryConfigurationSource { InitialData = source })

  13:         .Build();

  14:  

  15: FormatOptions options = new FormatOptions(configuration.GetSection("Format"));

  16: DateTimeFormatOptions dateTime = options.DateTime;

  17: CurrencyDecimalFormatOptions currencyDecimal = options.CurrencyDecimal;

  18:  

  19: Console.WriteLine("DateTime:");

  20: Console.WriteLine($"\tLongDatePattern: {dateTime.LongDatePattern}");

  21: Console.WriteLine($"\tLongTimePattern: {dateTime.LongTimePattern}");

  22: Console.WriteLine($"\tShortDatePattern: {dateTime.ShortDatePattern}");

  23: Console.WriteLine($"\tShortTimePattern: {dateTime.ShortTimePattern}");        

  24:  

  25: Console.WriteLine("CurrencyDecimal:");

  26: Console.WriteLine($"\tDigits:{currencyDecimal.Digits}");

  27: Console.WriteLine($"\tSymbol:{currencyDecimal.Symbol}");

在得到利用读取的配置创建的 FormatOptions对象之后,为了验证该对象与原始配置数据是否一致,我们依然将它的相关属性打印在控制台上。这个程序之后之后改程序会在控制台上呈现如下所示的输出结果。(S02)

   1: DateTime:

   2:         LongDatePattern : dddd, MMMM d, yyyy

   3:         LongTimePattern : h:mm:ss tt

   4:         ShortDatePattern: M/d/yyyy

   5:         ShortTimePattern: h:mm tt

   6:  

   7: CurrencyDecimal:

   8:         Digits          : 2

   9:         Symbol          : $

四、将结构化配置直接绑定为对象

在真正的项目开发过程中,我们倾向于像我们演示的两个实例一样通过创建相应的类型(比如演示实例中的DateTimeFormatOptions、CurrencyDecimalOptions和FormatOptions)来定义一组相关的配置选项(Option),我们将定义配置选项(Option)的这些类型称为Option类型。在上面演示的实例中,为了创建这些封装配置的对象,我们都是采用手工读取配置的形式,如果定义的配置项太多的话,逐条读取配置项其实是一项非常繁琐的工作。

对于一个对象来说,如果我们将它的属性视为它的子节点,一个对象同样具有类似于Configuration对象的树形层次结构。如果我们根据某个Option类型的结构来定义配置,或者根据配置的结构来定义这个Option类型,Option类型的属性成员将与某个配置节具有一一对应的关系,那么原则上我们可以自动将配置信息绑定为一个具体的Option对象。

.NET Core的配置系统采用一种叫做“Options Pattern”的编程模式来支持从原始配置到Options对象之间的绑定。这种编程模式涉及的API定义在“Microsoft.Extensions.Options.ConfigurationExtensions”这个NuGet包中,所以我们需要在project.json文件中按照如下的方式添加针对性的依赖。除此之外,“Options Pattern”涉及到对DI的使用,所以我们还需要添加针对NuGet包“Microsoft.Extensions
.DependencyInjection”的依赖。

   1: {

   2:   ...

   3:   "dependencies": {

   4:     "Microsoft.Extensions.Options.ConfigurationExtensions"    : "1.0.0",

   5:     "Microsoft.Extensions.DependencyInjection"                : "1.0.0"

   6:   },

   7: }

借助于Options Pattern的自动绑定机制,我们无需逐条地读取配置,所以我们可以将这个三个Options类型(DateTimeFormatOptions、CurrencyDecimalOptions和FormatOptions)的构造函数全部删除,只保留其属性成员。在作为程序入口的Main方法中,我们采用如下的方式创建这个表示格式设置的FormatOptions对象。

   1: ...

   2: FormatOptions options = new ServiceCollection()

   3:     .AddOptions()

   4:     .Configure<FormatOptions>(config.GetSection("Format"))

   5:     .BuildServiceProvider()

   6:     .GetService<IOptions<FormatOptions>>()

   7:     .Value;

如上面的代码片段所示,我们创建一个ServiceCollection对象并调用扩展方法AddOptions注册于针对Option模型的服务。接下来我们调用Configure方法将FormatOptions这个Option类型与对应的Configuration对象进行映射。我们最后利用这个ServiceCollection对象生成一个ServiceProvider,并调用其GetService方法得到一个类型为IOptions<FormatOptions>的对象,后者的Value属性返回的就是绑定了相关配置的FormatOptions对象。

.NET Core采用的全新配置系统[1]: 读取配置数据的更多相关文章

  1. .NET Core采用的全新配置系统[10]: 配置的同步机制是如何实现的?

    配置的同步涉及到两个方面:第一,对原始的配置文件实施监控并在其发生变化之后从新加载配置:第二,配置重新加载之后及时通知应用程序进而使后者能够使用最新的配置.要了解配置同步机制的实现原理,先得从认识一个 ...

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

    在<.NET Core采用的全新配置系统[1]: 读取配置数据>中,我们通过实例的方式演示了几种典型的配置读取方式,其主要目的在于使读者朋友们从编程的角度对.NET Core的这个全新的配 ...

  3. .NET Core采用的全新配置系统[3]: “Options模式”下的配置是如何绑定为Options对象

    配置的原子结构就是单纯的键值对,并且键和值都是字符串,但是在真正的项目开发中我们一般不会单纯地以键值对的形式来使用配置.值得推荐的做法就是采用<.NET Core采用的全新配置系统[1]: 读取 ...

  4. .NET Core采用的全新配置系统[7]: 将配置保存在数据库中

    我们在<聊聊默认支持的各种配置源>和<深入了解三种针对文件(JSON.XML与INI)的配置源>对配置模型中默认提供的各种ConfigurationSource进行了深入详尽的 ...

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

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

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

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

  7. .NET Core采用的全新配置系统[5]: 聊聊默认支持的各种配置源[内存变量,环境变量和命令行参数]

    较之传统通过App.config和Web.config这两个XML文件承载的配置系统,.NET Core采用的这个全新的配置模型的最大一个优势就是针对多种不同配置源的支持.我们可以将内存变量.命令行参 ...

  8. .NET Core采用的全新配置系统[9]: 为什么针对XML的支持不够好?如何改进?

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

  9. .NET Core采用的全新配置系统[8]: 如何实现配置与源文件的同步

    配置的同步涉及到两个方面:第一,对原始的配置文件实施监控并在其发生变化之后从新加载配置:第二,配置重新加载之后及时通知应用程序进而使后者能够使用最新的配置.接下来我们利用一个简单的.NET Core控 ...

随机推荐

  1. 编写高质量代码:改善Java程序的151个建议(第5章:数组和集合___建议75~78)

    建议75:集合中的元素必须做到compareTo和equals同步 实现了Comparable接口的元素就可以排序,compareTo方法是Comparable接口要求必须实现的,它与equals方法 ...

  2. Chrome出了个小bug:论如何在Chrome下劫持原生只读对象

    Chrome出了个小bug:论如何在Chrome下劫持原生只读对象 概述 众所周知,虽然JavaScript是个很灵活的语言,浏览器里很多原生的方法都可以随意覆盖或者重写,比如alert.但是为了保证 ...

  3. C# 中参数验证方式的演变

    一般在写方法的时候,第一步就是进行参数验证,这也体现了编码者的细心和缜密,但是在很多时候这个过程很枯燥和乏味,比如在拿到一个API设计文档的时候,通常会规定类型参数是否允许为空,如果是字符可能有长度限 ...

  4. ExtJS 4.2 组件的查找方式

    组件创建了,就有方法找到这些组件.在DOM.Jquery都有各自的方法查找元素/组件,ExtJS也有自己独特的方式查找组件.元素.本次从全局查找.容器内查找.form表单查找.通用组件等4个方面介绍组 ...

  5. MVVM TextBox的键盘事件

    MVVM下RichTextBox的键盘回车事件设置为发送,不是回车 xmlns:i="http://schemas.microsoft.com/expression/2010/interac ...

  6. C#多线程之线程池篇3

    在上一篇C#多线程之线程池篇2中,我们主要学习了线程池和并行度以及如何实现取消选项的相关知识.在这一篇中,我们主要学习如何使用等待句柄和超时.使用计时器和使用BackgroundWorker组件的相关 ...

  7. Linux上如何查看物理CPU个数,核数,线程数

    首先,看看什么是超线程概念 超线程技术就是利用特殊的硬件指令,把两个逻辑内核模拟成两个物理芯片,让单个处理器都能使用线程级并行计算,进而兼容多线程操作系统和软件,减少了CPU的闲置时间,提高的CPU的 ...

  8. html5的web存储

    在html5标准之前,web存储信息需要cookie来完成,但是cookie不适合大量数据存储.因为需要等待服务器响应,所以速度慢/效率低. 本地存储的特点: localstorage是仅存储在用户的 ...

  9. 独立开发 一个社交 APP 的架构分享 (已实现)

    (本博客为原创:http://www.cnblogs.com/linguanh/)   My BananaCloud Android Application 前言:  这算是我的第一个 完完全全 由自 ...

  10. $ORACLE_HOME变量值末尾多“/”惹的祸

    之前一直误以为$ORACLE_HOME变量的路径中末尾多写一个"/"不会有影响. 今天做实验时碰到一个情景,发现并不是这样. 环境:OEL 5.7 + Oracle 10.2.0. ...