.NET Core 3.0源码理解Configuration

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

 

.NET Core 采用了统一的调用方式来加载不同类型的配置信息,并通过统一的抽象接口IConfigurationSource 对配置源进行管理,这也是刚刚所说的灵活。

 

而其扩展性就是我们可以自己自定义新的 Provider 实例,而不会改变其原来的调用方式。接下来的文章将会基于 Consul,扩展一个新的 Provider 实例。

 

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

 

.NET Core 3.0 源码理解 Configuration

 

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

 

  • 环境变量

     

  • 命令行参数

     

  • 各种形式的配置文件

     

  • 内存对象

     

  • 用户自定义扩展源 

 

核心对象

 

在介绍.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):获取指定键的子节点

 

我们来看一下源码:

/// <summary>
/// Represents a set of key/value application configuration properties.
/// </summary>
public interface IConfiguration
{
/// <summary>
/// Gets or sets a configuration value.
/// </summary>
/// <param name="key">The configuration key.</param>
/// <returns>The configuration value.</returns>
string this[string key] { get; set; }

/// <summary>
/// Gets a configuration sub-section with the specified key.
/// </summary>
/// <param name="key">The key of the configuration section.</param>
/// <returns>The <see cref="IConfigurationSection"/>.</returns>
/// <remarks>
///     This method will never return <c>null</c>. If no matching sub-section is found with the specified key,
///     an empty <see cref="IConfigurationSection"/> will be returned.
/// </remarks>
IConfigurationSection GetSection(string key);
/// <summary>
/// Gets the immediate descendant configuration sub-sections.
/// </summary>
/// <returns>The configuration sub-sections.</returns>
IEnumerable<IConfigurationSection> GetChildren();

/// <summary>
/// Returns a <see cref="IChangeToken"/> that can be used to observe when this configuration is reloaded.
/// </summary>
/// <returns>A <see cref="IChangeToken"/>.</returns>
IChangeToken GetReloadToken();
}

 

通常我们要求配置文件要有足够的灵活性,尤其是我们所扩展的配置信息存放在了其他服务器,当修改的时候我们很需要一套监控功能,以及时灵活的应对配置信息的修改。现在.NET Core 为我们提供了这样一个功能,我们只需要自定义少量代码即可完成配置信息的同步。

 

这个方法就是 GetReloadToken(),其返回值是 IChangeToken。此处对配置信息的同步只做一个引子,后面的文章会详细说明。

 

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

 

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

 

ConfigurationRoot 是配置的根节点,也实现了 IConfigurationRoot,此接口只有一个方法,其主要功能就是实现对配置信息的重新加载,另外还包括一个 IConfigurationProvider 类型的集合属性。

其源码如下

/// <summary>
/// Represents the root of an <see cref="IConfiguration"/> hierarchy.
/// </summary>
public interface IConfigurationRoot : IConfiguration
{
/// <summary>
/// Force the configuration values to be reloaded from the underlying <see cref="IConfigurationProvider"/>s.
/// </summary>
void Reload();

/// <summary>
/// The <see cref="IConfigurationProvider"/>s for this configuration.
/// </summary>
    IEnumerable<IConfigurationProvider> Providers { get; }
}

 

下面是 ConfigurationRoot 关于 Reload()方法的实现

/// <summary>
/// Force the configuration values to be reloaded from the underlying sources.
/// </summary>
public void Reload()
{
foreach (var provider in _providers)
    {
        provider.Load();
    }
    RaiseChanged();
}

 

通过源码我们知道,如果调用了 Reload()方法,所有类型的 Provider 都会重新加载。

 

前面有 ConfigurationRoot 表示配置的根节点,那么 ConfigurationSection 则表示非跟节点,毕竟父节点、子节点都是相对,所以此处使用非根节点。

ConfigurationSection 继承于 IConfigurationSection,该接口只有三个只读属性,分别表示配置信息的 Key、Value 以及路径信息,需要指出的是,此处的路径信息主要指从根节点到当前节点的路径,以表示当前节点的位置,类似于 A:B:C 可以表示节点 C 的位置,其中 A、B、C 都是 ConfigurationSection 的 Key。

 

