.Net Core 配置源码学习 (一)
一 背景
相比.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 { IEnumerableProviders { get; } } public class ConfigurationRoot : IConfigurationRoot, IDisposable { private readonly IList _providers; public IEnumerable Providers => _providers; /// /// 根据指定的Key获取或者设置对应的Value /// /// The configuration key. /// The configuration value. 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 { ////// The configuration key value pairs for this provider. /// protected IDictionary<string, string> Data { get; set; } /// /// 根据指定的 键 获取对应的 值, 获取到返回true,否则返回 false /// public virtual bool TryGet(string key, out string value) => Data.TryGetValue(key, out value); /// /// 加载数据 /// public virtual void Load() { } }
代码非常简单 ,每个Provider 维护了一个 Data (字典类型) 属性 , 配置就是从这个字典拿到的。 到此基本的读取配置逻辑我们已经知道了 ,给自己点个赞吧 ??
2.4 使用便利性
这时 ,我们可以再看开头那个栗子,读取配置其实还有这种写法 :
configurationRoot.Providers.First().TryGet("Key1", out string Val); Console.WriteLine("Key1=" + Val); //输出 Val=Value1
但是这种写法就感觉有些累赘了,还是 configurationRoot["Key1"] 更方便一些 , 有时我们甚至连"[" 和 "]" 都不想打 !
从使用的角度考虑,框架做了一些封装,并提供了不少扩展方法。但是基本的读取逻辑都是一样的。
如果我们引入 "Microsoft.Extensions.Configuration.Binder
" nuget包 ,读取配置操作时会发现一些新的方法。
比如配置和强模型绑定,并结合泛型使用 ,如:
public static T Get(this IConfiguration configuration) public static T GetValue (this IConfiguration configuration, string key, T defaultValue)
三 配置写入
现在我们需要找Provider中的Data 属性是怎么写入的 ,还从开头的控制台程序入手 , 其中有这样一段代码 :
new ConfigurationBuilder().Add(new MemoryConfigurationSource() { InitialData = dicData }).Build();
我们重点看Add
方法
3.1 IConfigurationBuilder 相关源码
public interface IConfigurationBuilder { IListSources {get;} IConfigurationBuilder Add(IConfigurationSource source); IConfigurationRoot Build(); } /// /// Used to build key/value based configuration settings for use in an application. /// public class ConfigurationBuilder : IConfigurationBuilder { /// /// Returns the sources used to obtain configuration values. /// public IList Sources { get; } = new List (); /// /// Adds a new configuration source. /// /// The configuration source to add. /// The same . public IConfigurationBuilder Add(IConfigurationSource source) { if (source == null) { throw new ArgumentNullException(nameof(source)); } Sources.Add(source); return this; } /// /// Builds an with keys and values from the set of providers registered in /// . /// /// An with keys and values from the registered providers. public IConfigurationRoot Build() { var providers = new List (); 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 { ////// The initial key value configuration pairs. /// public IEnumerable string, string>> InitialData { get; set; } /// /// Builds the for this source. /// /// The . /// A public IConfigurationProvider Build(IConfigurationBuilder builder) { return new MemoryConfigurationProvider(this); } }
可以看到 ,IConfigurationSource 只干了一件事 :其 Build 方法返回一个Provider。
还拿开头的栗子来讲 ,MemoryConfigurationSource.Build 方法返回了 一个新的 MemoryConfigurationProvider 实例。
再看下 MemoryConfigurationProvider
public class MemoryConfigurationProvider : ConfigurationProvider, IEnumerablestring, string>> { private readonly MemoryConfigurationSource _source; /// /// Initialize a new instance from the source. /// /// The source settings. 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); } } } /// /// Add a new key and value pair. /// /// The configuration key. /// The configuration value. public void Add(string key, string value) { Data.Add(key, value); } }
MemoryConfigurationProvider 的构造函数完成了数据的初始化 。
到此为止,开头的那个栗子我们就梳理完了。
也许是我们开头那个例子过于简单了 , 写配置这块有点虎头蛇尾的感觉 。
我们再看一下主角 IConfigurationProvider , 发现其中定义的 Load 方法 ,我们还知道 ConfigurationProvider 是一个虚拟类 ,其 Load 方法还是个虚方法。
我们在翻一下源码看有没有其它类继承它呢 ? 如果有的话 ,它们是不是覆盖了 Load 方法呢?
果然我们发现了 不少 Provider , 如 JsonConfigurationProvider,XmlConfigurationProvider,EnvironmentVariablesConfigurationProvider 等。
为方便讲解,我们以EnvironmentVariablesConfigurationProvider 为例,并精简了代码。 Load 方法负责了负载配置的任务。男团其他成员也是如此。所以说 Provider们 实力担当。
public class EnvironmentVariablesConfigurationProvider : ConfigurationProvider { private readonly string _prefix; ////// Initializes a new instance. /// public EnvironmentVariablesConfigurationProvider() : this(string.Empty) { } /// /// Initializes a new instance with the specified prefix. /// /// A prefix used to filter the environment variables. public EnvironmentVariablesConfigurationProvider(string prefix) { _prefix = prefix ?? string.Empty; } /// /// Loads the environment variables. /// public override void Load() { Load(Environment.GetEnvironmentVariables()); } internal void Load(IDictionary envVariables) { var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var filteredEnvVariables = envVariables .Cast () .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 读取配置数据
闯荡天下的天外流星全套剑谱奉上
限于篇幅 ,本篇介绍 配置读取 ,配置写入 2个知识点。
下一篇会介绍 配置变更监控 ,自定义配置 2个知识点。
感觉写的还凑合就点个赞吧 ??