ASP.NET Core 3 框架揭秘(上下册)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

6.5 多样性的配置源

.NET Core 采用的全新的配置模型的一个主要特点就是对多种不同配置源提供支持。我们可以将内存变量、命令行参数、环境变量和物理文件作为原始配置数据的来源。如果将物理文件作为配置源,就可以选择不同的格式(如XML、JSON和INI等)。如果这些默认支持的配置源形式无法满足需求,也可以通过注册自定义 IConfigurationSource 的方式将其他形式的数据作为配置源。

6.5.1 MemoryConfigurationSource

6.1节至6.4节的大部分实例演示都使用MemoryConfigurationSource来提供原始的配置。我们知道 MemoryConfigurationSource 配置源采用一个字典对象(具体来说应该是一个元素类型为KeyValuePair<string,string>的集合) 作为存放原始配置数据的容器。作为一个IConfigurationSource 对象,它总是通过创建某个对应的 IConfigurationProvider 对象来完成具体的配置数据读取工作,那么MemoryConfigurationSource究竟会提供一个什么样的IConfiguration Provider对象?

上面的代码片段体现了 MemoryConfigurationSource 类型的完整定义,由此可以看到,它具有一个 IEnumerable<KeyValuePair<string,string>>类型的属性 InitialData来存放初始的配置数据。从 Build 方法的实现可以看出,真正被它用来读取原始配置数据的是一个 MemoryConfiguration Provider类型的对象,该类型的定义如下面的代码片段所示。

从上面的代码片段可以看出,MemoryConfigurationProvider 派生于抽象类 Configuration Provider,同时实现了 IEnumerable<KeyValuePair<string,string>>接口。ConfigurationProvider对象直接使用一个 Dictionary<string,string>来保存配置数据,当我们根据一个 MemoryConfiguration Source对象调用构造函数创建MemoryConfigurationProvider时,它只需要将通过InitialData属性保存的配置数据转移到这个字典中即可。MemoryConfigurationProvider 定义的 Add 方法在任何时候都可以向配置字典中添加一个新的配置项。

通过前面对配置模型的介绍可知,IConfigurationProvider对象在配置模型中所起的作用就是读取原始配置数据并将其转换成配置字典。在所有预定义的 IConfigurationProvider 实现类型中,MemoryConfigurationProvider 最为简单、直接,因为它对应的配置源就是一个配置字典,所以根本不需要做任何结构转换。

利用MemoryConfigurationSource生成配置时,我们需要将其注册到IConfigurationBuilder对象之上。具体来说,我们可以像前面演示的实例一样直接调用IConfigurationBuilder接口的Add方法,也可以调用如下所示的两个重载的AddInMemoryCollection扩展方法。

6.5.2 EnvironmentVariablesConfigurationSource

顾名思义,环境变量就是描述当前执行环境并影响进程执行行为的变量。按照作用域的不同,可以将环境变量分为 3 类,即针对当前系统、当前用户和当前进程的环境变量。系统和用户级别的环境变量保存在注册表中,它们的路径分别为“HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Session Manager\Environment”和“HKEY_CURRENT_USER\Environment”。

环境变量的提取和维护可以通过静态类型Environment来完成。具体来说,我们可以调用它的静态方法 GetEnvironmentVariable 获得某个指定名称的环境变量的值,而 GetEnvironment Variables 方法则会返回所有的环境变量,EnvironmentVariableTarget 枚举类型的参数代表环境变量作用域决定的存储位置。如果在调用 GetEnvironmentVariable 方法或者 GetEnvironment Variables方法时没有显式指定参数target或者将参数指定为EnvironmentVariableTarget.Process,在进程初始化前存在的所有环境变量(包括针对系统、当前用户和当前进程)将会作为候选列表。

环境变量的添加、修改和删除均由 SetEnvironmentVariable 方法完成,如果没有显式指定参数 target,默认采用的就是 EnvironmentVariableTarget.Process。如果希望删除指定名称的环境变量,在调用这个方法的时候将参数value设置为Null或者空字符串即可。

