源码解析.Net中IConfiguration配置的实现
前言
关于IConfituration的使用,我觉得大部分人都已经比较熟悉了,如果不熟悉的可以看这里。因为本篇不准备讲IConfiguration都是怎么使用的,但是在源码部分的解读,网上资源相对少一点,所以本篇准备着重源码这一块的设计,尽量的让读者能够理解它的内部实现。
IConfiguration类之间的关系
这里我整理了一个UML(可能不是那么标准,一些依赖关系没有体现)。可能直接看会有点不懂,下面我会慢慢讲这些东西。

源码解析
我们知道.net中的配置加载是有优先级的,如果有相同的key的话,一般后面加载的会覆盖前面的值,它们的优先级顺序如下图:

- 在Host.CreateDefaultBuilder(args)执行的代码中,将委托添加到一个IConfigurationBuilder的集合中,builder为HostBuilder,hostingContext为HostBuilderContext,config就是IConfigurationBuilder,我们先来看下加载的代码,如下:
builder.ConfigureAppConfiguration((hostingContext, config) =>
{
IHostEnvironment env = hostingContext.HostingEnvironment;
bool reloadOnChange = GetReloadConfigOnChangeValue(hostingContext);
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: reloadOnChange) //加载appsettings.json
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: reloadOnChange); //根据环境变量加载appsettings.json
if (env.IsDevelopment() && env.ApplicationName is { Length: > 0 })
{
var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
if (appAssembly is not null)
{
config.AddUserSecrets(appAssembly, optional: true, reloadOnChange: reloadOnChange); //开发环境加载UserSecrets
}
}
config.AddEnvironmentVariables(); //加载环境变量
if (args is { Length: > 0 })
{
config.AddCommandLine(args); //加载命令行参数
}
})
- 接下来我们拿其中一个例子,加载命令行参数,看看在AddCommandLine中都做了什么事情,是如何构建出IConfiguration对象的。
- 从UML图中可以看到,扩展对象都实现了IConfigurationSource,并且继承了抽象类ConfigurationProvider,源码如下:
//ConfigurationBuilder扩展类
public static class CommandLineConfigurationExtensions
{
public static IConfigurationBuilder AddCommandLine(this IConfigurationBuilder configurationBuilder, string[] args)
{
return configurationBuilder.AddCommandLine(args, switchMappings: null);
}
public static IConfigurationBuilder AddCommandLine(
this IConfigurationBuilder configurationBuilder,
string[] args,
IDictionary<string, string> switchMappings)
{
//SwitchMappings是Key映射,可以看微软文档
configurationBuilder.Add(new CommandLineConfigurationSource { Args = args, SwitchMappings = switchMappings });
return configurationBuilder;
}
}
public class CommandLineConfigurationSource : IConfigurationSource
{
public IDictionary<string, string> SwitchMappings { get; set; }
public IEnumerable<string> Args { get; set; }
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new CommandLineConfigurationProvider(Args, SwitchMappings);
}
}
//主要实现其Load方法,将kv加载到Data中,以便拿取。
public class CommandLineConfigurationProvider : ConfigurationProvider
{
private readonly Dictionary<string, string> _switchMappings;
public CommandLineConfigurationProvider(IEnumerable<string> args, IDictionary<string, string> switchMappings = null)
{
Args = args ?? throw new ArgumentNullException(nameof(args));
//默认情况下mapping是null
if (switchMappings != null)
{
//是个私有方法,源码就不贴了,可以自己下载来看看。
_switchMappings = GetValidatedSwitchMappingsCopy(switchMappings);
}
}
protected IEnumerable<string> Args { get; private set; }
//load方法,主要是来设置kv,因为data是个字典类型,要把kv给设置和获取到。
public override void Load()
{
var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
string key, value;
using (IEnumerator<string> enumerator = Args.GetEnumerator())
{
while (enumerator.MoveNext())
{
string currentArg = enumerator.Current;
int keyStartIndex = 0;
if (currentArg.StartsWith("--"))
{
keyStartIndex = 2;
}
else if (currentArg.StartsWith("-"))
{
keyStartIndex = 1;
}
else if (currentArg.StartsWith("/"))
{
currentArg = $"--{currentArg.Substring(1)}";
keyStartIndex = 2;
}
int separator = currentArg.IndexOf('=');
if (separator < 0)
{
if (keyStartIndex == 0)
{
continue;
}
if (_switchMappings != null && _switchMappings.TryGetValue(currentArg, out string mappedKey))
{
key = mappedKey;
}
else if (keyStartIndex == 1)
{
continue;
}
else
{
key = currentArg.Substring(keyStartIndex);
}
string previousKey = enumerator.Current;
if (!enumerator.MoveNext())
{
continue;
}
value = enumerator.Current;
}
else
{
string keySegment = currentArg.Substring(0, separator);
if (_switchMappings != null && _switchMappings.TryGetValue(keySegment, out string mappedKeySegment))
{
key = mappedKeySegment;
}
else if (keyStartIndex == 1)
{
throw new FormatException(SR.Format(SR.Error_ShortSwitchNotDefined, currentArg));
}
else
{
key = currentArg.Substring(keyStartIndex, separator - keyStartIndex);
}
value = currentArg.Substring(separator + 1);
}
data[key] = value;
}
}
Data = data;
}
}
2.在构建主机信息HostBuilder,下面只贴出主要代码,可以看到最后执行了ConfigurationBuilder的Build方法,构建IConfiguration,源码如下:
public class HostBuilder : IHostBuilder
{
private List<Action<IConfigurationBuilder>> _configureHostConfigActions = new List<Action<IConfigurationBuilder>>();
public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate)
{
_configureAppConfigActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
return this;
}
//即我们在main函数中看到的Build方法
public IHost Build()
{
//只能执行一次这个方法
if (_hostBuilt)
{
throw new InvalidOperationException(SR.BuildCalled);
}
_hostBuilt = true;
using var diagnosticListener = new DiagnosticListener("Microsoft.Extensions.Hosting");
const string hostBuildingEventName = "HostBuilding";
const string hostBuiltEventName = "HostBuilt";
if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(hostBuildingEventName))
{
Write(diagnosticListener, hostBuildingEventName, this);
}
//执行Host配置(应用程序执行路径,增加_dotnet环境变量,获取命令行参数,加载预配置)
BuildHostConfiguration();
//设置主机环境变量
CreateHostingEnvironment();
//设置上下文
CreateHostBuilderContext();
//构建程序配置(加载appsetting.json)
BuildAppConfiguration();
//构造容器,加载服务
CreateServiceProvider();
var host = _appServices.GetRequiredService<IHost>();
if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(hostBuiltEventName))
{
Write(diagnosticListener, hostBuiltEventName, host);
}
return host;
}
private void BuildAppConfiguration()
{
//对于已经加载过的配置不再重新加载
IConfigurationBuilder configBuilder = new ConfigurationBuilder()
.SetBasePath(_hostingEnvironment.ContentRootPath)
.AddConfiguration(_hostConfiguration, shouldDisposeConfiguration: true);
//注意这里是AppConfig
foreach (Action<HostBuilderContext, IConfigurationBuilder> buildAction in _configureAppConfigActions)
{
buildAction(_hostBuilderContext, configBuilder);
}
_appConfiguration = configBuilder.Build();
//将新的配置赋值给config
_hostBuilderContext.Configuration = _appConfiguration;
}
}
- ConfigurationBuilder的Build方法中加载对用Provider类里面的Load方法,加载配置信息。在获取对应Key值时,由加载的顺序,按照从后到前的顺序,依次查询key对应的value值,源码如下:
public class ConfigurationBuilder : IConfigurationBuilder
{
//所有的配置源集合
public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();
public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>();
public IConfigurationBuilder Add(IConfigurationSource source)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
Sources.Add(source);
return this;
}
public IConfigurationRoot Build()
{
var providers = new List<IConfigurationProvider>();
foreach (IConfigurationSource source in Sources)
{
//通过配置源构建provider对象。
IConfigurationProvider provider = source.Build(this);
providers.Add(provider);
}
//构建配置根对象
return new ConfigurationRoot(providers);
}
}
public class ConfigurationRoot : IConfigurationRoot, IDisposable
{
private readonly IList<IConfigurationProvider> _providers;
private readonly IList<IDisposable> _changeTokenRegistrations;
private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();
public ConfigurationRoot(IList<IConfigurationProvider> providers)
{
if (providers == null)
{
throw new ArgumentNullException(nameof(providers));
}
_providers = providers;
_changeTokenRegistrations = new List<IDisposable>(providers.Count);
foreach (IConfigurationProvider p in providers)
{
p.Load();
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
}
}
public IEnumerable<IConfigurationProvider> Providers => _providers;
//通过索引拿数据,比如其他获取value的方法,其都是扩展类中实现的,读者可以自己看下。
public string this[string key]
{
get
{
//从后往前查Porvider里面对应的KV,每个Provider里面都有一个Data(字典)对象,只获取第一个的。
for (int i = _providers.Count - 1; i >= 0; i--)
{
IConfigurationProvider provider = _providers[i];
if (provider.TryGet(key, out string value))
{
return value;
}
}
return null;
}
set
{
if (_providers.Count == 0)
{
throw new InvalidOperationException(SR.Error_NoSources);
}
//修改时,所有的key对应的value值都修改
foreach (IConfigurationProvider provider in _providers)
{
provider.Set(key, value);
}
}
}
}
总结
可以看到微软为了保留扩展性,增加了抽象类和抽象接口,让用户能够自定义的扩展实现自己的配置。
配置的加载顺序尤为重要,后加载的会覆盖前面的key。
文件监听,由于篇幅原因,不写那么多了,原理是通过ChangeToken和FileProvider实现的。
这里只是把核心代码给提出来,并不是全部代码,所以读者如果想看其他的,需要自己翻下源码。其实在读源码的过程中,一方面是为了探究实现原理,更大的收获是能够学习到人家巧妙的设计,能够用到自己的代码中。其实笔者的水平的也不是很高,如果有差错的地方,望读者能够提出来,以便我及时改正。
源码解析.Net中IConfiguration配置的实现的更多相关文章
- 源码解析.Net中DependencyInjection的实现
前言 笔者的这篇文章和上篇文章思路一样,不注重依赖注入的使用方法,更加注重源码的实现,我尽量的表达清楚内容,让读者能够真正的学到东西.如果有不太清楚依赖注入是什么或怎么在.Net项目中使用的话,请点击 ...
- 源码解析.Net中Host主机的构建过程
前言 本篇文章着重讲一下在.Net中Host主机的构建过程,依旧延续之前文章的思路,着重讲解其源码,如果有不知道有哪些用法的同学可以点击这里,废话不多说,咱们直接进入正题 Host构建过程 下图是我自 ...
- 源码解析.Net中Middleware的实现
前言 本篇继续之前的思路,不注重用法,如果还不知道有哪些用法的小伙伴,可以点击这里,微软文档说的很详细,在阅读本篇文章前,还是希望你对中间件有大致的了解,这样你读起来可能更加能够意会到意思.废话不多说 ...
- Spark 源码解析 : DAGScheduler中的DAG划分与提交
一.Spark 运行架构 Spark 运行架构如下图: 各个RDD之间存在着依赖关系,这些依赖关系形成有向无环图DAG,DAGScheduler对这些依赖关系形成的DAG,进行Stage划分,划分的规 ...
- 源码解析C#中PriorityQueue(优先级队列)的实现
前言 前段时间看到有大佬对.net 6.0新出的PriorityQueue(优先级队列)数据结构做了解析,但是没有源码分析,所以本着探究源码的心态,看了看并分享出来.它不像普通队列先进先出(FIFO) ...
- multiprocessing 源码解析 更新中......
一.参考链接 1.源码包下载·链接: https://pypi.org/search/?q=multiprocessing+ 2.源码包 链接:https://pan.baidu.com/s/1j ...
- spring boot 源码解析52-actuate中MVCEndPoint解析
今天有个别项目的jolokia的endpoint不能访问,调试源码发现:endpoint.enabled的开关导致的. 关于Endpoint, <Springboot Endpoint之二:En ...
- 源码解析Android中View的measure量算过程
Android中的Veiw从内存中到呈现在UI界面上需要依次经历三个阶段:量算 -> 布局 -> 绘图,关于View的量算.布局.绘图的总体机制可参见博文< Android中View ...
- Spring5源码解析2-register方法注册配置类
接上回已经讲完了this()方法,现在来看register(annotatedClasses);方法. // new AnnotationConfigApplicationContext(AppCon ...
随机推荐
- LeetCode解题记录(贪心算法)(一)
1. 前言 目前得到一本不错的算法书籍,页数不多,挺符合我的需要,于是正好借这个机会来好好的系统的刷一下算法题,一来呢,是可以给部分同学提供解题思路,和一些自己的思考,二来呢,我也可以在需要复习的时候 ...
- ansible 任务执行
ansible 任务执行模式 Ansible 系统由控制主机对被管节点的操作方式可分为两类,即adhoc和playbook: ad-hoc模式(点对点模式)• ad-hoc模式(点对点模式) 使用单个 ...
- Redis和Memcached到底有什么区别?
前言 我们都知道,Redis和Memcached都是内存数据库,它们的访问速度非常之快.但我们在开发过程中,这两个内存数据库,我们到底要如何选择呢?它们的优劣都有哪些?为什么现在看Redis要比Mem ...
- leetcode1047
思路分析,这题是在栈分类的题目的,所以顺便复习下数据结构,先用java现成的,就当复习了. 1.判断栈顶和插入的元素是否相等,如果相等,那就出栈,不相等入栈结束 java版:
- js定时器中引用的外部函数如何传递参数
问题:比如在一个点击事件中我需要将点击事件参数event传入到定时器中,如果只是单纯的在setTimeout(timer(e),1000)中写上括弧e,则该定时器不会等到1s才执行,而是会立即执行.那 ...
- Maven多模块开发SpringBoot项目自定义第三方依赖版本
参考:官方文档 - Build System of Maven https://blog.didispace.com/books/spring-boot-reference/IX. 'How-to' ...
- Vulnhub -- DC1靶机渗透
信息收集 开启了 22端口,ssh服务 80,http服务 111,RPC服务 先观察80端口 发现采用的是Drupal的cms 目录爆破: 但是,没有找到可以直接利用的点 登录的页面进行SQL注入即 ...
- 记一次lombok踩坑记
引言 今天中午正在带着耳机遨游在代码的世界里,被运营在群里@了,气冲冲的反问我最近有删生产的用户数据的吗?我肯定客气的回答道没有呀?生产的数据我怎么能随随便便可以删除,这可是公司的红线,再说了我也没有 ...
- redis的单线程
一.redis为什么这么快 1.完全基于内存,绝大部分请求是纯粹的内存操作,非常快速.数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1):2.数据结构简单 ...
- 用activiti实现类似钉钉审批流程-附整个系统源码
前言 目前市场上有很多开源平台没有整合工作流,即使有,也是价格不菲的商业版,来看这篇文章的估计也了解了行情,肯定不便宜.我这个快速开发平台在系统基础功能(用户管理,部门管理-)上整合了工作流,你可以直 ...