Configuration总体介绍

微软在.NET
Core里设计出了全新的配置体系,并以非常灵活、可扩展的方式实现。从其源码来看,其运行机制大致是,根据其Source,创建一个Builder实例,并会向其添加Provider,在我们使用配置信息的时候,会从内存中获取相应的Provider实例。

.NET
Core采用了统一的调用方式来加载不同类型的配置信息,并通过统一的抽象接口IConfigurationSource对配置源进行管理,这也是刚刚所说的灵活。而其扩展性就是我们可以自己自定义新的Provider实例,而不会改变其原来的调用方式。接下来的文章将会基于Consul,扩展一个新的Provider实例。

在ASP.NET Core 中,我们的应用配置是基于IConfigurationProvider的键值对。 我们先看一下思维导图:


<https://img2018.cnblogs.com/blog/533598/201905/533598-20190512230516963-348574716.png>

基于上图,我们可以看到主要有键值对源有多种,分别是:

 环境变量

命令行参数

各种形式的配置文件

内存对象

用户自定义扩展源 

核心对象

在介绍.NET
Core配置功能之前,先简要说明一下Microsoft.Extensions.Configuration.Abstractions,该组件抽象了.NET
Core的配置功能,并对自定义扩展制定了新的标准。以下介绍的四个核心对象全部来自于该组件。

IConfiguration

该接口表示一组键/值应用程序配置属性,应用程序使用配置时的入口对象,.NET
Core对其有多种扩展,其派生类包括位于统一类库的IConfigurationSection,以及Microsoft.Extensions.Configuration类库中的ConfigurationRoot、ConfigurationSection、IConfigurationRoot。我们可以通过DI获取IConfiguration实例。

它主要有以下三个方法:

* GetChildren():获取直接子配置子节
* GetReloadToken():返回一个IChangeToken,可用于确定何时重新加载配置
* GetSection(String):获取指定键的子节点
我们来看一下源码:
1: /// <summary> 2: /// Represents a set of key/value application
configuration properties. 3: /// </summary> 4: public interface IConfiguration
5: { 6: /// <summary> 7: /// Gets or sets a configuration value. 8: ///
</summary> 9: /// <param name="key">The configuration key.</param> 10: ///
<returns>The configuration value.</returns> 11: string this[string key] { get;
set; } 12:   13: /// <summary> 14: /// Gets a configuration sub-section with
the specified key. 15: /// </summary> 16: /// <param name="key">The key of
the configuration section.</param> 17: /// <returns>The <see
cref="IConfigurationSection"/>.</returns> 18: /// <remarks> 19: /// This
method will never return <c>null</c>. If no matching sub-section is found with
the specified key, 20: /// an empty <see cref="IConfigurationSection"/> will
be returned. 21: /// </remarks> 22: IConfigurationSection GetSection(string
key); 23:   24: /// <summary> 25: /// Gets the immediate descendant
configuration sub-sections. 26: /// </summary> 27: /// <returns>The
configuration sub-sections.</returns> 28: IEnumerable<IConfigurationSection>
GetChildren(); 29:   30: /// <summary> 31: /// Returns a <see
cref="IChangeToken"/> that can be used to observe when this configuration is
reloaded. 32: /// </summary> 33: /// <returns>A <see
cref="IChangeToken"/>.</returns> 34: IChangeToken GetReloadToken(); 35: }

通常我们要求配置文件要有足够的灵活性,尤其是我们所扩展的配置信息存放在了其他服务器,当修改的时候我们很需要一套监控功能,以及时灵活的应对配置信息的修改。现在.NET
Core为我们提供了这样一个功能,我们只需要自定义少量代码即可完成配置信息的同步。这个方法就是GetReloadToken(),其返回值是IChangeToken。此处对配置信息的同步只做一个引子,后面的文章会详细说明。


由于ConfigurationRoot、ConfigurationSection聚集于IConfiguration接口,此处也对这两个类进行讨论,方便我们对.NET
Core的配置功能有个更加形象的印象。这两个接口,本质上就是.NET Core关于配置信息的读取方式。

XML是使用比较广泛的一种数据结构,我们在配置XML时,一般会使用根节点、父节点、子节点之类的术语,此处也一样。