除了在程序中利用静态类型 Environment,还可以采用命令行的方式查看和设置环境变量。除此之外,在开发环境中还可以利用“System Properties”(系统属性)设置工具,以可视化的方式查看和设置系统与用户级别的环境变量(“This PC”→“Properties”→“Change Settings”→“Advanced”→“Environment Variables”)。如果采用Visual Studio调试编写的应用,则可以采用设置项目属性的方式来设置进程级别的环境变量(“Properties”→“Debug”→“Environment Variables”),如图6-16所示。如第1章中陈述,设置的环境变量会被保存到launchSettings.json文件中。

图6-16 设置环境变量

针对环境变量的配置源可以通过 EnvironmentVariablesConfigurationSource 类型来表示,该类型定义在 NuGet 包“Microsoft.Extensions.Configuration.EnvironmentVariables”之中。该类型定义了一个字符串类型的属性Prefix,表示环境变量名的前缀。如果设置了Prefix属性,系统只会选择名称作为前缀的环境变量。

由前面给出的代码片段可以看出,EnvironmentVariablesConfigurationSource配置源会利用对应的 EnvironmentVariablesConfigurationProvider对象来读取环境变量,此操作体现在如下所示的Load 方法中。由于环境变量本身就是一个数据字典,所以 EnvironmentVariables ConfigurationProvider 对象无须再进行结构上的转换。当 Load 方法被执行之后,它只需要将符合条件的环境变量筛选出来并添加到自己的配置字典中即可。

值得一提的是,如果创建 EnvironmentVariablesConfigurationProvider对象时指定了用于筛选环境变量的前缀,当符合条件的环境变量被添加到自身的配置字典之后,配置项的名称会将此前缀剔除。例如,前缀设置为“FOO_”,环境变量 FOO_BAR被添加到配置字典之后,配置项名称会变成BAR,这个细节也体现在上面定义的Load方法中。

在使用 EnvironmentVariablesConfigurationSource时,可以调用 Add 方法将它注册到指定的IConfigurationBuilder对象上。除此之外,EnvironmentVariablesConfigurationSource的注册还可以直接调用IConfigurationBuilder接口的如下3个重载的AddEnvironmentVariables扩展方法来完成。

下面的实例演示了如何将环境变量作为配置源。如下面的代码片段所示,可以调用Environment 的静态方法 SetEnvironmentVariable 设置 4 个环境变量,变量名称具有相同的前缀“TEST_”。我们调用AddEnvironmentVariables方法创建了一个EnvironmentVariablesConfiguration Source 对象,并将其注册到创建的 ConfigurationBuilder 对象之上,在调用该方法时可以将环境变量名称的前缀设置为“TEST_”。最终将由 ConfigurationBuilder构建的 IConfiguration对象绑定成一个Profile对象。(S613)

6.5.3 CommandLineConfigurationSource

在很多情况下,我们会采用 Self-Host方式将一个 ASP.NET Core应用寄宿到一个托管进程中,此时我们倾向于采用命令行的方式来启动寄宿程序。当以命令行的形式启动一个 ASP.NET Core 应用时,我们希望直接使用命名行开关(Switch)来控制应用的一些行为,所以命令行开关自然也就成了配置常用的来源之一。配置模型针对这种配置源的支持是通过CommandLineConfigurationSource 实现的,该类型定义在 NuGet 包“Microsoft.Extensions.Configuration.CommandLine”中。

以命令行的形式执行某个命令时,命令行开关(包括名称和值)体现为一个简单的字符串数组,所以 CommandLineConfigurationSource 的根本目的在于将命名行开关从字符串数组转换成配置字典。要充分理解这个转换规则,我们需要先了解 CommandLineConfigurationSource 支持的命令行开关究竟采用什么样的形式来指定。下面通过一个简单的实例来说明命令行开关的几种指定方式。假设我们有一个命令exec,并采用如下方式执行某个托管程序(app)。

在执行“exec”命令时可以通过相应的命令行开关指定多个选项。总的来说,命令行开关的指定形式大体上分为两种:单参数(Single Argument)和双参数(Double Arguments)。单参数形式就是采用等号(=)将命令行开关的名称和值通过如下方法采用一个参数来指定。

{name}={value}.

{prefix}{name}={value}.

对于第二种单参数命令行开关的指定形式,我们可以在开关名称前面添加一个前缀,目前的前缀支持“/”、“--”和“-”这 3 种。遵循这样的格式,我们可以采用如下 3 种方式将命令行开关 architecture 设置为 x64。下面的列表之所以没有使用前缀“-”,是因为这个前缀要求使用命令行开关映射(Switch Mapping),下面会单独进行介绍。

