ASP.NET Core的配置(4):多样性的配置来源[上篇]

较之传统通过App.config和Web.config这两个XML文件承载的配置系统,ASP.NET Core采用的这个全新的配置模型的最大一个优势就是针对多种不同配置源的支持。我们可以将内存变量、命令行参数、环境变量和物理文件作为原始配置数据的来源,如果采用物理文件作为配置源,我们可以选择不同的格式,比如XML、JSON和INI等。如果这些默认支持的配置源形式还不能满足你的需求,我们还可以通过注册自定义ConfigurationProvider的方式将其他形式数据作为我们的配置来源。接下来就让我们来逐个认识一下配置模型原生提供的ConfigurationProvider。

目录
MemoryConfigurationProvider
EnvironmentVariablesConfigurationProvider
CommandLineConfigurationProvider
JsonConfigurationProvider
XmlConfiguationProvider
IniConfigurationProvider
自定义ConfigurationProvider

一、MemoryConfigurationProvider

通过本章第2节对配置模型的介绍,我们知道ConfigurationProvider在配置模型中所起的作用就是读取原始的配置数据并将其转换成基于数据字典的物理结构。在所有的ConfigurationProvider类型中,MemoryConfigurationProvider最为简单直接,因为它对应的配置源就是一个数据字典,根本不需要作任何的结构转换。

MemoryConfigurationProvider定义在“Microsoft.Extensions.Configuration.Memory”命名空间下。。如下面的代码片段所示,派生于基类ConfigurationProvider的MemoryConfigurationProvider同时实现了IEnumerable<KeyValuePair<string, string>>接口,所以它自身可以作为一个字典对象来使用。原始的配置数据可以在创建MemoryConfigurationProvider的时候作为构造函数的参数来指定,也可以通过调用Add方法逐个进行添加。

a
 1: public class MemoryConfigurationProvider : ConfigurationProvider, IEnumerable<KeyValuePair<string, string>>
 2: {
 3:     public MemoryConfigurationProvider();
 4:     public MemoryConfigurationProvider(IEnumerable<KeyValuePair<string, string>> initialData);
 5:  
 6:     public void Add(string key, string value);
 7:     public IEnumerator<KeyValuePair<string, string>> GetEnumerator();
 8: }

在使用的时候,我们需要将MemoryConfigurationProvider对象注册到ConfigurationBuilder之上。具体来说,我们可以像前面演示的实例一样直接调用ConfigurationBuilder的Add方法,也可以调用如下所示的扩展方法AddInMemoryCollection。

 1: public static class MemoryConfigurationExtensions
 2: { 
 3:     public static IConfigurationBuilder AddInMemoryCollection(this IConfigurationBuilder configurationBuilder); 
 4:     public static IConfigurationBuilder AddInMemoryCollection(this IConfigurationBuilder configurationBuilder, IEnumerable<KeyValuePair<string, string>> initialData);
 5: }


二、EnvironmentVariablesConfigurationProvider

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

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

 1: public static class Environment
 2: { 
 3:     public static string GetEnvironmentVariable(string variable);
 4:     public static string GetEnvironmentVariable(string variable, EnvironmentVariableTarget target);
 5:     public static IDictionary GetEnvironmentVariables();
 6:     public static IDictionary GetEnvironmentVariables(EnvironmentVariableTarget target); 
 7: 
 8:     public static void SetEnvironmentVariable(string variable, string value);
 9:     public static void SetEnvironmentVariable(string variable, string value, EnvironmentVariableTarget target);
 10: }
 11:  
 12: public enum EnvironmentVariableTarget
 13: {
 14:     Process,
 15:     User,
 16:     Machine
 17: }

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

借助EnvironmentVariablesConfigurationProvider,我们可以将环境变量作为配置源。该类型定义在“Microsoft.Extensions.Configuration.EnvironmentVariables”程序集中,程序集的名称同时也是所在NuGet包的名称。如下面的代码片段所示,EnvironmentVariablesConfigurationProvider具有两个构造函数重载,如果调用默认无参构造函数,意味着我们会使用所有的环境变量。另一个构造函数提供了一个字符串类型的参数prefix,如果调用这个构造函数来创建一个EnvironmentVariablesConfigurationProvider,意味着我们只会使用名称以此为前缀的环境变量。

 1: public class EnvironmentVariablesConfigurationProvider : ConfigurationProvider
 2: { 
 3:     public EnvironmentVariablesConfigurationProvider();
 4:     public EnvironmentVariablesConfigurationProvider(string prefix);
 5:     public override void Load();
 6: }

