本文转载自微信公众号「老王Plus」,作者老王Plus的老王。转载本文请联系老王Plus公众号。
Dotnet 6.0 大家都装了没?
我打算开个专题,系统地写一写 Dotnet 6.0 在各个方面的特性,以及全新的开发方式。也是因为最近讨论 6.0 比较多,看到很多人的畏难情绪,所以打算写写相关的内容。
了解了,就不怕了。
要写的内容很多,我会分几篇来写。
今天是第一篇:ConfigurationManager,配置管理器。
ConfigurationManager 是干什么用的?
引用微软官方的说法:ConfigurationManager 是用来支持 ASP.Net Core 的新的 WebApplication 模型。这个模型主要的作用是在一些特定的场景下(后面我们会说到),用来简化 ASP.NET Core 的启动代码。
当然,如果我们去看 MSDN 的文档,会发现 ConfigurationManager 本身实现还是挺复杂的。好在,大多数情况下,这是一个半隐藏的东西,你可能都意识不到你已经用到了它。
那它到底是干什么用的?
这得从 .Net 5.0 的 Configuration 说起。
.Net 5.0 里的 Configuration
Configuration 配置,从 3.1 到 5.0,增加了很多很多的配置类型,如果你去 MSDN 上看,有好几大篇。
这里面,我们接触最多的是两个:
- IConfigurationBuilder - 这个接口主要用来增加配置源,并在构建器上调用 Build() 来读取每个配置源,并形成最终的配置
- IConfigurationRoot - 这就是上面 Build() 完成后形成的配置,我们会从这里面读配置值
在实际应用中,IConfigurationBuilder 通常被我们用做配置源列表的包装器,最常用的是通过 AddJsonFile(),将配置源添加到源列表中。看到 AddJsonFile(),你是不是想到了什么?
简单来说,IConfigurationBuilder 是这样的:
- public interface IConfigurationBuilder
- {
- IDictionary<string, object> Properties { get; }
- IList<IConfigurationSource> Sources { get; }
- IConfigurationBuilder Add(IConfigurationSource source);
- IConfigurationRoot Build();
- }
而 IConfigurationRoot,里面放的是经过合并的配置值。这个合并需要注意一下,多个配置源逐个加入时,相同名称的项,后面的配置会覆盖前面的项。
在 .Net 5.0 以前,IConfigurationBuilder 和 IConfigurationRoot 接口分别由 ConfigurationBuilder 和 ConfigurationRoot 实现。使用时通常是这么写:
- var builder = new ConfigurationBuilder();
- // 加入静态值
- builder.AddInMemoryCollection(new Dictionary<string, string>
- {
- { "MyKey", "MyValue" },
- });
- // 加入文件
- builder.AddJsonFile("appsettings.json");
- IConfigurationRoot config = builder.Build();
- string value = config["MyKey"]; // 取一个值
- IConfigurationSection section = config.GetSection("SubSection"); // 取一个节
这是在 Console 程序中。
在 ASP.NET Core 中,通常不需要这么显式的 new 和 Build(),但事实上也是调用的这个接口。
在默认的 ConfigurationBuilder 实现中,调用 Build() 将遍历所有的源,加载 Provider 程序,并将它们传递给一个新的ConfigurationRoot 实例:
- public IConfigurationRoot Build()
- {
- var providers = new List<IConfigurationProvider>();
- foreach (IConfigurationSource source in Sources)
- {
- IConfigurationProvider provider = source.Build(this);
- providers.Add(provider);
- }
- return new ConfigurationRoot(providers);
- }
然后,ConfigurationRoot 依次遍历每个提供程序并加载配置值:
- public class ConfigurationRoot : IConfigurationRoot, IDisposable
- {
- private readonly IList<IConfigurationProvider> _providers;
- private readonly IList<IDisposable> _changeTokenRegistrations;
- public ConfigurationRoot(IList<IConfigurationProvider> providers)
- {
- _providers = providers;
- _changeTokenRegistrations = new List<IDisposable>(providers.Count);
- foreach (IConfigurationProvider p in providers)
- {
- p.Load();
- _changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
- }
- }
- // ...
- }
这种架构,会有个小问题。在团队开发的时候,在没有统一沟通的情况下,有可能会在多处调用 Build()。当然这也没什么问题。只不过,正常来说这个没有必要,毕竟这是在读文件,会很慢。
不过,在 .Net 5.0 之前,都是这么做。
可喜的是,在 .Net 6.0 里,微软也注意到这个问题,并引入了一个新的类型:ConfigurationManager。
.Net 6.0 里的 ConfigurationManager
ConfigurationManager 是一个 .Net 6.0 中新的配置类型。这个类型也同样实现了两个接口:IConfigurationBuilder 和 IConfigurationRoot。那么,通过这两个接口的实现,我们可以简化上一节讲到的 .Net 5.0 中的通用模式。
不过,还是有一点点区别。这里 IConfigurationBuilder 将源保存为 IList:
- public interface IConfigurationBuilder
- {
- IList<IConfigurationSource> Sources { get; }
- // ...
- }
这样做有一个好处,就是对于源 IList,就有了 Add() 和 Remove() 方法,我们可以在不知道 ConfigurationManager 的情况下增加和删除配置提供程序。
- private class ConfigurationSources : IList<IConfigurationSource>
- {
- private readonly List<IConfigurationSource> _sources = new();
- private readonly ConfigurationManager _config;
- public ConfigurationSources(ConfigurationManager config)
- {
- _config = config;
- }
- public void Add(IConfigurationSource source)
- {
- _sources.Add(source);
- _config.AddSource(source); // 增加源
- }
- public bool Remove(IConfigurationSource source)
- {
- var removed = _sources.Remove(source); // 删除源
- _config.ReloadSources(); // 重新加载源
- return removed;
- }
- // ...
- }
这样做可以确保 ConfigurationManager 在改变源的 IList 时,能自动加载源的配置数据。
看一下 ConfigurationManager.AddSource 的定义:
- public class ConfigurationManager
- {
- private void AddSource(IConfigurationSource source)
- {
- lock (_providerLock)
- {
- IConfigurationProvider provider = source.Build(this);
- _providers.Add(provider);
- provider.Load();
- _changeTokenRegistrations.Add(ChangeToken.OnChange(() => provider.GetReloadToken(), () => RaiseChanged()));
- }
- RaiseChanged();
- }
- }
这个方法会立即调用 IConfigurationSource 的 Build() 方法来创建 IConfigurationProvider,并加入到源列表中。
下面,这个方法就调用 IConfigurationProvider 的 Load() 方法,将数据加载到 Provider。
这个方法解决了一件事,就是当我们需要从不同的位置向 IConfigurationBuilder 加入各种源时,源只需要加载一次,而且只会加载一次。
上面的代码是增加源。当我们需要 Remove() 源,或者干脆清除掉全部的源 Clear() 时,就需要调用 ReloadSource():
- private void ReloadSources()
- {
- lock (_providerLock)
- {
- DisposeRegistrationsAndProvidersUnsynchronized();
- _changeTokenRegistrations.Clear();
- _providers.Clear();
- foreach (var source in _sources)
- {
- _providers.Add(source.Build(this));
- }
- foreach (var p in _providers)
- {
- p.Load();
- _changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
- }
- }
- RaiseChanged();
- }
当然,看懂上面的代码,也就明白两件事:
- 增加源是代码最小的,加到列表中就行了 ;
- 删除或更改源代码会有点大,需要重新遍历加载所有源。
如果需要对配置的源进行大量的操作,这样的代价会比较大。不过,这种情况会很不常见。
总结一下
.Net 6.0 引入了一个新的 ConfigurationManager,用来优化配置的构建。
ConfigurationManager 同样实现了 ConfigurationBuilder 和 ConfigurationRoot。这算是个兼容性的设置,主要是为了支持 WebHostBuilder 和 HostBuilder 中对配置的调用。同时,也兼容了早期代码中的调用方式。所以,代码升级时,相关配置调用的部分,如果不想改代码,是完全可以的。而如果想做点改动,就换成使用 ConfigurationManager,或者通过 WebApplicationBuilder 来加载(会自动调用 ConfigurationManager),应用程序会有更好的性能。
这算是一个小礼物,相信也是微软权衡以后的结果。