除了采用单参数形式,还可以采用双参数形式来指定命令行开关。双参数就是使用两个参数分别定义命令行开关的名称和值。这种形式采用的具体格式为{prefix}{name} {value},所以上述命令行开关architecture也可以采用如下方式来指定。

命令行开关的全名和缩写之间具有一个映射关系(Switch Mapping)。以上述两个命令行开关为例,我们可以采用首字母“a”来代替“architecture”。如果使用“-”作为前缀,不论采用单参数还是双参数,都必须使用映射后的开关名称。值得一提的是,同一个命令行开关可以具有多个映射,如可以同时将“architecture”映射为“arch”。假设“architecture”具有这两种映射,我们就可以按照如下两种方式指定CPU架构。

了解了命令行开关的指定形式之后,下面介绍 CommandLineConfigurationSource 类型和由它提供的 CommandLineConfigurationProvider对象。由于原始的命令行参数总是体现为一个采用空格分隔的字符串,这样的字符串可以进一步转换成一个字符串集合,所以CommandLineConfigurationSource 对象以字符串集合作为配置源。如下面的代码片段所示,CommandLineConfigurationSource类型具有两个属性,即 Args属性和 SwitchMappings属性:前者代表承载原始命令行参数的字符串集合,后者则保存了命令行开关的缩写与全称之间的映射关系。CommandLineConfigurationSource 实现的 Build 方法会根据这两个属性创建并返回一个CommandLineConfigurationProvider对象。

具有如下定义的 CommandLineConfigurationProvider类型依然是抽象类 ConfigurationProvider的继承者。CommandLineConfigurationProvider类型的目的很明确,就是对体现为字符串集合的原始命令行参数进行解析,并将解析出的参数名称和值添加到配置字典中,这一切都是在重写的Load方法中完成的。

在采用基于命令行参数作为配置源时,我们可以创建一个 CommandLineConfiguration Source对象,并将其注册到ConfigurationBuilder上。我们也可以调用IConfigurationBuilder接口的如下3个AddCommandLine扩展方法将两个步骤合二为一。

为了使读者对 CommandLineConfigurationSource/CommandLineConfigurationProvider 解析命令行参数采用的策略有深刻的认识,下面演示一个简单的实例。如下面的代码片段所示,我们创建了一个 ConfigurationBuilder对象,并调用 AddCommandLine方法注册了针对命令行参数的配置源,Main方法的参数args直接作为原始的命令行参数。

在调用 AddCommandLine扩展方法注册 CommandLineConfigurationSource对象时,我们指定了一个命令行开关映射表,它将命令行开关“architecture”映射为“a”和“arch”。需要注意的是,在通过字典定义命令行开关映射时,作为目标名称的 Key 应该添加前缀“-”。下面调用ConfigurationBuilder对象的Build方法创建IConfiguration对象,然后从中提取“architecture”配置项的值并打印出来。如图 6-17 所示,可以采用命令行的形式启动这个程序,并以不同的形式指定“architecture”的值。(S614)

图6-17以命令行参数的形式提供配置

6.5.4 FileConfigurationSource

物理文件是我们最常用到的原始配置载体,而最佳的配置文件格式主要有3种,即JSON、XML 和 INI,对应的配置源类型分别是 JsonConfigurationSource、XmlConfigurationSource 和IniConfigurationSource,它们具有如下一个相同的基类FileConfigurationSource。

FileConfigurationSource 对象总是利用 IFileProvider 对象来读取配置文件,我们可以利用FileProvider属性来设置这个对象。配置文件的路径可以用 Path属性表示,一般来说这是一个针对 IFileProvider 对象根目录的相对路径。在读取配置文件时,这个路径将作为参数调用IFileProvider 对象的 GetFileInfo 方法,以得到描述配置文件的 IFileInfo 对象,该对象的CreateReadStream方法最终会被调用来读取文件内容。

如果 FileProvider 属性并没有被显式赋值,并且指定的配置文件路径是一个绝对路径(如“c:\app\appsettings.json”),那么将创建一个针对配置文件所在目录(“c:\app”)的 PhysicalFileProvider,并作为 FileProvider 的属性值,而 Path 属性将被设置成配置文件名。如果指定的仅仅是一个相对路径,FileProvider 属性就不会被自动初始化。这个逻辑可以在ResolveFileProvider方法中实现,并体现在如下测试程序中。

