前言
友情提示:建议阅读本文之前先了解下.Net Core配置体系相关,也可以参考本人之前的文章《.Net Core Configuration源码探究 》然后对.Net Core的Configuration体系有一定的了解,使得理解起来更清晰。
在.Net6中关于配置相关多出一个关于配置相关的类ConfigurationManager,如果大概了解过Minimal API中的WebApplicationBuilder类相信你肯定发现了,在Minimal API中的配置相关属性Configuration正是ConfigurationManager的对象。ConfigurationManager本身并没有引入新的技术,也不是一个体系,只是在原来的基础上进行了进一步的封装,使得配置体系有了一个新的外观操作,暂且可以理解为新瓶装旧酒。本文我们就来了解下ConfigurationManager类,来看下微软为何在.Net6中会引入这么一个新的操作。
使用方式
关于.Net6中ConfigurationManager的使用方式,我们先通过简单的示例演示一下
ConfigurationManager configurationManager = new();configurationManager.AddjsonFile("appsettings.json",true,reloadOnChange:true);string serviceName = configurationManager["ServiceName"];Console.WriteLine(serviceName);当然,关于获取值得其他方式。比如GetSection、GetChildren相关方法还是可以继续使用的,或者使用Binder扩展包相关的Get<string>()、GetValue<NacosOptions>("nacos")类似的方法也照样可以使用。那它和之前的.Net Core上的配置使用起来有什么不一样呢,我们看一下之前配置相关的使用方式,如下所示
IConfigurationBuilder configurationBuilder = new ConfigurationBuilder().AddJsonFile("appsettings.json");IConfiguration configuration = configurationBuilder.Build();string serviceName = configuration["ServiceName"];Console.WriteLine(serviceName);这里需要注意的是,如果你是使用ConfigurationManager或者是IConfiguration封装的Helper类相关,并没有通过框架体系默认注入的时候,一定要注意将其设置为单例模式。其实这个很好理解,先不说每次用的时候都去实例化带来的内存CPU啥的三高问题。读取配置文件本质不就是把数据读到内存中吗?内存中有一份缓存这就好了,每次都去重新实例去读本身就是一种不规范的方式。许多时候如果你实在不知道该定义成什么样的生命周期,可以参考微软的实现方式,以ConfigurationManager为例,我们可以参考WebApplicationBuilder类中对ConfigurationManager注册的生命周期[点击查看源码]
public ConfigurationManager Configuration { get; } = new();//这里注册为了单例模式Services.AddSingleton<IConfiguration>(_ => Configuration);通过上面我们演示的示例可以看出在ConfigurationManager的时候注册配置和读取配置相关都只是使用了这一个类。而在之前的配置体系中,注册配置需要使用IConfigurationBuilder,然后通过Build方法得到IConfiguration实例,然后读取是通过IConfiguration实例进行的。本身操作配置的时候IConfigurationBuilder和IConfiguration是满足单一职责原则没问题,像读取配置这种基础操作,应该是越简单越好,所以微软才进一步封装了ConfigurationManager来简化配置相关的操作。
在.Net6中微软并没有放弃IConfigurationBuilder和IConfiguration,因为这是操作配置文件的基础类,微软只是借助了它们两个在上面做了进一层封装而已,这个是需要我们了解的。
源码探究
上面我们了解了新的ConfigurationManager的使用方式,这里其实我们有疑问了,为什么ConfigurationManager可以进行注册和读取操作。上面我提到过ConfigurationManager本身就是新瓶装旧酒,而且它只是针对原有的配置体系做了一个新的外观,接下来哦我们就从源码入手,看一下它的实现方式。
定义入手
首先来看一下ConfigurationManager的的定义,如下所示[点击查看源码]
public sealed class ConfigurationManager : IConfigurationBuilder, IConfigurationRoot, IDisposable{}其实只看它的定义就可以解答我们心中的大部分疑惑了,之所以ConfigurationManager能够满足IConfigurationBuilder和IConfigurationRoot这两个操作的功能是因为它本身就是实现了这两个接口,集它们的功能于一身了,IConfigurationRoot接口本身就集成自IConfiguration接口。因此如果给ConfigurationManager换个马甲的话你就会发现还是原来的配方还是原来的味道
ConfigurationManager configurationManager = new();IConfigurationBuilder configurationBuilder = configurationManager.AddJsonFile("appsettings.json", true, reloadOnChange: true);//尽管放心的调用Build完全不影响啥IConfiguration configuration = configurationBuilder.Build();string serviceName = configuration["ServiceName"];Console.WriteLine(serviceName);这种写法只是为了更好的看清它的本质,如果真实操作这么写,确实有点画蛇添足了,因为ConfigurationManager本身就是为了简化我们的操作。
认识IConfigurationBuilder和IConfiguration
通过上面我们了解到ConfigurationManager可以直接注册过配置文件就可以直接去操作配置文件里的内容,这一步是肯定通过转换得到的,毕竟之前的方式我们是通过IConfigurationBuilder的Build操作得到的IConfiguration的实例,那么我们就先来看下原始的方式是如何实现的。这里需要从IConfigurationBuilder的默认实现类ConfigurationBuilder说起,它的实现很简单[点击查看源码]
public class ConfigurationBuilder : IConfigurationBuilder{ /// <summary> /// 添加的数据源被存放到了这里 /// </summary> public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>(); public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>(); /// <summary> /// 添加IConfigurationSource数据源 /// </summary> /// <returns></returns> public IConfigurationBuilder Add(IConfigurationSource source) { if (source == null) { throw new ArgumentNullException(nameof(source)); } Sources.Add(source); return this; } public IConfigurationRoot Build() { //获取所有添加的IConfigurationSource里的IConfigurationProvider var providers = new List<IConfigurationProvider>(); foreach (var source in Sources) { var provider = source.Build(this); providers.Add(provider); } //用providers去实例化ConfigurationRoot return new ConfigurationRoot(providers); }}这里我们来解释一下,其实我们注册配置相关的时候比如AddJsonFile()、AddEnvironmentVariables()、AddInMemoryCollection()等等它们其实都是扩展方法,本质就是添加IConfigurationSource实例,而IConfigurationBuilder的Build本质操作其实就是在IConfigurationSource集合中得到IConfigurationProvider集合,因真正从配置读取到的数据都是包含在IConfigurationProvider实例中的,ConfigurationRoot通过一系列的封装,让我们可以更便捷的得到配置里相关的信息。这就是ConfigurationBuilder的工作方式,也是配置体系的核心原理。 } } set { lock (_providerLock) { //也可以把值放到IConfigurationProvider集合中 ConfigurationRoot.SetConfiguration(_providers, key, value); } }}
其中_providers中的值是我们在AddSource方法中添加进来的,这里的本质其实还是针对ConfigurationRoot做了封装。ConfigurationRoot实现了IConfigurationRoot接口,IConfigurationRoot实现了IConfiguration接口。而ConfigurationRoot的GetConfiguration方法和SetConfiguration是最直观体现ConfigurationRoot本质就是IConfigurationProvider包装的证据。我们来看一下ConfigurationRoot这两个方法的实现[点击查看源码]
internal static string GetConfiguration(IList<IConfigurationProvider> providers, string key){ //倒序遍历providers,因为Configuration采用的后来者居上的方式,即后注册的Key会覆盖先前注册的Key for (int i = providers.Count - 1; i >= 0; i--) { IConfigurationProvider provider = providers[i]; //如果找到Key的值就直接返回 if (provider.TryGet(key, out string value)) { return value; } } return null;}internal static void SetConfiguration(IList<IConfigurationProvider> providers, string key, string value){ if (providers.Count == 0) { throw new InvalidOperationException(""); } //给每个provider都Set这个键值,虽然浪费了一部分内存,但是可以最快的获取 foreach (IConfigurationProvider provider in providers) { provider.Set(key, value); }}关于GetSection的方法实现,本质上是返回ConfigurationSection实例,ConfigurationSection本身也是实现了IConfiguration接口,所有关于配置获取的操作出口都是面向IConfiguration的。
public IConfigurationSection GetSection(string key) => new ConfigurationSection(this, key);
GetChildren方法是获取配置的所有子节点的操作,本质是返回IConfigurationSection的集合,实现方式如如下
private readonly object _providerLock = new();public IEnumerable<IConfigurationSection> GetChildren(){ lock (_providerLock) { //调用了GetChildrenImplementation方法 return this.GetChildrenImplementation(null).ToList(); }}这里调用了GetChildrenImplementation方法,而GetChildrenImplementation是一个扩展方法,我们来看一下它的实现[点击查看源码]
internal static IEnumerable<IConfigurationSection> GetChildrenImplementation(this IConfigurationRoot root, string path){ //在当前ConfigurationManager实例中获取到所有的IConfigurationProvider实例 //然后包装成IConfigurationSection集合 return root.Providers .Aggregate(Enumerable.Empty<string>(), (seed, source) => source.GetChildKeys(seed, path)) .Distinct(StringComparer.OrdinalIgnoreCase) .Select(key => root.GetSection(path == null ? key : ConfigurationPath.Combine(path, key)));}通过这段代码再次应验了那句话所有获取配置数据都是面向IConfiguration接口的,数据本质都是来自于IConfigurationProvider读取配置源中的数据。
ConfigurationBuilderProperties
在ConfigurationManager中还包含了一个Properties属性,这个属性本质来源于IConfigurationBuilder。在IConfigurationBuilder中它和IConfigurationSource是平行关系,IConfigurationSource用于在配置源中获取数据,而Properties是在内存中获取数据,本质是一个字典
private readonly ConfigurationBuilderProperties _properties = new ConfigurationBuilderProperties(this);IDictionary<string, object> IConfigurationBuilder.Properties => _properties;
这里咱们就不细说这个具体实现了,我们知道它本质是字典,然后操作都是纯内存的操作即可,来看一下它的定义[点击查看源码]
private class ConfigurationBuilderProperties : IDictionary<string, object>{}基本上许多缓存机制即内存操作都是基于字典做的一部分实现,所以大家对这个实现的方式有一定的认识即可,即使在配置体系的核心操作ConfigurationProvider中读取的配置数据也是存放在字典中的。这个可以去ConfigurationProvider类中自行了解一下[点击查看源码]
protected IDictionary<string, string> Data { get; set; }protected ConfigurationProvider(){ Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);}总结
通过本文我们了解到了.Net6配置体系中的新成员ConfigurationManager,它是一个新内容但不是一个新技术,因为它是在原有的配置体系中封装了一个新的外观,以简化原来对配置相关的操作。原来对配置的操作需要涉及IConfigurationBuilder和IConfiguration两个抽象操作,而新的ConfigurationManager只需要一个类,其本质是因为ConfigurationManage同时实现了IConfigurationBuilder和IConfiguration接口,拥有了他们两个体系的能力。整体来说重写了IConfigurationBuilder的实现为主,而读取操作主要还是借助原来的ConfigurationRoot对节点数据的读取操作。








