- 一 背景
相比.Net Framework , .NET Core的配置系统 ,有一些明显的优点 ,如:
1 支持更丰富的配置源
2 读取配置时, 可以用相同的方式读取, 并且非常方便
3 修改配置后,不用重启应用
本期对配置相关的源码简单梳理一下。 只说主逻辑 ,并且从用的角度说起 ,逐步深入。
- 二 配置读取
- 2.1 举个栗子
进入上代码环节 ,新建控制台应用 , 只需要引入 “Microsoft.Extensions.Configuration
” 1个nuget包 ,就可以使用基本的配置了 。
(不得不说,相较于.NET Framework,.Net Core 的组件化设计实现的相当彻底 ) github地址
//数据源
var dicData = new Dictionary<string, string>() { { "Key1", "Value1" }, { "Key2", "Value2" } };
IConfigurationRoot configurationRoot = new ConfigurationBuilder()
.Add(new MemoryConfigurationSource() { InitialData = dicData }) // 添加配置源
.Build(); // 构建配置
Console.WriteLine("Key1=" + configurationRoot["Key1"]);//输出内容 Key1=Value1
我们好奇 configurationRoot[“Key1”] 是怎么读取到 “Value1” 呢 ? 这就需要看源码了。 IConfigurationRoot
到底是什么 , 以及 ["key1"]
的代码执行逻辑是怎样的 。
(注意: 为了方便了解主线逻辑 , 本文的中 .Net Core 源码有删减 )
- 2.2
IConfigurationRoot
相关源码
源码如下:
public interface IConfiguration
{
string this[string key] { get; set; }
}
public interface IConfigurationRoot : IConfiguration
{
IEnumerable<IConfigurationProvider> Providers { get; }
}
public class ConfigurationRoot : IConfigurationRoot, IDisposable
{
private readonly IList<IConfigurationProvider> _providers;
public IEnumerable<IConfigurationProvider> Providers => _providers;
/// <summary>
/// 根据指定的Key获取或者设置对应的Value
/// </summary>
/// <param name="key">The configuration key.</param>
/// <returns>The configuration value.</returns>
public string this[string key]
{
get
{
for (var i = _providers.Count - 1; i >= 0; i--)
{
var provider = _providers[i];
if (provider.TryGet(key, out var value))
{
return value;
}
}
return null;
}
set
{
if (!_providers.Any())
{
throw new InvalidOperationException(Resources.Error_NoSources);
}
foreach (var provider in _providers)
{
provider.Set(key, value);
}
}
}
1 IConfigurationRoot
有个 Providers
属性 ;
2 读取配置是通过ConfigurationRoot类中的索引器实现的 , 更具体的说就是通过 遍历Providers ,调用其.TryGet(key, out var value)
获取的 。
3 细心的同学会发现,一旦获取到配置就结束并返回了。如此就有了从哪先获取的问题 ,我们发现遍历时是LIFO(后入先出)的顺序,所以后面添加的Provider有更高的优先级 。
到目前为止 ,我们只需要记住一个主知识点 :获取配置是由多个 Provider(供应者) provide (供应) 的 ,并且越后面的 Provider 优先级越高,具有覆盖性。
再想深入就得研究一下 : IConfigurationProvider
是怎么回事 , 特别是其中的 TryGet
方法是怎么实现的。
- 2.3
IConfigurationProvider
相关源码
public interface IConfigurationProvider
{
bool TryGet(string key, out string value);
void Load();
}
public abstract class ConfigurationProvider : IConfigurationProvider
{
/// <summary>
/// The configuration key value pairs for this provider.
/// </summary>
protected IDictionary<string, string> Data { get; set; }
/// <summary>
/// 根据指定的 键 获取对应的 值, 获取到返回true,否则返回 false
/// </summary>
public virtual bool TryGet(string key, out string value)
=> Data.TryGetValue(key, out value);
/// <summary>
/// 加载数据
/// </summary>
public virtual void Load()
{
}
}
代码非常简单 ,每个Provider 维护了一个 Data (字典类型) 属性 , 配置就是从这个字典拿到的。 到此基本的读取配置逻辑我们已经知道了 。
- 2.4 使用便利性
这时 ,我们可以再看开头那个栗子,读取配置其实还有这种写法 :
configurationRoot.Providers.First().TryGet("Key1", out string Val);
Console.WriteLine("Key1=" + Val); //输出 Val=Value1
从使用的角度考虑,框架做了一些封装,并提供了不少扩展方法。但是基本的读取逻辑都是一样的。
如果我们引入 “Microsoft.Extensions.Configuration.Binder
” nuget包 ,读取配置操作时会发现一些新的方法。
比如配置和强模型绑定,并结合泛型使用 ,如:
public static T Get<T>(this IConfiguration configuration)
public static T GetValue<T>(this IConfiguration configuration, string key, T defaultValue)
- 三 配置写入
现在我们需要确定一下 Provider 中的Data 属性是怎么写入的 ,还从开头的控制台程序入手 , 其中有这样一段代码 :
new ConfigurationBuilder().Add(new MemoryConfigurationSource() { InitialData = dicData }).Build();
我们重点看Add
方法
- 3.1 IConfigurationBuilder 相关源码
public interface IConfigurationBuilder
{
IList<IConfigurationSource> Sources {get;}
IConfigurationBuilder Add(IConfigurationSource source);
IConfigurationRoot Build();
}
/// <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>
/// 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);
}
}
Add就是把配置源 Source ,添加到内部的Sources列表。IConfigurationBuilder 除了Add方法还有一个 Build
方法。
IConfigurationBuilder 就像一个车间 ,Add一堆零件后 ,Build一下, 整出来一辆汽车(即:IConfigurationRoot)。 很明显IConfigurationBuilder 是一个构建者模式 。
我们再仔细看下 IConfigurationSource 中的Build
方法的实现逻辑 。
- 3.2 IConfigurationSource 相关源码
public interface IConfigurationSource
{
IConfigurationProvider Build(IConfigurationBuilder builder);
}
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);
}
}
可以看到 ,IConfigurationSource 只干了一件事 :其 Build 方法返回一个Provider。还拿开头的栗子来讲 ,MemoryConfigurationSource.Build 方法返回了 一个新的 MemoryConfigurationProvider 实例。
再看下 MemoryConfigurationProvider:
public class MemoryConfigurationProvider : ConfigurationProvider, IEnumerable<KeyValuePair<string, string>>
{
private readonly MemoryConfigurationSource _source;
/// <summary>
/// Initialize a new instance from the source.
/// </summary>
/// <param name="source">The source settings.</param>
public MemoryConfigurationProvider(MemoryConfigurationSource source)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
_source = source;
if (_source.InitialData != null)
{
foreach (var pair in _source.InitialData)
{
Data.Add(pair.Key, pair.Value);
}
}
}
/// <summary>
/// Add a new key and value pair.
/// </summary>
/// <param name="key">The configuration key.</param>
/// <param name="value">The configuration value.</param>
public void Add(string key, string value)
{
Data.Add(key, value);
}
}
MemoryConfigurationProvider 的构造函数完成了数据的初始化 。
到此为止,开头的那个栗子我们就梳理完了。
也许是我们开头那个例子过于简单了 , 写配置这块有点虎头蛇尾的感觉 。
我们再看一下主角 IConfigurationProvider , 发现其中定义的 Load 方法 ,我们还知道 ConfigurationProvider 是一个虚拟类 ,其 Load 方法还是个虚方法。
我们在翻一下源码看有没有其它类继承它呢 ? 如果有的话 ,它们是不是覆盖了 Load 方法呢?
果然我们发现了不少 Provider , 如 JsonConfigurationProvider,XmlConfigurationProvider,EnvironmentVariablesConfigurationProvider 等。
为方便讲解,我们以EnvironmentVariablesConfigurationProvider 为例,并精简了代码。 Load 方法负责了负载配置的任务。其他Provider 也是如此。所以说 Provider责任重大。
public class EnvironmentVariablesConfigurationProvider : ConfigurationProvider
{
private readonly string _prefix;
/// <summary>
/// Initializes a new instance.
/// </summary>
public EnvironmentVariablesConfigurationProvider() : this(string.Empty)
{ }
/// <summary>
/// Initializes a new instance with the specified prefix.
/// </summary>
/// <param name="prefix">A prefix used to filter the environment variables.</param>
public EnvironmentVariablesConfigurationProvider(string prefix)
{
_prefix = prefix ?? string.Empty;
}
/// <summary>
/// Loads the environment variables.
/// </summary>
public override void Load()
{
Load(Environment.GetEnvironmentVariables());
}
internal void Load(IDictionary envVariables)
{
var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var filteredEnvVariables = envVariables
.Cast<DictionaryEntry>()
.Where(entry => ((string)entry.Key).StartsWith(_prefix, StringComparison.OrdinalIgnoreCase));
foreach (var envVariable in filteredEnvVariables)
{
var key = ((string)envVariable.Key).Substring(_prefix.Length);
data[key] = (string)envVariable.Value;
}
Data = data;
}
}
但是 IConfigurationSource 呢 ,是不是可有可无 ?其实不是。IConfigurationSource 作用是提供原始配置,并将必要的信息传递给 Provider 。
我们现在大概知道 IConfigurationBuilder , IConfigurationProvider , IConfigurationRoot ,IConfigurationSource 之间的关系。
到此我们总结一下:
1 各种配置原始数据( IConfigurationSource) 通过 IConfigurationProvider 转成键值对格式的配置数据
2 IConfigurationBuilder 内部维护多个 IConfigurationSource,IConfigurationBuilder 中的 Build方法 构建出 IConfigurationRoot
3 IConfigurationRoot 内部维护多个 IConfigurationProvider,并通过 IConfigurationProvider 读取配置数据。