除了ResolveFileProvider方法,FileConfigurationSource还定义了EnsureDefaults方法,该方法会确保 FileConfigurationSource总是具有一个用于加载配置文件的 IFileProvider对象。具体来说,EnsureDefaults方法最终会调用IConfigurationBuilder接口具有如下定义的GetFileProvider扩展方法来获取默认的IFileProvider对象。

从上面给出的代码片段可以看出,GetFileProvider 扩展方法实际上是将 IConfiguration Builder 对象的 Properties 属性表示的字典作为存放 IFileProvider 对象的容器(对应的 Key 为FileProvider)。如果这个容器中存在一个 IFileProvider 对象,那么它将作为方法的返回值。反之,该方法会将当前应用的基础目录(默认为当前应用程序域的基础目录,也就是当前执行的.exe文件所在的目录)作为根目录创建一个PhysicalFileProvider对象。

由于默认情况下 EnsureDefaults 方法会从 IConfigurationBuilder 对象的属性字典中提取IFileProvider对象,所以可以在这个属性字典中存放一个默认的 IFileProvider对象供所有注册在它上面的 FileConfigurationSource 对象共享。实际上,IConfigurationBuilder 接口提供的SetFileProvider方法和SetBasePath方法可以实现这个功能。

FileConfigurationSource 对象的 Optional 属性表示当前配置源是否可以默认。如果该属性被设置成 False,即使指定的配置文件不存在也不会抛出异常。可默认的配置文件在支持多环境的场景中具有广泛应用。正如前面的演示实例,我们可以按照如下方式加载两个配置文件:基础配置文件 appsettings.json 一般包含相对全面的配置,针对某个环境的差异化配置则定义在appsettings.{environment}.json 文件中。前者是必需的,后者则是可以默认的,这保证了应用程序在缺少基于当前环境的差异化配置文件的情况下依然可以使用定义在基础配置文件中的默认配置。

FileConfigurationSource 借助 IFileProvider 对象提供的文件系统监控功能,实现了配置文件在更新后的自动实时加载功能,这个特性通过ReloadOnChange属性来开启或者关闭。在默认情况下这个特性是关闭的,我们需要通过将这个属性设置为 True 来显式地开启该特性。如果开启了配置文件的重新加载功能,一旦配置文件发生变化,IFileProvider对象会在第一时间将通知发送给对应的FileConfigurationProvider对象,后者会调用Load方法重新加载配置文件。考虑到针对配置文件的写入此时可能尚未结束,所以 FileConfigurationSource采用“延时加载”的方式来解决这个问题,具体的延时通过ReloadDelay属性来控制。该属性的单位是毫秒,默认设置的延时为250毫秒。

考虑到针对配置文件的加载不可能百分之百成功,所以 FileConfigurationSource提供了相应的异常处理机制。具体来说,可以通过 FileConfigurationSource对象的 OnLoadException属性注册一个 Action<FileLoadExceptionContext>类型的委托作为异常处理器。作为参数的FileLoadExceptionContext 对象代表 FileConfigurationProvider 在加载配置文件出错的情况下为异常处理器提供的执行上下文。

如上面的代码片段所示,我们可以从 FileLoadExceptionContext 上下文中获取抛出的异常和当前 FileConfigurationProvider对象。如果异常处理结束之后上下文对象的 Ignore属性被设置为True,FileConfigurationProvider对象就会认为目前的异常(可能是原来抛出的异常,也可能是异常处理器设置的异常)是可以被忽略的,此时程序会继续执行,否则异常还是会抛出来。另外,最终抛出来的是原来的异常,所以通过修改上下文的 Exception属性无法达到抛出另一个异常的目的。

就像为注册到 IConfigurationBuilder 对象上的所有 FileConfigurationSource 注册一个共享的IFileProvider 对象一样,我们也可以调用 IConfigurationBuilder 接口的 SetFileLoad ExceptionHandler 扩展方法注册一个共享的异常处理器,该方法依然是利用 IConfiguration Builder 对象的属性字典来存放这个作为异常处理器的委托对象的。注册的这个异常处理器可以通过对应的GetFileLoadExceptionHandler扩展方法来获取。

