5.1 读取配置信息
.NET 的配置支持多样化的数据源。内存变量、环境变量、命令行参数及各种格式的配置文件都可以作为配置的数据来源。
5.1.1 编程模型三要素
从编程层面来看,.NET 的配置系统由 3 个核心对象构成。
供应用程序使用的配置体现为 IConfiguration 对象,IConfigurationSource 是对数据源的抽象。整个配置系统需要解决的是如何将不同的 IConfigurationSource 对象提供的数据转换为 IConfiguration 对象,这个转换过程由 IConfigurationBuilder 对象完成。
以上 3 个接口都定义在“Microsoft.Extensions.Configuration.Abstractions” NuGet 包中,具体实现定义在“Microsoft.Extensions.Configuration” NuGet 包中。
5.1.2 以“键-值”对的形式读取配置
创建 IConfiguration 对象的正确编程方式:首先,创建一个 ConfigurationBuilder 对象,并为之注册一个或者多个 IConfigurationSource 对象,然后利用它创建 IConfiguration 对象。
创建一个设定日期/时间的显示格式的类:
1 namespace MM.Martin.Configuration; 2 3 using Microsoft.Extensions.Configuration; 4 5 public class DateTimeFormatOptions{ 6 public string LongDatePattern {get;set;} 7 8 public string LongTimePattern {get;set;} 9 10 public string ShortDatePattern {get;set;} 11 12 public string ShortTimePattern {get;set;} 13 14 public DateTimeFormatOptions(IConfiguration configuration){ 15 LongDatePattern = configuration["LongDatePattern"]!; 16 LongTimePattern = configuration["LongTimePattern"]!; 17 ShortDatePattern = configuration["ShortDatePattern"]!; 18 ShortTimePattern = configuration["ShortTimePattern"]!; 19 } 20 }
接下来通过创建 IConfiguration 对象给这个类赋值:
1 IConfiguration configuration = new ConfigurationBuilder() 2 .Add(new MemoryConfigurationSource{ InitialData = source! }) 3 .Build(); 4 5 var options = new DateTimeFormatOptions(configuration);
从代码中可以注意到,内存配置源我们使用的是 MemoryConfigurationSource。
5.1.3 读取结构化的配置
将保持属性层次化结构的配置称为“配置树”,一个 IConfiguration 对象正好是这棵配置树的某个节点的描述,整棵配置树的根节点由对应的 IConfiguration 对象表示。
通过一个 Dictionary 对配置树进行扁平化处理,然后通过 ConfigurationBuilder#AddInMemoryCollection 方法加进配置中。
1 var source = new Dictionary<string, string>{ 2 ["format:dateTime:longDatePattern"] = "dddd, MMMM d, yyyy", 3 ["format:dateTime:longTimePattern"] = "h:mm:ss tt", 4 ["format:dateTime:shortDatePattern"] = "M/d/yyyy", 5 ["format:dateTime:shortTimePattern"] = "h:mm tt", 6 7 ["format:currencyDecimal:digits"] = "5", 8 ["format:currencyDecimal:symbol"] = "$" 9 }; 10 11 var configuration = new ConfigurationBuilder() 12 .AddInMemoryCollection(source) 13 .Build();
FormatOptions 类中包含了两个保存显示格式的类,一个保存金额格式,一个保存日期/时间格式。把这些格式放在一个配置中,通过 IConfiguration#GetSection 方法获取对应的值。这里传给构造函数的参数
1 namespace MM.Martin.Configuration; 2 3 using Microsoft.Extensions.Configuration; 4 5 public class FormatOptions{ 6 public CurrencyFormatOptions CurrencyFormat {get;set;} 7 8 public DateTimeFormatOptions DateTimeFormat {get;set;} 9 10 public FormatOptions(IConfiguration configuration){ 11 CurrencyFormat = new CurrencyFormatOptions(configuration.GetSection("CurrencyDecimal")); 12 DateTimeFormat = new DateTimeFormatOptions(configuration.GetSection("DateTime")); 13 } 14 }
5.1.4 将结构化配置直接绑定为对象
如果承载配置数据的 IConfiguration 对象与对应的 Options 类型具有兼容的结构,那么可以利用自动绑定机制将 IConfiguration 对象直接转换成 Options 对象。
配置绑定的 API 在“Microsoft.Extensions.Configuration.Binder” NuGet 包中。
按照书上的做未成功,还需要研究。在自己的电脑上尝试是 OK 的,在公司电脑上不行,不知道是不是 .NET Core 的版本问题。。
5.1.5 将配置定义在文件中
在项目根目录添加一个 appsettings.json 文件,内容如下:
{ "format": { "datetime": { "longDatePattern": "dddd, MMMM d, yyyy", "longTimePattern": "h:mm:ss tt", "shortDatePattern": "M/d/yyyy", "shortTimePattern": "h:mm tt" }, "currencyDecimal": { "digits": 2, "symbol": "$" } } }
基于 JSON 的配置源通过 JsonConfigurationSource 表示,这个类型定义在“Microsoft.Extensions.Configuration.Json” NuGet 包中。
1 var configuration = new ConfigurationBuilder() 2 .AddJsonFile("appsettings.json") 3 .Build(); 4 5 var options = new FormatOptions(configuration.GetSection("format"));
5.1.6 根据环境动态加载配置文件
为不同的环境提供对应的配置文件,先要提供一个基础配置文件,还需要为相应的环境提供对应的差异化配置文件,后者通常采用环境名称作为文件扩展名。比如,appsettings.development.json,appsettings.staging.json,appsettings.production.json。
1 var index = Array.IndexOf(args, "/env"); 2 3 var environment = index > -1 ? args[index + 1] : "development"; 4 5 var configuration = new ConfigurationBuilder() 6 .AddJsonFile("appsettings.json") 7 .AddJsonFile($"appsettings.{environment}.json") 8 .Build(); 9 10 var options = new FormatOptions(configuration.GetSection("format")); 11 Console.WriteLine(options.CurrencyFormat.Digits);
使用命令行时传递参数,比如:
dotnet run /env production
5.1.7 配置内容的同步
.NET 的配置模型提供了配置源的监控功能,它能保证原始配置改变之后应用程序能及时接收到通知。
1 var index = Array.IndexOf(args, "/env"); 2 3 var environment = index > -1 ? args[index + 1] : "development"; 4 5 var configuration = new ConfigurationBuilder() 6 .AddJsonFile(path: "appsettings.json") 7 .AddJsonFile($"appsettings.{environment}.json", optional: true, reloadOnChange: true) 8 .Build(); 9 10 ChangeToken.OnChange(() => configuration.GetReloadToken(), () => { 11 var options = new FormatOptions(configuration.GetSection("format")); 12 Console.WriteLine(options.CurrencyFormat.Digits); 13 }); 14 15 Console.ReadLine();
很奇怪,每次文件变动会打印出两个值,未找到原因。。
5.2 配置模型
除了上面提到的 3 个核心对象,其实还有 IConfigurationProvider 对象。
5.2.1 数据结构及其转换
配置模型的最终目的在于提取原始的配置数据并将其转换成一个 IConfiguration 对象。
配置从原始结构向逻辑结构的转变不是一蹴而就的,之间有个中间形态。一棵配置树通过其叶子节点承载所有的原子配置项,并可以利用一个数据字典来表达,被称为“配置字典”。
5.2.2 IConfiguration
一个 IConfiguration 对象具有树形层次结构并不是说该对象内部真的使用这样的数据结构来存储配置文件,而是它提供的 API 在逻辑上体现出树形层次结构。
一个 IConfiguration 对象表示配置树的某个配置节点。表示根节点的 IConfiguration 对象是一个 IConfigurationRoot 对象,非根节点是一个 IConfigurationSection 对象,两者都继承自 IConfiguration 。
IConfigurationRoot 代表了整棵配置树,它被重新加载的话意味着整棵配置树被刷新(#Reload)。它的 Providers 属性返回一组 IConfiguationProvider 对象的集合,它们由注册的 IConfigurationSource 对象提供。
表示非根配置节点的 IConfiguraionSection,只读属性 Key 用来唯一标识多个“同父”的配置节,Path 表示当前配置节点在配置树中的路径,由组成当前路径的所有配置节的 Key 组成,Key 之间用冒号分隔。
1 var source = new Dictionary<string, string>{ 2 ["format:DateTime:longDatePattern"] = "dddd, MMMM d, yyyy", 3 ["format:DateTime:longTimePattern"] = "h:mm:ss tt", 4 ["format:DateTime:shortDatePattern"] = "M/d/yyyy", 5 ["format:DateTime:shortTimePattern"] = "h:mm tt", 6 7 ["format:CurrencyDecimal:digits"] = "5", 8 ["format:CurrencyDecimal:symbol"] = "$" 9 }; 10 11 var configuration = new ConfigurationBuilder() 12 .Add(new MemoryConfigurationSource{ InitialData = source! }) 13 .Build(); 14 IConfigurationSection section = configuration.GetSection("format:DateTime"); 15 IEnumerable<IConfigurationSection> sections = section.GetChildren(); 16 foreach(var s in sections){ 17 Console.WriteLine($"Key: {s.Key}, Path: {s.Path}, Value: {s.Value}"); 18 }
上面程序的最终结果中,比如第一个节点的 Key 是 longDatePattern,Path 是 format:DateTime:longDatePattern。
5.2.3 IConfigurationProvider
IConfigurationProvider 对象负责原始数据的读取,将配置数据从原始结构转为配置字典。配置数据的加载通过调用 Load 方法来完成。
每种类型的配置源都对应一个 IConfigurationProvider,它们一般不直接实现 IConfigurationProvider 接口,而是继承 ConfigurationProvider 抽象类,后者仅仅是对一个 IDictionary<string,string>对象的封装(key 不区分大小写)。这里也能大概看出配置字典的数据结构了。
5.2.4 IConfigurationSource
IConfigurationSource 对象的目的是提供对应的 IConfigurationProvider。
IConfigurationSource 接口利用唯一的 Build 方法,根据指定的 IConfigurationBuilder 对象构建对应的 IConfigurationProvider。
5.2.5 IConfigurationBuilder
在配置模型中处于核心位置。
5.2.6 ConfigurationManager
表示一个可以动态改变的配置。它实现了 IConfigurationBuilder 接口 和 IConfigurationRoot 接口。可以理解为又当运动员又当裁判。它还能在配置源发生改变的时候刷新自己的配置状态。
ASP.NET Core 框架使用的就是这个对象。
5.3 配置绑定
配置绑定的扩展方法定义在“Microsoft.Extensions.Configuration.Binder” NuGet 包。
5.3.1 绑定配置项的值
最简单的绑定是针对配置叶子节点配置节的绑定。这个值是一个字符串,所以绑定最终体现为如何将这个字符串转换成指定的目标类型,这个操作体现在 IConfiguration 接口的两个 GetValue 方法上。
GetValue 方法会将配置节名称作为参数调用指定 IConfiguration#GetSection 方法得到表示对应配置节的 IConfigurationSection 对象,然后提取它的 Value 属性,按照下面的规则转换成目标类型:
1)如果目标类型为 Object,则直接返回原始值(字符串或 Null);
2)如果目标类型不为 Nullable<T>,则针对目标类型的 TypeConverter 将被用来完成类型转换;
3)如果目标类型为 Nullable<T>,原始值是 Null 或空字符串的情况下会直接返回 Null(这句话原文好像有点问题?),否则就按上面的规则将值转换成类型基础 T。
5.3.2 绑定复合对象
如果用一棵树表示一个复合对象,那么叶子节点承载所有的数据,且叶子节点的数据类型均为基元类型。如果用数据字典来提供一个复杂对象所有的原始数据,那么这个字典只需要包含叶子节点对应的值即可。只需要将叶子节点所在的路径作为字典元素的 Key。
1 var source = new Dictionary<string, string>{ 2 ["gender"] = "Male", 3 ["age"] = "18", 4 ["contactInfo:emailAddress"] = "foobar@outlook.com", 5 ["contactInfo:phoneNo"] = "123456789" 6 }; 7 8 var configuration = new ConfigurationBuilder() 9 .AddInMemoryCollection(source!) 10 .Build(); 11 12 var profile = configuration.Get<Profile>(); 13 Debug.Assert(profile.Gender == Gender.Male);
5.3.3 绑定集合
如果绑定的目标类型是集合(包括数组),当前 IConfiguration 对象的每个子配置节将绑定为集合的元素。
1 var source = new Dictionary<string, string>{ 2 ["0:gender"] = "Male", 3 ["0:age"] = "18", 4 ["0:contactInfo:emailAddress"] = "foobar@outlook.com", 5 ["0:contactInfo:phoneNo"] = "123", 6 7 ["1:gender"] = "Male", 8 ["1:age"] = "25", 9 ["1:contactInfo:emailAddress"] = "bar@outlook.com", 10 ["1:contactInfo:phoneNo"] = "456", 11 12 ["2:gender"] = "Male", 13 ["2:age"] = "25", 14 ["2:contactInfo:emailAddress"] = "baz@outlook.com", 15 ["2:contactInfo:phoneNo"] = "789" 16 }; 17 18 var configuration = new ConfigurationBuilder() 19 .AddInMemoryCollection(source!) 20 .Build(); 21 22 var list = configuration.Get<IList<Profile>>(); 23 Debug.Assert(list![0].Gender == Gender.Male); 24 Debug.Assert(list![1].ContactInfo.EmailAddress == "bar@outlook.com");
集合的配置绑定不会因为某个元素的绑定失败而终止。如果目标类型是数组,则最终绑定生成的数组长度与子配置节的个数是一致的,但错误的元素是空;如果是列表,则不会生成对应的元素。
5.3.4 绑定字典
能够通过配置绑定生成的字典是一个实现了 IDictionary<string, T>的类型,它的 Key 必须是一个字符串(或者枚举)。字典对象与集合的配置树在结构上几乎一样,唯一的区别是集合元素的索引直接变成字典元素的 Key。
1 var source = new Dictionary<string, string>{ 2 ["foo:gender"] = "Male", 3 ["foo:age"] = "18", 4 ["foo:contactInfo:emailAddress"] = "foobar@outlook.com", 5 ["foo:contactInfo:phoneNo"] = "123", 6 7 ["bar:gender"] = "Male", 8 ["bar:age"] = "25", 9 ["bar:contactInfo:emailAddress"] = "bar@outlook.com", 10 ["bar:contactInfo:phoneNo"] = "456", 11 12 ["baz:gender"] = "Male", 13 ["baz:age"] = "25", 14 ["baz:contactInfo:emailAddress"] = "baz@outlook.com", 15 ["baz:contactInfo:phoneNo"] = "789" 16 }; 17 18 var configuration = new ConfigurationBuilder() 19 .AddInMemoryCollection(source!) 20 .Build(); 21 22 var dictionary = configuration.Get<IDictionary<string, Profile>>(); 23 Debug.Assert(dictionary!["foo"].Gender == Gender.Male); 24 Debug.Assert(dictionary!["baz"].ContactInfo.EmailAddress == "baz@outlook.com");
5.4 配置的同步
需要解决两个问题:第一,对原始的配置源实施监控并在其发生变化之后重新加载配置;第二,配置重新加载之后及时通知应用程序,及时使用最新的配置。
5.4.1 配置数据流
由 IConfigurationRoot 对象表示的配置树是无状态的,无论是 ConfigurationRoot 对象还是 ConfigurationSection 对象自身是没有存储任何的配置数据。 ConfigurationRoot 对象保持着对所有 IConfigurationProvider 对象的引用,并直接利用后者提供配置数据。也就是说,配置数据在整个模型中以配置字典的形式存储在 IConfigurationProvider 对象上。
5.4.2 ConfigurationReloadToken
ConfigurationReloadToken 对象的作用是通知应用程序配置源已经发生改变,并且新的数据已经被相应的 IConfigurationProvider 对象重新加载进来。
ConfigurationReloadToken 对象本质上是对一个 CancellationTokenSource 对象的封装,它利用 OnReload 方法借助内置的 CancellationTokenSource 对象发送通知。
5.4.3 ConfigurationRoot
一个 ConfigurationRoot 对象是对一组 IConfigurationProvider 对象的封装。
在 RaiseChanged 方法中会调用 ConfigurationReloadToken 的 OnReload 方法对外发出“配置重新加载”的通知。
应用程序会在以下两个场景中接收到配置被重新加载的通知:
1)某个 IConfigurationProvider 对象捕捉到对应配置源是的改变后自动重新加载配置。
2)显式调用 ConfigurationRoot#Reload 手动加载配置。
从索引的定义可以看出,ConfigurationRoot 在读取 Value 值时针对 IConfigurationProvider 列表的遍历是从后往前的,这也是“后来居上”原则的体现。
5.4.4 ConfigurationSection
一个 ConfigurationSection 对象是通过表示配置树根的 ConfigurationRoot 对象和当前配置节在配置树中的路径来构建的。
ConfigurationSection 类型中实现的大部分成员都是调用 ConfigurationRoot 对象的相应 API 来实现的。
5.5 多样性的配置源
5.5.1 MemoryConfigurationSource
MemoryConfigurationSource 采用一个字典对象(KeyValuePair<string, string> 的集合)作为存储原始配置数据的容器。它利用一个 IEnumberable<KeyValuePair<string, string> 类型的属性 InitialData 来存储初始的配置数据。真正被它用来读取原始配置数据的是 MemoryConfigurationProvider 。
5.5.2 EnvironmentVariablesConfigurationSource
环境变量可分为 3 类,针对当前系统,当前用户和当前进程的环境变量。系统和用户级的保存在注册表。
EnvironmentVariablesConfigurationSource 定义在“Microsoft.Extensions.Configuration.EnvironmentVariables” NuGet 包中。
EnvironmentVariablesConfigurationSource 利用对应的 EnvironmentVariablesConfigurationProvider 对象来读取环境变量。
可以直接调用 Add 方法将它注册到 IConfigurationBuilder 上,也可以调用 AddEnvironmentVariables 扩展方法注册。
1 System.Environment.SetEnvironmentVariable("TEST_GENDER", "Male"); 2 System.Environment.SetEnvironmentVariable("TEST_AGE", "18"); 3 System.Environment.SetEnvironmentVariable("TEST_CONTACTINFO:EMAILADDRESS", "Maritn_Fu@infosys.com"); 4 System.Environment.SetEnvironmentVariable("TEST_CONTACTINFO:PHONENO", "123456789"); 5 6 var profile = new ConfigurationBuilder() 7 .AddEnvironmentVariables("TEST_") 8 .Build() 9 .Get<Profile>(); 10 11 Console.WriteLine($"{profile.Gender}: {profile.Age}");
5.5.3 CommandLineConfigurationSource
CommandLineConfigurationSource 定义在“Microsoft.Extensions.Configuration.CommandLine” NuGet 包中。
命令行参数的指定形式大致分为两种:单参数和双参数。单参数的形式就是使用“=”将命令行参数的名称和值:{name}={value},{prefix}{name}={value},目前前缀支持“/”,“--”,“-” 3 种;双参数就是使用两个参数分别定义命令行参数的名称和值,具体格式{prefix}{name} {value}。
CommandLineConfigurationProvider 类型对体现为字符串集合的原始命令行参数进行解析,并将解析出来的参数名称和值添加到配置字典中去。
一般是创建一个 CommandLineConfigurationSource 对象,然后将其注册到 IConfigurationBuilder 对象上,IConfigurationBuilder#AddCommandLine 扩展方法将两步合二为一。
1 try{ 2 var mapping = new Dictionary<string, string>{ 3 ["-a"] = "architecture", 4 ["-arch"] = "architecture" 5 }; 6 7 var configuration = new ConfigurationBuilder() 8 .AddCommandLine(args, mapping) 9 .Build(); 10 11 Console.WriteLine($"Architecture: {configuration["architecture"]}"); 12 } 13 catch(Exception ex){ 14 Console.WriteLine($"Error: {ex.Message}"); 15 }
按书上 P163 尝试,并不是所有的都可以,不知道是有什么变动。
5.5.4 FileConfigurationSource
基类 FileConfigurationSource,有 JsonConfigurationSource、XmlConfigurationSource 和 IniConfigurationSource 3 个具体实现。使用 IFileProvider 读取配置文件。
1 var source = new FakeConfigurationSource{ 2 Path = @"C:\app\appsettings.json" 3 }; 4 Debug.Assert(source.FileProvider == null); 5 6 source.ResolveFileProvider(); 7 var fileProvider = (PhysicalFileProvider?)source.FileProvider; 8 Console.WriteLine(fileProvider?.Root); 9 Debug.Assert(fileProvider?.Root == @"C:\app"); 10 Debug.Assert(source.Path == "appsettings.json");
从上面可以看出,在 FileProvider 没有显式赋值的情况下,将路径设置为绝对路径,则调用 ResolveFileProvider 方法后创建了一个 PhysicalFileProvider,Path 属性被设置为文件名。唯一和书上不同的是,我的 fileProvider.Root 是“C:\”。
FileConfigurationSource#EnsureDefaults(IConfigurationBuilder) 会从 IConfigurationBuilder 对象的 Properties 属性表示的字典中获取 IFileProvider。所以可以在这个属性字典中存储一个默认的 IFileProvider 对象供所有注册在它上面的 FileConfigurationSource 对象共享。IConfigurationBuilder 同时提供了 SetFileProvider 方法和 SetBasePath 方法。
FileConfigurationSource 的 Optional 属性表示当前配置源是否可以缺省。如果为 true,那么指定的文件不存在也不会抛出异常。
1 public abstract class FileConfigurationSource : IConfigurationSource 2 { 3 /// <summary> 4 /// Used to access the contents of the file. 5 /// </summary> 6 public IFileProvider FileProvider { get; set; } 7 8 /// <summary> 9 /// The path to the file. 10 /// </summary> 11 public string Path { get; set; } 12 13 /// <summary> 14 /// Determines if loading the file is optional. 15 /// </summary> 16 public bool Optional { get; set; } 17 18 /// <summary> 19 /// Determines whether the source will be loaded if the underlying file changes. 20 /// </summary> 21 public bool ReloadOnChange { get; set; } 22 23 /// <summary> 24 /// Number of milliseconds that reload will wait before calling Load. This helps 25 /// avoid triggering reload before a file is completely written. Default is 250. 26 /// </summary> 27 public int ReloadDelay { get; set; } = 250; 28 29 /// <summary> 30 /// Will be called if an uncaught exception occurs in FileConfigurationProvider.Load. 31 /// </summary> 32 public Action<FileLoadExceptionContext> OnLoadException { get; set; } 33 }
FileConfigurationSource 借助 IFileProvider 对象提供的文件系统监控功能实现了配置文件在更新后的实时加载新内容的功能,这个特性通过 ReloadOnChange 属性来开启或关闭。默认是关闭的。如果开启自动重新加载配置文件的功能,一旦配置文件发送变化,IFileProvider 就会将通知发送给对应的 FileConfigurationProvider 对象,后者调用 Load 方法重新加载配置文件。考虑到此时配置文件的写入可能还未结束,所以 FileConfigurationSource 采用“延时加载”的方式解决问题,通过 ReloadDelay 属性控制,默认 250 毫秒。
配置文件加载出现异常可以通过 FileConfigurationSource 的 OnLoadException 属性注册一个 Action<FileLoadExceptionContext>委托对象作为异常处理器。可以调用 IConfigurationBuilder#setFileLoadExceptionHandler 扩展方法注册一个共享的异常处理器,也保存在 Properties 字典中,key 为“FileLoadExceptionHandler”。
1. JsonConfigurationSource
1 public override IConfigurationProvider Build(IConfigurationBuilder builder) 2 { 3 EnsureDefaults(builder); 4 return new JsonConfigurationProvider(this); 5 }
在创建对应的 JsonConfigurationProvider 对象之前会先调用 EnsureDefaults 方法,确保读取配置文件的 IFileProvider 和处理配置文件加载异常的处理器被初始化。
2. XmlConfigurationSource
当采用一个 XML 元素表示一个复杂对象时,对象的数据成员可以定义为当前 XML 元素的子元素。如果数据成员是一个简单的数据类型,可以选择将其定义成当前 XML 元素的属性。
不能正确转换为配置字典,这是因为字典的 Key 必须是唯一的,而且最终构成配置树的每个节点必须具有不同的路径。为了解决这个问题,当同一个等级的多个同名 XML 被添加到配置字典时,Key 对应的路径会额外增加一级,对应的值为自增的整数,看起来是从 0 开始的。
IConfigurationBuilder#AddXmlFile。
3. IniConfigurationSource
INI 文件以 {Key}={Value} 的形式定义配置项。当 Value 值前后有空格时,必须使用双引号,否则就会被忽略。
还可以采用 [{SectionName}] 的形式定义配置节对它们进行分组。
IConfigurationBuilder#AddIniFile。
5.5.5 StreamConfigurationSource
StreamConfigurationSource 对象通过指定的 Stream 对象来读取配置内容。
派生的有 JsonStreamConfigurationSource、XmlStreamConfigurationSource 和 IniStreamConfigurationSource,对应的,IConfigurationBuidler 有 AddJsonStream、AddXmlStream 和 AddIniStream。
5.5.6 ChainedConfigurationSource
ChainedConfigurationSource 类型表示的配置源比较特殊,因为它承载的原始数据就是一个 IConfiguration 对象。