[ASP.NET Core 3框架揭秘] 配置[6]:多样化的配置源[上篇]

时间:2022-06-01 22:12:05

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

一、MemoryConfigurationSource

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

public class MemoryConfigurationSource : IConfigurationSource
{
public IEnumerable<KeyValuePair<string, string>> InitialData { get; set; }
public IConfigurationProvider Build(IConfigurationBuilder builder) => new MemoryConfigurationProvider(this);
}

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

public class MemoryConfigurationProvider : ConfigurationProvider,  IEnumerable<KeyValuePair<string, string>>
{
public MemoryConfigurationProvider(MemoryConfigurationSource source);
public void Add(string key, string value);
public IEnumerator<KeyValuePair<string, string>> GetEnumerator();
IEnumerator IEnumerable.GetEnumerator();
}

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

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

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

public static class MemoryConfigurationBuilderExtensions
{
public static IConfigurationBuilder AddInMemoryCollection( this IConfigurationBuilder configurationBuilder);
public static IConfigurationBuilder AddInMemoryCollection( this IConfigurationBuilder configurationBuilder, IEnumerable<KeyValuePair<string, string>> initialData);
}

二、EnvironmentVariablesConfigurationSource

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

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

public static class Environment
{
public static string GetEnvironmentVariable(string variable);
public static string GetEnvironmentVariable(string variable, EnvironmentVariableTarget target); public static IDictionary GetEnvironmentVariables();
public static IDictionary GetEnvironmentVariables( EnvironmentVariableTarget target); public static void SetEnvironmentVariable(string variable, string value);
public static void SetEnvironmentVariable(string variable, string value, EnvironmentVariableTarget target);
} public enum EnvironmentVariableTarget
{
Process,
User,
Machine
}

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

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

[ASP.NET Core 3框架揭秘] 配置[6]:多样化的配置源[上篇]

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

public class EnvironmentVariablesConfigurationSource : IConfigurationSource
{
public string Prefix { get; set; }
public IConfigurationProvider Build(IConfigurationBuilder builder)=> new EnvironmentVariablesConfigurationProvider(Prefix);
}

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

public class EnvironmentVariablesConfigurationProvider : ConfigurationProvider
{
private readonly string _prefix;
public EnvironmentVariablesConfigurationProvider(string prefix = null) => _prefix = prefix ?? string.Empty;
public override void Load()
{
var dictionary = Environment.GetEnvironmentVariables()
.Cast<DictionaryEntry>()
.Where(it => it.Key.ToString().StartsWith( _prefix, StringComparison.OrdinalIgnoreCase))
.ToDictionary(it => it.Key.ToString().Substring(_prefix.Length), it => it.Value.ToString());
Data = new Dictionary<string, string>( dictionary, StringComparer.OrdinalIgnoreCase);
}
}

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

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

public static class EnvironmentVariablesExtensions
{
public static IConfigurationBuilder AddEnvironmentVariables( this IConfigurationBuilder configurationBuilder);
public static IConfigurationBuilder AddEnvironmentVariables( this IConfigurationBuilder builder, Action<EnvironmentVariablesConfigurationSource> configureSource);
public static IConfigurationBuilder AddEnvironmentVariables( this IConfigurationBuilder configurationBuilder, string prefix);
}

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

public class Program
{
public static void Main()
{
Environment.SetEnvironmentVariable("TEST_GENDER", "Male");
Environment.SetEnvironmentVariable("TEST_AGE", "");
Environment.SetEnvironmentVariable("TEST_CONTACTINFO:EMAILADDRESS", "foobar@outlook.com");
Environment.SetEnvironmentVariable("TEST_CONTACTINFO:PHONENO", ""); var profile = new ConfigurationBuilder()
.AddEnvironmentVariables("TEST_")
.Build()
.Get<Profile>(); Debug.Assert(profile.Equals(new Profile(Gender.Male, , "foobar@outlook.com", "")));
}
}

三、CommandLineConfigurationSource对象

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

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

exec app {options}

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

{name}={value}
{prefix}{name}={value}

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

exec app architecture=x64
exec app /architecture=x64
exec app --architecture=x64

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

exec app /architecture x64
exec app –-architecture x64

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

exec app -a=x64
exec app -arch=x64
exec app -a x64
exec app -arch x64

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

public class CommandLineConfigurationSource : IConfigurationSource
{
public IEnumerable<string> Args { get; set; }
public IDictionary<string, string> SwitchMappings { get; set; } public IConfigurationProvider Build(IConfigurationBuilder builder) => new CommandLineConfigurationProvider( Args,SwitchMappings);
}

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

public class CommandLineConfigurationProvider : ConfigurationProvider
{
protected IEnumerable<string> Args { get; }
public CommandLineConfigurationProvider(IEnumerable<string> args, IDictionary<string, string> switchMappings = null);
public override void Load();
}

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

public static class CommandLineConfigurationExtensions
{
public static IConfigurationBuilder AddCommandLine( this IConfigurationBuilder builder, Action<CommandLineConfigurationSource> configureSource);
public static IConfigurationBuilder AddCommandLine( this IConfigurationBuilder configurationBuilder, string[] args);
public static IConfigurationBuilder AddCommandLine( this IConfigurationBuilder configurationBuilder, string[] args, IDictionary<string, string> switchMappings);
}

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

class Program
{
static void Main(string[] args)
{
try
{
var mapping = new Dictionary<string, string>
{
["-a"] = "architecture",
["-arch"] = "architecture"
};
var configuration = new ConfigurationBuilder()
.AddCommandLine(args, mapping)
.Build();
Console.WriteLine($"Architecture: {configuration["architecture"]}");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}

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

[ASP.NET Core 3框架揭秘] 配置[6]:多样化的配置源[上篇]

[ASP.NET Core 3框架揭秘] 配置[1]:读取配置数据[上篇]
[ASP.NET Core 3框架揭秘] 配置[2]:读取配置数据[下篇]
[ASP.NET Core 3框架揭秘] 配置[3]:配置模型总体设计
[ASP.NET Core 3框架揭秘] 配置[4]:将配置绑定为对象
[ASP.NET Core 3框架揭秘] 配置[5]:配置数据与数据源的实时同步
[ASP.NET Core 3框架揭秘] 配置[6]:多样化的配置源[上篇]
[ASP.NET Core 3框架揭秘] 配置[7]:多样化的配置源[中篇]
[ASP.NET Core 3框架揭秘] 配置[8]:多样化的配置源[下篇]
[ASP.NET Core 3框架揭秘] 配置[9]:自定义配置源