前面我们提到FileConfigurationSource的EnsureDefaults方法,除了在IFileProvider对象没有被初始化的情况下调用IConfigurationBuilder的GetFileProvider扩展方法提供一个默认的IFileProvider对象,EnsureDefaults 方法还会在异常处理器没有初始化的情况下调用上面的 GetFileLoad ExceptionHandler扩展方法提供一个默认的异常处理器。

对于配置系统默认提供的针对 3种文件格式化的 FileConfigurationSource类型来说,它们提供的 IConfigurationProvider 实现都派生于抽象基类 FileConfigurationProvider。对于自定义的FileConfigurationSource,我们也倾向于将这个抽象类作为对应 IConfigurationProvider 实现类型的基类。

创建一个FileConfigurationProvider对象时需要提供对应的FileConfigurationSource对象,它会赋值给Source属性。如果指定的FileConfigurationSource对象开启了配置文件更新监控和自动加载功能(其属性 OnLoadException 返回 True),FileConfigurationProvider 对象会利用FileConfigurationSource 对象提供的 IFileProvider 对象对配置文件实施监控,并通过注册回调的方式在配置文件更新的时候调用Load方法重新加载配置。

由于FileConfigurationSource提供了IFileProvider对象,所以FileConfigurationProvider对象可以调用其 CreateReadStream 方法获取读取配置文件内容的流对象,也就是说,我们可以利用这个 Stream对象来完成配置的加载。根据基于 Stream加载配置的功能体现在抽象方法 Load上,所以FileConfigurationProvider的派生类都需要重写这个方法。

JsonConfigurationSource

JsonConfigurationSource 代表针对基于 JSON 文件的配置源,该类型定义在 NuGet 包“Microsoft.Extensions.Configuration.Json”中。从下面的定义可以看出,JsonConfigurationSource 重写的Build方法在提供对应的JsonConfigurationProvider对象之前会调用EnsureDefaults方法,这个方法确保用于读取配置文件的 IFileProvider 对象和处理配置文件加载异常的处理器被初始化。JsonConfigurationProvider派生于抽象类 FileConfigurationProvider,它利用重写的 Load方法读取配置文件的内容并将其转换成配置字典。

IConfigurationBuilder 接口用如下几个 AddJsonFile 扩展方法来注册 JsonConfiguration Source。如果调用第一个 AddJsonFile 方法重载,就可以利用指定的 Action<JsonConfiguration Source>对象对创建的JsonConfigurationSource进行初始化。而其他AddJsonFile方法重载实际上就是通过相应的参数初始化 JsonConfigurationSource 对象的 Path 属性、Optional 属性和ReloadOnChange属性。

当使用 JSON文件定义配置时,不论对于何种数据结构(复杂对象、集合、数组和字典),我们都能通过 JSON文件以一种简单而自然的方式来定义它们。同样以前面定义的 Profile类型为例,我们可以利用如下所示的3个JSON文件分别定义一个完整的Profile对象、一个Profile对象的集合,以及一个Key和Value类型分别为字符串与Profile的字典。

XmlConfigurationSource

XML 也是一种常用的配置定义形式,它对数据的表达能力甚至强于 JSON,几乎所有类型的数据结构都可以用 XML表示出来。用一个 XML元素表示一个复杂对象时,对象的数据成员可以定义为当前XML元素的子元素。如果数据成员是一个简单的数据类型,我们还可以选择将其定义成当前 XML 元素的属性(Attribute)。针对一个 Profile 对象,我们可以采用如下两种不同的形式来定义。

或者:

虽然 XML对数据结构的表达能力总体上强于 JSON,但是 XML作为配置模型的数据来源有其局限性,如它们对集合的表现形式有点不尽如人意。例如,对于一个元素类型为 Profile 的集合,我们可以采用如下结构的XML来表现。

上述 XML 不能正确地转换成配置字典,这是因为字典的 Key 必须是唯一的,而且最终构成配置树的每个节点必须具有不同的路径。上面这段XML无法满足这个基本的要求,因为表示一个 Profile对象的 3个 XML元素(<Profile>...</Profile>)是同质的,对于由它们表示的 3个Profile 对象来说,分别表示性别、年龄、电子邮箱地址和电话号码的 4 个叶子节点的路径是完全一样的,所以根本无法作为配置字典的 Key。通过前面针对配置绑定的介绍可知,如果需要用配置字典来表示一个 Profile 对象的集合,就需要按照如下方式为每个集合元素加上相应的索引(foo、bar和baz)。