以下是 ConfigurationSection 的源码

/// <summary>
/// Represents a section of application configuration values.
/// </summary>
public interface IConfigurationSection : IConfiguration
{
/// <summary>
/// Gets the key this section occupies in its parent.
/// </summary>
string Key { get; }

/// <summary>
/// Gets the full path to this section within the <see cref="IConfiguration"/>.
/// </summary>
string Path { get; }

/// <summary>
/// Gets or sets the section value.
/// </summary>
string Value { get; set; }
}

 

IConfigurationBuilder

 

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

 

两个只读属性:

 

  • Properties:获取可用于在 IConfigurationBuilder 之间共享数据的键/值集合

     

  • Sources:该属性用于缓存不同的配置源,以用于相对应的 Provider 的创建

 

两个方法:

 

  • Add(IConfigurationSource source):新增 IConfigurationSource,并添加到属性中 Sources 中

     

  • Build():该方法遍历 Sources 属性,并调用 IConfigurationSource 的 Build()方法,通过获取 Provider 集合,最终创建 IConfigurationRoot对象

 

ConfigurationBuilder 源码如下

/// <summary>
/// Used to build key/value based configuration settings for use in an application.
/// </summary>
public class ConfigurationBuilder : IConfigurationBuilder
{
/// <summary>
/// Returns the sources used to obtain configuration values.
/// </summary>
public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();

/// <summary>
/// Gets a key/value collection that can be used to share data between the <see cref="IConfigurationBuilder"/>
/// and the registered <see cref="IConfigurationProvider"/>s.
/// </summary>
public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>();

/// <summary>
/// Adds a new configuration source.
/// </summary>
/// <param name="source">The configuration source to add.</param>
/// <returns>The same <see cref="IConfigurationBuilder"/>.</returns>
public IConfigurationBuilder Add(IConfigurationSource source)
{
if (source == null)
         {
throw new ArgumentNullException(nameof(source));
         }
         Sources.Add(source);
return this;
     }

/// <summary>
/// Builds an <see cref="IConfiguration"/> with keys and values from the set of providers registered in
/// <see cref="Sources"/>.
/// </summary>
/// <returns>An <see cref="IConfigurationRoot"/> with keys and values from the registered providers.</returns>
public IConfigurationRoot Build()
{
var providers = new List<IConfigurationProvider>();
foreach (var source in Sources)
         {
var provider = source.Build(this);
             providers.Add(provider);
         }
return new ConfigurationRoot(providers);
     }
 }

 

此处令人感慨颇多,我们最终调用 ConfigurationRoot 的构造函数,究其原因是 Provider 提供了统一的数据访问方式,不管是基于何种类型的 Provider,我们都可以调用其 Load()方法加载配置项。此外,IConfigurationBuilder 本身有很多的扩展方法来注册数据源,比如 AddJsonFile()扩展方法。我们来看一下,我们常见的写法,

var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings1.json", false, true)
            .AddJsonFile("appsettings2.json", false, true);
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,这也是一一对应的关系。所有不同的源最终都会转化成统一的键值对表示。

 

以下为

/// <summary>
/// Represents a source of configuration key/values for an application.
/// </summary>
public interface IConfigurationSource
{
/// <summary>
/// Builds the <see cref="IConfigurationProvider"/> for this source.
/// </summary>
/// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param>
/// <returns>An <see cref="IConfigurationProvider"/></returns>
IConfigurationProvider Build(IConfigurationBuilder builder);
}

 

以下是 MemoryConfigurationSource 的源码

/// <summary>
/// Represents in-memory data as an <see cref="IConfigurationSource"/>.
/// </summary>
public class MemoryConfigurationSource : IConfigurationSource
{
/// <summary>
/// The initial key value configuration pairs.
/// </summary>
public IEnumerable<KeyValuePair<string, string>> InitialData { get; set; }
/// <summary>
/// Builds the <see cref="MemoryConfigurationProvider"/> for this source.
/// </summary>
/// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param>
/// <returns>A <see cref="MemoryConfigurationProvider"/></returns>
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new MemoryConfigurationProvider(this);
    }
}

 

