.NET Core采用的全新配置系统[5]: 聊聊默认支持的各种配置源[内存变量,环境变量和命令行参数]
较之传统通过App.config和Web.config这两个XML文件承载的配置系统,.NET Core采用的这个全新的配置模型的最大一个优势就是针对多种不同配置源的支持。我们可以将内存变量、命令行参数、环境变量和物理文件作为原始配置数据的来源,如果采用物理文件作为配置源,我们可以选择不同的格式(比如XML、JSON和INI等) 。如果这些默认支持的配置源形式还不能满足你的需求,我们还可以通过注册自定义ConfigurationSource的方式将其他形式数据作为我们的配置来源。 [ 本文已经同步到《ASP.NET Core框架揭秘》之中]
目录
一、内存变量
二、环境变量
三、命令行参数
一、内存变量
从本被系列第一篇开始到现在,我们所有的实例演示一直都在使用MemoryConfigurationSource这种类型的ConfigurationSource来提供原始的配置。我们知道MemoryConfigurationSource采用一个字典对象(具体来说应该是一个元素类型为KeyValuePair<string, string>的集合)作为存放原始配置数据的容器。作为一个ConfigurationSource,它总是通过创建某个对应的ConfigurationProvider来从事具体的配置数据读取工作,那么MemoryConfigurationSource会提供一个怎样的ConfigurationProvider呢?
1: public class MemoryConfigurationSource : IConfigurationSource
2: {
3: public IEnumerable<KeyValuePair<string, string>> InitialData { get; set; }
4:
5: public IConfigurationProvider Build(IConfigurationBuilder builder)
6: {
7: return new MemoryConfigurationProvider(this);
8: }
9: }
上面给出的代码片段体现了MemoryConfigurationSource的完整定义,我们可以看到它具有一个IEnumerable<KeyValuePair<string, string>>类型的属性InitialData来存放初始的配置数据。从Build方法的实现可以看出,真正被它用来读取原始配置数据的是一个MemoryConfigurationProvider类型的对象,该类型的定义如下面的代码片段所示。
1: public class MemoryConfigurationProvider : ConfigurationProvider, IEnumerable<KeyValuePair<string, string>>
2: {
3: public MemoryConfigurationProvider(MemoryConfigurationSource source);
4: public void Add(string key, string value);
5: public IEnumerator<KeyValuePair<string, string>> GetEnumerator();
6: IEnumerator IEnumerable.GetEnumerator();
7: }
从上面的代码片段可以看出,MemoryConfigurationProvider派生于抽象类ConfigurationProvider,同时还实现了IEnumerable<KeyValuePair<string, string>>接口。我们知道ConfigurationProvider直接使用一个Dictionary<string, string>来保存配置数据,当我们根据一个MemoryConfigurationSource对象调用构造函数创建MemoryConfigurationProvider的时候,它只需要将通过InitiateData属性保存的配置数据转移到这个字典中即可。MemoryConfigurationProvider还定义了一个Add方法是我们可以在任何时候都可以向配置字典中添加一个新的配置项。
通过前面对配置模型的介绍,我们知道ConfigurationProvider在配置模型中所起的作用就是读取原始的配置数据并将其转换成配置字典。在所有的预定义的ConfigurationProvider类型中,MemoryConfigurationProvider最为简单直接,因为它对应的配置源就是一个配置字典,所以根本不需要作任何的结构转换。
在利用MemoryConfigurationSource生成配置的时候,我们需要将它注册到ConfigurationBuilder之上。具体来说,我们可以像前面演示的实例一样直接调用ConfigurationBuilder的Add方法,也可以调用如下所示的了两个重载的扩展方法AddInMemoryCollection。
1: public static IConfigurationBuilder AddInMemoryCollection(this IConfigurationBuilder configurationBuilder);
2: public static IConfigurationBuilder AddInMemoryCollection(this IConfigurationBuilder configurationBuilder, IEnumerable<KeyValuePair<string, string>> initialData);
二、环境变量
顾名思义,环境变量就是描述当前执行环境并影响进程执行行为的变量。按照作用域的不同,我们将环境变量划分成三类,即分别针对当前系统、当前用户和当前进程的环境变量。系统和用户级别的环境变量保存在注册表中,其路径分别为“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:
6: public static IDictionary GetEnvironmentVariables();
7: public static IDictionary GetEnvironmentVariables(EnvironmentVariableTarget target);
8:
9: public static void SetEnvironmentVariable(string variable, string value);
10: public static void SetEnvironmentVariable(string variable, string value, EnvironmentVariableTarget target);
11: }
12:
13: public enum EnvironmentVariableTarget
14: {
15: Process,
16: User,
17: Machine
18: }
环境变量的添加、修改和删除均由SetEnvironmentVariable方法来完成,如果没有显式指定target参数,默认采用的是EnvironmentVariableTarget.Process。如果希望删除指定名称的环境变量,我们只需要在调用这个方法的时候将value参数设置为Null或者空字符串即可。
除了在程序中利用静态类型Environment,我们还可以执行命令行的方式查看和设置环境变量。除此之外,我们还可以利用“系统属性(System Properties)”设置工具以可视化的方式查看和设置系统和用户级别的环境变量(“This PC”>“Properties”>“Change Settings”>“Advanced”>“Environment Variables”)。如果采用Visual Studio 2015来调试我们编写的应用,我们可以设置项目属性的方式来设置进程级别的环境变量( “Properties” > “Debug”> “Environment Variables” )。
针对环境变量的配置源通过如下一个 EnvironmentVariablesConfigurationSource类型来表示,该类型定义在NuGet包“Microsoft.Extensions.Configuration.EnvironmentVariables”之中。该类型指定义了一个字符串类型的属性Prefix,它表示用于筛选环境变量采用的前缀,也就是说如果我们设置了这个Prefix属性,只会选择名称以此作为前缀的环境变量。
1: public class EnvironmentVariablesConfigurationSource : IConfigurationSource
2: {
3: public string Prefix { get; set; }
4: public IConfigurationProvider Build(IConfigurationBuilder builder)
5: {
6: return new EnvironmentVariablesConfigurationProvider(this.Prefix);
7: }
8: }
通过上面给出的代码片段我们可以看出EnvironmentVariablesConfigurationSource会利用对应的EnvironmentVariablesConfigurationProvider来完成对环境变量的读取工作。如下所示的代码基本体现了EnvironmentVariablesConfigurationProvider的定义。由于作为原始配置数据的环境变量本身就是一个Key和Value均为字符串的数据字典,所以EnvironmentVariablesConfigurationProvider无需在进行结构转换,所以当Load方法被执行之后,它只需要将符合条件筛选出来并添加到自己的配置字典中即可。值得一提的是,如果我们在创建EnvironmentVariablesConfigurationProvider对象是指定了用于筛选环境变量的前缀,当符合条件的环境变量被添加到自身的配置字典之后,这个前缀也会从元素的Key中剔除。这个细节也体现在上面定义的Load方法中。
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: }
在使用EnvironmentVariablesConfigurationSource的时候,我们可以调用Add方法将它注册到指定的ConfigurationBuilder对象上。除此之外,EnvironmentVariablesConfigurationSource的中注册还可以直接调用IConfigurationBuilder接口的如下两个重载的扩展方法AddEnvironmentVariables来完成。
1: public static IConfigurationBuilder AddEnvironmentVariables(this IConfigurationBuilder configurationBuilder);
2: public static IConfigurationBuilder AddEnvironmentVariables(this IConfigurationBuilder configurationBuilder, string prefix);
我们照例编写一个简单的实例来演示如何采用环境变量作为配置源。如下面的代码片段所示,我们调用Environment的静态方法SetEnvironment方法设置了四个环境变量,变量名称具有相同的前缀“TEST_”。我们调用方法AddEnvironmentVariables创建一个EnvironmentVariablesConfigurationSource对象并将其注册到创建的ConfigurationBuilder之上。调用AddEnvironmentVariables方法是我们将环境变量名称前缀“TEST_” 作为参数。后续的代码我们已经很熟悉了,即采用Options模式读取环境变量并绑定为一个Profile对象。
1: Environment.SetEnvironmentVariable("TEST_gender", "Male");
2: Environment.SetEnvironmentVariable("TEST_age", "18");
3: Environment.SetEnvironmentVariable("TEST_contactInfo:emailAddress", "foobar@outlook.com");
4: Environment.SetEnvironmentVariable("TEST_contactInfo:PhoneNo", "123456789");
5:
6: IConfiguration config = new ConfigurationBuilder()
7: .AddEnvironmentVariables("TEST_")
8: .Build();
9:
10: Profile profile = new ServiceCollection()
11: .AddOptions()
12: .Configure<Profile>(config)
13: .BuildServiceProvider()
14: .GetService<IOptions<Profile>>()
15: .Value;
三、命令行参数
在很多情况下,我们会采用Self-Host的方式将一个ASP.NET Core应用寄宿一个托管进程中,在这种情况下我们倾向于采用命令行的方式来启动寄宿程序。当以命令行的形式启动一个ASP.NET Core应用时,我们希望直接使用命名行开关(Switch)来控制应用的一些行为,所以命令行开关自然也就成为了配置常用的来源之一。配置模型针对这种配置源的支持是通过CommandLineConfigurationSource来实现的,该类型定义在NuGet包 “Microsoft.Extensions.Configuration.CommandLine”中。
在以命令行的形式执行某个命令的时候,命令行开关(包括名称和值)体现为一个简单的字符串集合,所以CommandLineConfigurationSource的根本目的在于将命名行开关从字符串数组转换成配置字典。要充分理解这个转换规则,我们先得来了解一下CommandLineConfigurationSource支持的命令行开关究竟采用怎样的形式来指定。我们通过一个简单的实例来说明命令行开关的集中指定方式。假设我们有一个命令“exec”并采用如下所示的方式执行某个托管程序(app)。
1: exec app {options}
在执行这个命令的时候我们通过相应的命令行开关指定两个选项,其中一个表示采用的CPU架构(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
为了执行上的便利,很多命名行开关都具有缩写的形式,命令行开关的全名和缩写之间具有一个映射关系(Switch Mapping)。以上述的这两个命令行开关为例,我们可以采用首字母“a”和“r”来代表作为全名的“architecture”和“runtime”。如果采用缩写的命令行开关名称,那么我们就可以按照如下两种方式指定CPU架构和运行时类型。
1: exec app –-a x64 –-r coreclr
2: exec app -a x64 -r coreclr
综上所示,我们一共有五种指定命名行开关的方式,其中三种采用命令行开关的全名,余下的两种则使用命令行开关的缩写形式。下表总结了这五种命名开关的指定形式所采用的原始参数以及缩写与全名的映射关系。这里隐藏着一个重要的细节,字符 “-” 只能以缩写的形式指定命令行开关的指,但是 “--” 则支持全称和缩写形式。
Arguments |
Switch Mapping |
/architecture x64 /runtime coreclr |
- |
--architecture x64 --runtime coreclr |
- |
architecture=x64 runtime=coreclr |
- |
--a x64 --r coreclr |
--a: architecture, --r: runtime |
-a x64 -r coreclr |
-a: architecture, -r: runtime |
原始的命令行参数总是体现为一个字符串数组, CommandLineConfigurationSource以字符串数组作为配置源,并利用对应的ConfigurationProvider将它转换成配置字典。如下面的代码片断所示,CommandLineConfigurationSource具有Args和SwitchMappings,前者正式代表承载着原始命令行参数的字符串数组,后者则保存了命令行开关的缩写与全称之间的映射关系。在实现的Build方法中,它根据这两个属性创建出一个CommandLineConfigurationProvider对象。
1: public class CommandLineConfigurationSource : IConfigurationSource
2: {
3: public IEnumerable<string> Args { get; set; }
4: public IDictionary<string, string> SwitchMappings { get; set; }
5:
6: public IConfigurationProvider Build(IConfigurationBuilder builder)
7: {
8: return new CommandLineConfigurationProvider(this.Args, this.SwitchMappings);
9: }
10: }
具有如下定义的CommandLineConfigurationProvider依然是抽象类ConfigurationProvider的继承者。它的目的很明确,就是对体现为字符串数组的原始命令行参数进行解析,并将解析出来参数名称和值添加到配置字典中 。这一切都是在重写的Load方法中完成的。
1: public class CommandLineConfigurationProvider : ConfigurationProvider
2: {
3: protected IEnumerable<string> Args { get; }
4: public CommandLineConfigurationProvider(IEnumerable<string> args, IDictionary<string, string> switchMappings = null);
5: public override void Load();
6: }
在采用基于命令行参数作为配置源的时候,我们可以创建一个CommandLineConfigurationSource并将其注册到ConfigurationBuilder之上。我们也可以调用IConfigurationBuilder接口的如下两个扩展方法AddCommandLine将两个步骤合二为一。
1: public static IConfigurationBuilder AddCommandLine(this IConfigurationBuilder configurationBuilder, string[] args);
2: public static IConfigurationBuilder AddCommandLine(this IConfigurationBuilder configurationBuilder, string[] args, IDictionary<string, string> switchMappings);
为了让读者朋友们对CommandLineConfigurationProvider解析命令行参数采用的策略具有一个深刻的认识,我们来演示一个简单的实例。我们创建一个控制台应用,并添加针对 “Microsoft.Extensions.Configuration.CommandLine”这个NuGet包的依赖。在Main方法中,我们编写了如下一段简单的实例程序。
1: while (true)
2: {
3: try
4: {
5: Console.Write("Enter command line switches:");
6: string arguments = Console.ReadLine();
7: Dictionary<string, string> mapping = new Dictionary<string, string>
8: {
9: ["--a"] = "architecture ",
10: ["-a"] = "architecture ",
11: ["--r"] = "runtime",
12: ["-r"] = "runtime",
13: };
14: IConfiguration config = new ConfigurationBuilder()
15: .AddCommandLine(arguments.Split(' '), mapping)
16: .Build();
17:
18: foreach (var section in config.GetChildren())
19: {
20: Console.WriteLine($"{section.Key}: {section.Value}");
21: }
22: }
23: catch(Exception ex)
24: {
25: Console.WriteLine(ex.Message);
26: }
27: }
如上面的代码片断所示,我们在一个无限循环中接收用户指定的命令行参数,并据此创建一个CommandLineConfigurationSource对象并将其注册到ConfigurationBuilder之上。我们在创建这个CommandLineConfigurationSource对象的时候,还指定一个表示命令行开关映射关系的字典。接下来我们利用这个ConfigurationBuilder生成一个Configuration对象,并将其所有子配置节的Key和Value打印出来。我们运行该程序后分别采用上述五种方式提供了命令行参数,根据如下所示的输出结果,会发现解析命令行参数生成的配置是完全等效的。