按照这样的结构,如果需要以 XML 的方式来表示一个 Profile 对象的集合,就不得不采用如下结构。但是这样的定义方式从语义角度来讲是不合理的,因为同一个集合的所有元素就应该是同质的,同质的XML元素采用不同的名称是不合理的。根据配置绑定的规则,这样的结构同样可以表示一个由 3 个元素组成的 Dictionary<string,Profile>对象,Key 分别是 Foo、Bar 和Baz。如果用这样的XML来表示一个字典对象,那么语义上完全没有问题。

针对 XML 文件的配置源类型为 XmlConfigurationSource,该类型定义在 NuGet 包“Microsoft.Extensions.Configuration.Xml”中。如下面的代码片段所示,XmlConfigurationSource通过重写的 Build 方法创建出对应的 XmlConfigurationProvider 对象。作为抽象类型FileConfigurationProvider 的继承者,XmlConfigurationProvider 通过重写的 Load 方法完成了针对XML文件的读取和配置字典的初始化。

JsonConfigurationSource的注册可以通过调用针对 IConfigurationBuilder接口的 AddJsonFile扩展方法来完成。与之类似,IConfigurationBuilder接口同样具有如下一系列名为AddXmlFile的扩展方法,这些方法可以帮助我们注册根据指定XML文件创建的XmlConfigurationSource对象。

XML之所以不能像 JSON格式那样可以以一种很自然的形式表示集合或者数组,是因为后者对这两种数据类型提供了明确的定义方式(采用中括号定义),但 XML 只有子元素的概念,我们无法确定它的子元素是否是一个集合。可以做这样一个假设:如果同一个XML元素下的所有子元素都具有相同的名称,那么我们可以将其视为集合。根据这个假设,对XmlConfigurationSource略加修改就可以解决XML难以表示集合数据结构的问题。

我们通过派生XmlConfigurationSource创建一个新的IConfigurationSource实现类型,姑且将其命名为ExtendedXmlConfigurationSource。XmlConfigurationSource提供的ConfigurationProvdier类型为 ExtendedXmlConfigurationProvider,它派生于 XmlConfigurationProvider。在重写的 Load方法中,ExtendedXmlConfigurationProvider 对原始的 XML 结构进行相应的改动,可以使原本不合法的XML(XML元素具有相同的名称)转换成一个针对集合的配置字典。图6-18展示了XML结构转换所采用的规则和步骤。

图6-18 XML结构转换所采用的规则和步骤

如图 6-18 所示,针对集合对原始 XML 所做的结构转换由两个步骤组成:第一步,为表示集合元素的 XML 元素添加一个名为 append_index 的属性(Attribute),我们采用零基索引作为该属性的值;第二步,根据第一步转换的结果创建一个新的 XML,同名的集合元素(如<profile>)将根据添加的索引值重新命名(如<profile_index_0>)。毫无疑问,转换后的这个XML可以很好地表示一个集合对象。如下所示的代码片段是 ExtendedXmlConfigurationProvider类型的定义,上述转换逻辑就体现在重写的Load方法中。(S615)

为了能够将上面的 XmlConfigurationProvider 应用到程序中,需要为它定义相应的IConfigurationSource 类型,为此我们定义了下面的 ExtendedXmlConfigurationSource 类型。它直接继承自 XmlConfigurationSource 类型,并在重写的 Build 方法中提供 ExtendedXmlConfiguration Provider对象。为了方便将 ExtendedXmlConfigurationSource对象注册到 IConfigurationBuilder对象上,也可以进一步定义如下这些扩展方法。

IniConfigurationSource

INI是 Initialization的缩写形式。INI文件又被称为初始化文件,也是 Windows操作系统普遍使用的配置文件,也被一些Linux操作系统和UNIX操作系统所支持。INI文件直接以键值对的形式定义配置项,如下所示的代码片段体现了 INI 文件的基本格式。总的来说,INI 文件以{Key}={Value}的形式定义配置项,{Value}可以定义在可选的双引号中(如果值的前后包括空白字符,就必须使用双引号,否则会被忽略)。