ConfigurationRoot是配置的根节点,也实现了IConfigurationRoot,此接口只有一个方法,其主要功能就是实现对配置信息的重新加载,另外还包括一个IConfigurationProvider类型的集合属性。其源码如下
1: /// <summary> 2: /// Represents the root of an <see
cref="IConfiguration"/> hierarchy. 3: /// </summary> 4: public interface
IConfigurationRoot : IConfiguration 5: { 6: /// <summary> 7: /// Force the
configuration values to be reloaded from the underlying <see
cref="IConfigurationProvider"/>s. 8: /// </summary> 9: void Reload(); 10:  
11: /// <summary> 12: /// The <see cref="IConfigurationProvider"/>s for this
configuration. 13: /// </summary> 14: IEnumerable<IConfigurationProvider>
Providers { get; } 15: }
下面是ConfigurationRoot关于Reload()方法的实现
1: /// <summary> 2: /// Force the configuration values to be reloaded from
the underlying sources. 3: /// </summary> 4: public void Reload() 5: { 6:
foreach (var provider in _providers) 7: { 8: provider.Load(); 9: } 10:  
11: RaiseChanged(); 12: }
通过源码我们知道,如果调用了Reload()方法,所有类型的Provider都会重新加载。


前面有ConfigurationRoot表示配置的根节点,那么ConfigurationSection则表示非跟节点,毕竟父节点、子节点都是相对,所以此处使用非根节点。ConfigurationSection继承于IConfigurationSection,该接口只有三个只读属性,分别表示配置信息的Key、Value以及路径信息,需要指出的是,此处的路径信息主要指从根节点到当前节点的路径,以表示当前节点的位置,类似于A:B:C可以表示节点C的位置,其中A、B、C都是ConfigurationSection的Key。以下是ConfigurationSection的源码
1: /// <summary> 2: /// Represents a section of application configuration
values. 3: /// </summary> 4: public interface IConfigurationSection :
IConfiguration 5: { 6: /// <summary> 7: /// Gets the key this section
occupies in its parent. 8: /// </summary> 9: string Key { get; } 10:   11:
/// <summary> 12: /// Gets the full path to this section within the <see
cref="IConfiguration"/>. 13: /// </summary> 14: string Path { get; } 15:  
16: /// <summary> 17: /// Gets or sets the section value. 18: /// </summary>
19: string Value { get; set; } 20: }
IConfigurationBuilder


该接口主要用于创建IConfigurationProvider,其派生类包括Microsoft.Extensions.Configuration.ConfigurationBuilder。其成员包括

两个只读属性:

* Properties:获取可用于在IConfigurationBuilder之间共享数据的键/值集合
* Sources:该属性用于缓存不同的配置源,以用于相对应的Provider的创建
两个方法:

* Add(IConfigurationSource source):新增IConfigurationSource,并添加到属性中Sources中
*
Build():该方法遍历Sources属性,并调用IConfigurationSource的Build()方法,通过获取Provider集合,最终创建IConfigurationRoot对象
ConfigurationBuilder源码如下
1: /// <summary> 2: /// Used to build key/value based configuration settings
for use in an application. 3: /// </summary> 4: public class
ConfigurationBuilder : IConfigurationBuilder 5: { 6: /// <summary> 7: ///
Returns the sources used to obtain configuration values. 8: /// </summary> 9:
public IList<IConfigurationSource> Sources { get; } = new
List<IConfigurationSource>(); 10:   11: /// <summary> 12: /// Gets a
key/value collection that can be used to share data between the <see
cref="IConfigurationBuilder"/> 13: /// and the registered <see
cref="IConfigurationProvider"/>s. 14: /// </summary> 15: public IDictionary<
string, object> Properties { get; } = new Dictionary<string, object>(); 16:  
17: /// <summary> 18: /// Adds a new configuration source. 19: /// </summary>
20: /// <param name="source">The configuration source to add.</param> 21: ///
<returns>The same <see cref="IConfigurationBuilder"/>.</returns> 22: public
IConfigurationBuilder Add(IConfigurationSource source) 23: { 24: if (source
==null) 25: { 26: throw new ArgumentNullException(nameof(source)); 27: }
28:  29: Sources.Add(source); 30: return this; 31: } 32:   33: ///
<summary> 34: /// Builds an <see cref="IConfiguration"/> with keys and values
from the set of providers registered in 35: /// <see cref="Sources"/>. 36:
/// </summary> 37: /// <returns>An <see cref="IConfigurationRoot"/> with keys
and values from the registered providers.</returns> 38: public
IConfigurationRoot Build() 39: { 40: var providers = new
List<IConfigurationProvider>(); 41: foreach (var source in Sources) 42: {
43: var provider = source.Build(this); 44: providers.Add(provider); 45: }
46: return new ConfigurationRoot(providers); 47: } 48: }
此处令人感慨颇多,我们最终调用 ConfigurationRoot
的构造函数,究其原因是Provider提供了统一的数据访问方式,不管是基于何种类型的Provider,我们都可以调用其Load()方法加载配置项。此外,IConfigurationBuilder本身有很多的扩展方法来注册数据源,比如AddJsonFile()扩展方法。我们来看一下,我们常见的写法,
1: var builder = new ConfigurationBuilder() 2:   3:
.SetBasePath(env.ContentRootPath) 4:   5: .AddJsonFile("appsettings1.json",
false, true) 6:   7: .AddJsonFile("appsettings2.json", false, true); 8:  
9:Configuration = builder.Build();
IConfigurationSource