IConfigurationProvider

 

通过上文的介绍,我们可以知道 IConfigurationProvider 是统一的对外接口,对用户提供配置的查询、重新加载等功能。

其派生类包括 Microsoft.Extensions.Configuration.ConfigurationProvider、Microsoft.Extensions.Configuration.ChainedConfigurationProvider、Microsoft.Extensions.Configuration.Memory.MemoryConfigurationProvider。另外该派生类还会在文件类配置场景下依赖 Microsoft.Extensions.Configuration.FileExtensions 组件。

 

以下是 Microsoft.Extensions.Configuration.ConfigurationProvider 的源码:

/// <summary>
/// Base helper class for implementing an <see cref="IConfigurationProvider"/>
/// </summary>
public abstract class ConfigurationProvider : IConfigurationProvider
{
private ConfigurationReloadToken _reloadToken = new ConfigurationReloadToken();
/// <summary>
/// Initializes a new <see cref="IConfigurationProvider"/>
/// </summary>
protected ConfigurationProvider()
{
        Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
    }
/// <summary>
/// The configuration key value pairs for this provider.
/// </summary>
protected IDictionary<string, string> Data { get; set; }

/// <summary>
/// Attempts to find a value with the given key, returns true if one is found, false otherwise.

/// </summary>
/// <param name="key">The key to lookup.</param>
/// <param name="value">The value found at key if one is found.</param>
/// <returns>True if key has a value, false otherwise.</returns>
public virtual bool TryGet(string key, out string value)
=> Data.TryGetValue(key, out value);

/// <summary>
/// Sets a value for a given key.
/// </summary>
/// <param name="key">The configuration key to set.</param>
/// <param name="value">The value to set.</param>
public virtual void Set(string key, string value)
=> Data[key] = value;

/// <summary>
/// Loads (or reloads) the data for this provider.
/// </summary>
public virtual void Load()
{ }

/// <summary>
/// Returns the list of keys that this provider has.
/// </summary>
/// <param name="earlierKeys">The earlier keys that other providers contain.</param>
/// <param name="parentPath">The path for the parent IConfiguration.</param>
/// <returns>The list of keys for this provider.</returns>
public virtual IEnumerable<string> GetChildKeys(
        IEnumerable<string> earlierKeys,
string parentPath)
{
var prefix = parentPath == null ? string.Empty : parentPath + ConfigurationPath.KeyDelimiter;
return Data
            .Where(kv => kv.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
            .Select(kv => Segment(kv.Key, prefix.Length))
            .Concat(earlierKeys)
            .OrderBy(k => k, ConfigurationKeyComparer.Instance);
    }

private static string Segment(string key, int prefixLength)
{
var indexOf = key.IndexOf(ConfigurationPath.KeyDelimiter, prefixLength, StringComparison.OrdinalIgnoreCase);
return indexOf < 0 ? key.Substring(prefixLength) : key.Substring(prefixLength, indexOf - prefixLength);
    }

/// <summary>
/// Returns a <see cref="IChangeToken"/> that can be used to listen when this provider is reloaded.
/// </summary>
/// <returns></returns>
public IChangeToken GetReloadToken()
{
return _reloadToken;
    }
/// <summary>
/// Triggers the reload change token and creates a new one.
/// </summary>
protected void OnReload()
{
var previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken());
        previousToken.OnReload();
    }
/// <summary>
/// Generates a string representing this provider name and relevant details.
/// </summary>
/// <returns> The configuration name. </returns>
public override string ToString() => $"{GetType().Name}";
}

 

通过源码,我们可以知道 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 各个组件的结构图:

 

.NET Core 3.0 源码理解 Configuration


转自:艾心

cnblogs.com/edison0621/p/10854215.html

© 版权声明

☆ END ☆
喜欢就点个赞吧
点赞0 分享
图片正在生成中,请稍后...