除了以{Key}={Value}的形式定义的原子配置项,我们还可以采用[{SectionName}]的形式定义配置节对它们进行分组。由于中括号([])是下一个配置节开始和上一个配置节结束的标志,所以采用 INI 文件定义的配置节并不存在层次化的结构,即没有子配置节的概念。除此之外,我们可以在INI文件中定义相应的注释,注释行前置的字符可以采用“;”、“#”或者“/”。

由于 INI 文件自身就体现为一个数据字典,所以可以采用“路径化”的 Key 来定义最终绑定为复杂对象、集合或者字典的配置数据。如果采用 INI文件定义一个 Profile对象的基本信息,我们也可以采用如下定义形式。

由于 Profile 的配置信息具有两个层次(Profile/ContactInfo),所以可以按照如下形式将EmailAddress和 PhoneNo定义在配置节 ContactInfo中,这个 INI文件在语义表达上和上面是完全等效的。

针对 INI 文件类型的配置源类型可以通过下面的 IniConfigurationSource 来表示,该类型定义在 NuGet包“Microsoft.Extensions.Configuration.Ini”中。IniConfigurationSource重写的 Build方法创建的是一个 IniConfigurationProvider 对象。作为抽象类 FileConfigurationProvider 的继承者,IniConfigurationProvider 利用重写的 Load 方法,来完成 INI 文件内容的读取和配置字典的初始化。

既然JsonConfigurationSource和XmlConfigurationSource的注册可以通过调用IConfiguration Builder接口的 AddJsonFile方法与 AddXmlFile方法来完成,那么 NuGet包“Microsoft.Extensions.Configuration.Ini”也会为IniConfigurationSource定义AddIniFile扩展方法。

6.5.5 StreamConfigurationSource

StreamConfigurationSource 对象通过指定的 Stream 对象来读取配置内容,所以这种配置源具有更加灵活的应用。如下面的代码片段所示,StreamConfigurationSource 是一个抽象类,用于读取配置内容的输出流体现在它的Stream属性中。

上面介绍了3种针对JSON文件、XML文件和INI文件的FileConfigurationSource类型,在它们所在的 NuGet 包中,还定义了针对 StreamConfigurationSource 的版本。如下面的代码片段所示,这些具体的 StreamConfigurationSource 类型通过重写的 Build 方法提供对应的IConfigurationProvider对象,然后由它们利用指定的 Stream对象读取对应的 JSON文件、XML文件和INI文本并转换成配置字典。针对上述3种具体的StreamConfigurationSource类型,我们可以调用如下3个对应的扩展方法来注册。

6.5.6 ChainedConfigurationSource

如下所示的 ChainedConfigurationSource 类型代表的配置源比较特殊,因为它承载的原始数据体现为一个 IConfiguration 对象。作为数据源的 IConfiguration 对象,它可以由 Chained ConfigurationSource 类型的 Configuration 属性来表示。实现的 Build 方法会返回对应的ChainedConfigurationProvider对象。除此之外,ChainedConfigurationSource类型还具有一个布尔类型的 ShouldDisposeConfiguration 属性,它决定了当提供的 ChainedConfigurationProvider 对象释放(调用其Dispose方法)的时候,指定的IConfiguration对象是否应该随之被释放。

虽然IConfiguration没有继承IDisposable接口,但是具体的实现类型(如表示配置树根节点的ConfigurationRoot类型)实现了IDisposable接口,所以当确定指定的IConfiguration对象没有使用时,我们应该调用其 Dispose 方法完成相应的释放和回收工作。如果指定的 IConfiguration对象的生命周期由创建的 ChainedConfigurationSource 控制,那么我们应该将 ShouldDispose Configuration属性设置为 True。如果该 IConfiguration对象的释放由其他对象负责,就应该将该属性设置为False。

上面是ChainedConfigurationProvider类型的完整定义。与上面介绍的IConfigurationProvider实现类型有所不同,ChainedConfigurationProvider 并没有继承自基类 ConfigurationProvider,而是直接利用提供的 IConfiguration 对象实现了 IConfigurationProvider 接口的所有成员。ChainedConfigurationSource 对象的注册可以通过如下所示的两个 AddConfiguration 扩展方法重载来完成。如果调用第一个方法重载,注册 ChainedConfigurationSource 对象的 ShouldDispose Configuration属性就被设置为False,这意味着提供IConfiguration对象的生命周期由外部控制。