由于作为原始配置数据的环境变量本身就是一个Key和Value均为字符串的数据字典,所以EnvironmentVariablesConfigurationProvider无需在进行结构转换,所以当Load方法被执行之后,它只需要将符合条件筛选出来并添加到自己的配置字典中即可。有一点值得一提的是,如果我们在创建EnvironmentVariablesConfigurationProvider对象是指定了用于筛选环境变量的前缀,当符合条件的环境变量被添加到自身的配置字典之后,这个前缀也会从元素的Key中剔除。如下所示的代码片段基本上体现了EnvironmentVariablesConfigurationProvider的实现逻辑。

 1: public class EnvironmentVariablesConfigurationProvider : ConfigurationProvider
 2: {
 3:     private readonly string prefix;
 4:  
 5:     public EnvironmentVariablesConfigurationProvider(string prefix = null)
 6:     {
 7:         this.prefix = prefix ?? string.Empty;
 8:     }
 9:  
 10:     public override void Load()
 11:     {
 12:         var dictionary = Environment.GetEnvironmentVariables()
 13:             .Cast<DictionaryEntry>()
 14:             .Where(it => it.Key.ToString().StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
 15:             .ToDictionary(it => it.Key.ToString().Substring(prefix.Length), it => it.Value.ToString());
 16:         this.Data = new Dictionary<string, string>(dictionary, StringComparer.OrdinalIgnoreCase);
 17:    }
 18: }

也正是因为环境变量自身是数据字典,所以我们可以采用路径化的变量名定义一组相关的环境变量来提供一个复杂对象、集合或者字典对象的配置数据。如下面的代码片段所示,我们采用这样的方式将绑定为一个Profile对象的基本信息定义成一组相关的环境变量。由于这组环境变量名称具有相同的前缀“Profile”,所以我们利用这个前缀来创建一个 EnvironmentVariablesConfigurationProvider对象。在将它添加到ConfigurationBuilder之后,我们是用后者生成的Configuration对象采用配置绑定的方式得到一个Profile对象。

 1: Environment.SetEnvironmentVariable("Profile:Gender", "Male");
 2: Environment.SetEnvironmentVariable("Profile:Age", "18");
 3: Environment.SetEnvironmentVariable("Profile:ContactInfo:Email", "foobar@outlook.com");
 4: Environment.SetEnvironmentVariable("Profile:ContactInfo:PhoneNo", "123456789");
 5:  
 6: Profile profile = new ConfigurationBuilder()
 7:     .Add(new EnvironmentVariablesConfigurationProvider("Profile:"))
 8:     .Build()
 9:     .Get<Profile>();

在使用EnvironmentVariablesConfigurationProvider的时候,我们可以按照上面演示的方式显式地调用Add方法将创建的EnvironmentVariablesConfigurationProvider对象注册到指定的ConfigurationBuilder对象之外,也可以直接调用如下所示的扩展方法AddEnvironmentVariables。

 1: public static class EnvironmentVariablesExtensions
 2: {
 3:     public static IConfigurationBuilder AddEnvironmentVariables(this IConfigurationBuilder configurationBuilder);
 4:     public static IConfigurationBuilder AddEnvironmentVariables(this IConfigurationBuilder configurationBuilder, string prefix);
 5: }


三、CommandLineConfigurationProvider

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

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

 1: exec app {options} 

在执行这个命令的时候我们通过相应的命令行开关指定两个选项,其中一个表示采用的CPUI架构(X86或者X64),另一个表示运行时类型(CLR或者CoreCLR),我们将这两个命令行开关分别命名为architecture和runtime。在执行命名行的时候,我们可以采用如下三种不同的方式指定这两个命名行开关。

 1: exec app /architecture x64 /runtime coreclr 
 2: exec app --architecture x64 --runtime coreclr 
 3: exec app architecture=x64 architecture=coreclr 

为了执行上的便利,很多命名行开关都具有缩写的形式。以上述的这两个命令行开关为例,我们可以采用首字母“a”和“r”来代表作为全名的“architecture”和“runtime”。如果采用缩写的命令行开关名称,那么我们就可以按照如下两种方式指定CPU架构和运行时类型。

 1: exec app –-a x64 –-r coreclr 
 2: exec app -a x64 -r coreclr 

综上所示,我们一共有五种指定命名行开关的方式,其中三种采用命令行开关的全名,余下的两种则使用命令行开关的缩写形式。这五种命名开关的指定形式所采用的原始参数以及缩写与全名的映射关系。

ASP.NET Core的配置(4):多样性的配置来源[上篇]

在对命令行开关的集中指定形式具有基本了解之后,我们来认识一下将它们引入配置模型并作为配置源数据来源的CommandLineConfigurationProvider。如下面的代码片断所示,我们需要以字符串集合的形式指定原始的命令行参数来创建一个CommandLineConfigurationProvider对象,只读属性Args返回的也正是这个集合。

 1: public class CommandLineConfigurationProvider : ConfigurationProvider
 2: {
 3:     public CommandLineConfigurationProvider(IEnumerable<string> args, IDictionary<string, string> switchMappings = null);
 4:     public override void Load();
 5:  
 6:     protected IEnumerable<string> Args { get; }
 7: }

构造函数另一个字典类型的参数switchMappings用于指定命令行开关名称的缩写形式与全名的映射关系。一个命令行开关可以包含多个不同的缩写形式,比如“architecture”可以缩写成“a”,也和缩写成“arch”。如果采用缩写形式,指定的命名行开关名称必须以“-”或者“--”为前缀,那么这个switchMappings参数对应字典对象中的Key也需要采用相应的前缀

在使用CommandLineConfigurationProvider的时候,我们可以直接创建这个对象并调用Add方法将其添加到指定的ConfigurationBuilder之中。我们可以直接调用ConfigurationBuilder对象具有如下定义的两个扩展方法AddCommandLine达到相同的目的。

 1: public static class CommandLineConfigurationExtensions
 2: {
 3:     public static IConfigurationBuilder AddCommandLine(this IConfigurationBuilder configurationBuilder, string[] args);
 4:     public static IConfigurationBuilder AddCommandLine(this IConfigurationBuilder configurationBuilder, string[] args,IDictionary<string, string> switchMappings);
 5: }

我们照例通过通过一个简单的实例来演示如何利用CommandLineConfigurationProvider将命令行开关作为配置的原始来源。如下面的代码片断所示,在静态方法GetConfigurations中,我们按照上面表格所示的五种方式创建了以命名行参数作为来源的Configuration对象。为了验证这五种命名行开关指定形式的等效性,我们从中提取配置项“architecture”和“runtime”并验证它们的值。

   1: public class Program
   2: {
   3:     public static void Main(string[] args)
   4:     {
   5:         foreach (IConfiguration configuration in GetConfigurations())
   6:         {
   7:             Debug.Assert(configuration["architecture"] == "x64");
   8:             Debug.Assert(configuration["runtime"] == "coreclr");
   9:         }
  10:     }
  11:  
  12:     private static IEnumerable<IConfiguration> GetConfigurations()
  13:     {
  14:         yield return new ConfigurationBuilder()
  15:          .AddCommandLine(new string[] { "/architecture", "x64", "/runtime", "coreclr" })
  16:          .Build();
  17:  
  18:         yield return new ConfigurationBuilder()
  19:         .AddCommandLine(new string[] { "--architecture", "x64", "--runtime", "coreclr" })
  20:         .Build();
  21:  
  22:         yield return new ConfigurationBuilder()
  23:         .AddCommandLine(new string[] { "architecture=x64", "runtime=coreclr" })
  24:         .Build();
  25:  
  26:         yield return new ConfigurationBuilder()
  27:         .AddCommandLine(new string[] { "--a", "x64", "--r", "coreclr" }, new Dictionary<string, string>
  28:         {
  29:             ["--a"] = "architecture",
  30:             ["--r"] = "runtime"
  31:         }).Build();
  32:  
  33:         yield return new ConfigurationBuilder()
  34:         .AddCommandLine(new string[] { "-a", "x64", "-r", "coreclr" }, new Dictionary<string, string>
  35:         {
  36:             ["-a"] = "architecture",
  37:             ["-r"] = "runtime"
  38:         }).Build();
  39:     }
  40: }

考虑到命名行的使用场景,我们一般情况下只利用命令行开关来提供单一的配置项,很少将其邦定为一个结构化的Options对象。不过命名行开关虽然以字符串集合的形式体现,但是它们可以直接映射为配置字典,所以我们完全可以通过采用路径化的命令行开关(比如“/foo:bar:baz abc”)来提供最终绑定为复杂对象设置集合和字典的配置源。

ASP.NET Core的配置(1):读取配置信息
ASP.NET Core的配置(2):配置模型详解
ASP.NET Core的配置(3): 将配置绑定为对象[上篇]
ASP.NET Core的配置(3): 将配置绑定为对象[下篇]
ASP.NET Core的配置(4):多样性的配置源[上篇]
ASP.NET Core的配置(4):多样性的配置源[中篇]
ASP.NET Core的配置(4):多样性的配置源[下篇]
ASP.NET Core的配置(5):配置的同步[上篇]
ASP.NET Core的配置(5):配置的同步[下篇]


作者:蒋金楠
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
上一篇:ASP.NET Core的配置(4):多样性的配置来源[下篇]


下一篇:ASP.NET Core的配置(4):多样性的配置来源[中篇]