该接口表示应用程序配置的键值对。其派生类包括Microsoft.Extensions.Configuration.ChainedConfigurationSource、Microsoft.Extensions.Configuration.Memory.MemoryConfigurationSource。另外该派生类还会在文件类配置场景下依赖Microsoft.Extensions.Configuration.FileExtensions组件。


它是所有配置源的抽象表示,包括JSON、XML、INI、环境变量等等。通过上文我们也知道了,IConfigurationBuilder会注册多个IConfigurationSource实例。它只有一个方法,就是Build()方法,并返回IConfigurationProvider,由此可见,IConfigurationProvider的创建依赖于IConfigurationSource,这也是一一对应的关系。所有不同的源最终都会转化成统一的键值对表示。

以下为
1: /// <summary> 2: /// Represents a source of configuration key/values for
an application. 3: /// </summary> 4: public interface IConfigurationSource 5:
{ 6: /// <summary> 7: /// Builds the <see cref="IConfigurationProvider"/> for
this source. 8: /// </summary> 9: /// <param name="builder">The <see
cref="IConfigurationBuilder"/>.</param> 10: /// <returns>An <see
cref="IConfigurationProvider"/></returns> 11: IConfigurationProvider
Build(IConfigurationBuilder builder); 12: }
以下是MemoryConfigurationSource的源码
1: /// <summary> 2: /// Represents in-memory data as an <see
cref="IConfigurationSource"/>. 3: /// </summary> 4: public class
MemoryConfigurationSource : IConfigurationSource 5: { 6: /// <summary> 7:
/// The initial key value configuration pairs. 8: /// </summary> 9: public
IEnumerable<KeyValuePair<string, string>> InitialData { get; set; } 10:   11:
/// <summary> 12: /// Builds the <see cref="MemoryConfigurationProvider"/> for
this source. 13: /// </summary> 14: /// <param name="builder">The <see
cref="IConfigurationBuilder"/>.</param> 15: /// <returns>A <see
cref="MemoryConfigurationProvider"/></returns> 16: public
IConfigurationProvider Build(IConfigurationBuilder builder) 17: { 18: return
new MemoryConfigurationProvider(this); 19: } 20: }
IConfigurationProvider


通过上文的介绍,我们可以知道IConfigurationProvider是统一的对外接口,对用户提供配置的查询、重新加载等功能。其派生类包括Microsoft.Extensions.Configuration.ConfigurationProvider、Microsoft.Extensions.Configuration.ChainedConfigurationProvider、Microsoft.Extensions.Configuration.Memory.MemoryConfigurationProvider。另外该派生类还会在文件类配置场景下依赖Microsoft.Extensions.Configuration.FileExtensions组件。