6.5.7 自定义ConfigurationSource(S616)

前面对配置模型中默认提供的各种 IConfigurationSource 实现类型进行了深入详尽的介绍,如果它们依然无法满足项目中的需求,还可以通过自定义 IConfigurationSource 实现类型来支持我们希望的配置源。就配置数据的持久化方式来说,将配置存储在数据库中是一种常见的方式。下面将创建一个针对数据库的IConfigurationSource实现类型,它采用Entity Framework Core来完成数据库的存取操作。由于篇幅所限,笔者没有对 Entity Framework Core 相关的编程进行单独介绍,如果读者对此不太熟悉,可以查阅Entity Framework Core的在线文档。

我们将自定义ConfigurationSource命名为DbConfigurationSource。在正式介绍DbConfiguration Source的实现之前,下面先介绍它在项目中的应用。我们将配置保存在 SQL Server数据库中的某个数据表中,并采用 Entity Framework Core 来读取它。可以将连接字符串作为配置定义在一个名为appSettings.json的JSON文件中。

如下所示的演示程序先创建了一个 ConfigurationBuilder 对象,并在它上面注册了一个指向connectionString.json文件的JsonConfigurationSource对象。针对DbConfigurationSource对象的注册体现在 AddDatabase 扩展方法上,这个方法具有两个参数,分别代表连接字符串的名称和初始的配置数据。前者正是connectionString.json设置的连接字符串名称DefaultDb;后者是一个字典对象,它提供的原始配置正好可以构成一个 Profile对象。利用 ConfigurationBuilder创建出相应的IConfiguration对象之后,可以读取配置并将其绑定为一个Profile对象。

如上面的代码片段所示,针对 DbConfigurationSource 对象的应用仅仅体现在为IConfigurationBuilder 接口定义的 AddDatabase 扩展方法上,所以使用起来是非常方便的,那么这个扩展方法背后具有怎样的逻辑实现?DbConfigurationSource 采用 Entity Framework Core 并以 Code First的方式进行数据操作,如下所示的 ApplicationSetting是表示基本配置项的 POCO类型,可以将配置项的 Key 以小写的方式存储。而 ApplicationSettingsContext 是对应的DbContext类型。

如下所示的代码片段是 DbConfigurationSource 类型的定义,它的构造函数有两个参数:第一个参数的类型为Action<DbContextOptionsBuilder>,可以用这个委托对象对创建DbContext采用的 DbContextOptions 进行设置;第二个可选的参数用来指定一些需要自动初始化的配置项。DbConfigurationSource 类型在重写的 Build 方法中利用这两个参数创建一个 DbConfiguration Provider对象。

DbConfigurationProvider派生于抽象类 ConfigurationProvider。在重写的 Load方法中,它会根据提供的 Action<DbContextOptionsBuilder>创建 ApplicationSettingsContext对象,利用它从数据库中读取配置数据并转换成字典对象,该字典对象最终被赋值给代表配置字典的 Data属性。如果数据表中没有数据,Lood 方法还会利用这个 DbContext 对象将提供的初始化配置添加到数据库中。

实例演示中用来注册 DbConfigurationSource 对象的 AddDatabase 扩展方法具有如下定义。该方法首先调用IConfigurationBuilder对象的Build方法创建一个IConfiguration对象,并调用该IConfiguration 对象的 GetConnectionString 扩展方法根据指定的连接字符串名称得到完整的连接字符串。下面调用构造函数创建一个 DbConfigurationSource 对象,并将其注册到 Configuration Builder 对象上。创建 DbConfigurationSource 对象时指定的 Action<DbContextOptionsBuilder>会完成针对连接字符串的设置。


[1] 如果项目采用的SDK类型为“Microsoft.NET.Sdk”,该应用在Visual Studio中运行时会将编译输出目录作为当前目录。如果项目采用的SDK类型为“Microsoft.NET.Sdk.Web”,那么项目根目录就是当前执行的目录,此时不需要设置配置文件的“Copy to Output Directory”属性。

[2] 由于加载的是编译后复制到输出目录下的配置文件,在做一个实验的时候,修改的是当前工作目录下的配置文件,而不是当前项目根目录下的配置文件。