以下是Microsoft.Extensions.Configuration.ConfigurationProvider的源码:
1: /// <summary> 2: /// Base helper class for implementing an <see
cref="IConfigurationProvider"/> 3: /// </summary> 4: public abstract class
ConfigurationProvider : IConfigurationProvider 5: { 6: private
ConfigurationReloadToken _reloadToken =new ConfigurationReloadToken(); 7:   8:
/// <summary> 9: /// Initializes a new <see cref="IConfigurationProvider"/>
10: /// </summary> 11: protected ConfigurationProvider() 12: { 13: Data =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); 14: } 15:  
16: /// <summary> 17: /// The configuration key value pairs for this provider.
18: /// </summary> 19: protected IDictionary<string, string> Data { get; set;
} 20:   21: /// <summary> 22: /// Attempts to find a value with the given
key, returns true if one is found, false otherwise. 23: /// </summary> 24:
/// <param name="key">The key to lookup.</param> 25: /// <param
name="value">The value found at key if one is found.</param> 26: ///
<returns>True if key has a value, false otherwise.</returns> 27: public virtual
bool TryGet(string key, out string value) 28: => Data.TryGetValue(key, out
value); 29:   30: /// <summary> 31: /// Sets a value for a given key. 32:
/// </summary> 33: /// <param name="key">The configuration key to set.</param>
34: /// <param name="value">The value to set.</param> 35: public virtual void
Set(string key, string value) 36: => Data[key] = value; 37:   38: ///
<summary> 39: /// Loads (or reloads) the data for this provider. 40: ///
</summary> 41: public virtual void Load() 42: { } 43: 44: /// <summary>
45: /// Returns the list of keys that this provider has. 46: /// </summary>
47: /// <param name="earlierKeys">The earlier keys that other providers
contain.</param> 48: /// <param name="parentPath">The path for the parent
IConfiguration.</param> 49: /// <returns>The list of keys for this
provider.</returns> 50: public virtual IEnumerable<string> GetChildKeys( 51:
IEnumerable<string> earlierKeys, 52: string parentPath) 53: { 54: var
prefix = parentPath ==null ? string.Empty : parentPath +
ConfigurationPath.KeyDelimiter; 55:   56: return Data 57: .Where(kv =>
kv.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) 58: .Select(kv
=> Segment(kv.Key, prefix.Length)) 59: .Concat(earlierKeys) 60: .OrderBy(k
=> k, ConfigurationKeyComparer.Instance); 61: } 62:   63: private static
string Segment(string key, int prefixLength) 64: { 65: var indexOf =
key.IndexOf(ConfigurationPath.KeyDelimiter, prefixLength,
StringComparison.OrdinalIgnoreCase); 66: return indexOf < 0 ?
key.Substring(prefixLength) : key.Substring(prefixLength, indexOf -
prefixLength); 67: } 68:   69: /// <summary> 70: /// Returns a <see
cref="IChangeToken"/> that can be used to listen when this provider is reloaded.
71: /// </summary> 72: /// <returns></returns> 73: public IChangeToken
GetReloadToken() 74: { 75: return _reloadToken; 76: } 77:   78: ///
<summary> 79: /// Triggers the reload change token and creates a new one. 80:
/// </summary> 81: protected void OnReload() 82: { 83: var previousToken =
Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken()); 84:
previousToken.OnReload(); 85: } 86:   87: /// <summary> 88: /// Generates
a string representing this provider name and relevant details. 89: ///
</summary> 90: /// <returns> The configuration name. </returns> 91: public
override string ToString() => $"{GetType().Name}"; 92: }

通过源码,我们可以知道ConfigurationProvider以字典类型缓存了多个Provider对象,有需要的时候,从内存中获取即可,配置的加载通过Load()方法实现,在ConfigurationRoot里我们介绍了其Reload,并且说明其方法是在循环调用ConfigurationProvider的Load方法,但是此处只提供了一个虚方法,其目的是要交给其他具体的Provider,比如环境变量、JSON、XML等,这些具体的Provider可以从相应的配置源中获取配置信息。所有的子节点KEY通过GetChildKeys方法实现,其重新加载方式通过ConfigurationReloadToken实例完成。

另外需要说明一下,在ConfigurationProvider构造函数里,对字典进行了初始化,并同时设置了字典Key不受大小写限制,这是一个需要注意的细节。

Configuration组件结构


通过查看.NET配置功能的源码,所有依赖均基于Microsoft.Extensions.Configuration.Abstractions,在其上有一层实现,即Microsoft.Extensions.Configuration,其内部也多数是抽象实现,并提供了多个虚方法交给其派生组件,比如环境变量、命令行参数、各种文件型配置等,当然各种文件型配置还要依赖Microsoft.Extensions.Configuration.FileExtensions组件。

以下是.NET Core 3.0预览版里的Configuration各个组件的结构图:


<https://img2018.cnblogs.com/blog/533598/201905/533598-20190512230517711-226377452.png>