[探索 .NET 6]01 揭开 ConfigurationManager 的面纱
Posted DotNET技术圈
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[探索 .NET 6]01 揭开 ConfigurationManager 的面纱相关的知识,希望对你有一定的参考价值。
在这个系列中,我将探索一下 .NET 6 中的一些新特性。已经有很多关于 .NET 6 的内容,包括很多来自 .NET 和 ASP.NET 团队本身的文章。在这个系列中,我将探索一下这些特性背后的一些代码。
在这第一篇文章中,来研究一下 ConfigurationManager
类,讲一下为什么要新增这个类,并看一下它的的一些实现代码。
WebApplication
模型,用于简化 ASP.NET Core 的启动代码。然而 ConfigurationManager
在很大程度上是一个实现细节。它的引入是为了优化一个特定的场景(我很快会讲),但在大多数情况下,你不需要(也不会)知道你在使用它。在我们讨论 ConfigurationManager
本身之前,我们先来看看它所取代的东西和原因。
Build()
读取每个配置源,并构建最终的配置。IConfigurationRoot - 代表最终“构建”好的配置。
IConfigurationBuilder
接口主要是一个围绕配置源列表的封装器。配置提供者通常包括扩展方法(如 AddJsonFile()
和 AddAzureKeyVault()
),将配置源添加到 Sources
列表中。
IDictionary< IList<IConfigurationSource> Sources IConfigurationBuilder IConfigurationRoot 代表最终“层”的配置值,结合了每个配置源的所有值,以提供所有配置值的最终“平面”视图。后者配置提供者(环境变量)覆盖了前者配置提供者(appsettings.json
、sharedsettings.json
)添加的值。
在 .NET 5 及以前的版本中,IConfigurationBuilder
和 IConfigurationRoot
接口分别由 ConfigurationBuilder
和 ConfigurationRoot
实现。如果你直接使用这些类型,你可能会这样做:
builder = ConfigurationBuilder();
Dictionary< ,
);
= config[里面从 Azure Key Vault 读取 secrects 的建议方式: config.AddJsonFile( config.AddEnvironmentVariables();
(context.HostingEnvironment.IsProduction())
IConfigurationRoot partialConfig = config.Build(); keyVaultName = partialConfig[ secretClient = SecretClient(
Uri( DefaultAzureCredential());
config.AddAzureKeyVault(secretClient, KeyVaultSecretManager());
)
配置 Azure Key Vault 提供者需要一个配置值,所以你陷入了一个鸡和蛋的问题--在你建立配置之前,你无法添加配置源。
解决办法是:
添加“初始”配置值;
通过调用 IConfigurationBuilder.Build()
构建“部分”配置结果;
从生成的 IConfigurationRoot
中检索所需的配置值;
使用这些值来添加剩余的配置源;
框架隐含地调用 IConfigurationBuilder.Build()
,生成最终的 IConfigurationRoot
并将其用于最终的应用配置。
这整个过程有点乱,但它本身并没有什么问题,那么缺点是什么呢?
缺点是我们必须调用 Build()
两次:一次是只使用第一个源来构建 IConfigurationRoot
,另一次是使用所有源来构建 IConfiguartionRoot
,包括 Azure Key Vault 源。
在默认的 ConfigurationBuilder
实现中,调用 Build()
会遍历所有的源,加载提供者,并将这些传递给 ConfigurationRoot
的一个新实例。
IConfigurationRoot providers = List<IConfigurationProvider>();
(IConfigurationSource source Sources)
IConfigurationProvider provider = source.Build( providers.Add(provider);
ConfigurationRoot(providers);
然后,ConfigurationRoot
依次循环遍历这些提供者,并加载配置值。
: IList<IConfigurationProvider> _providers;
IList<IDisposable> _changeTokenRegistrations;
_providers = providers;
_changeTokenRegistrations = List<IDisposable>(providers.Count);
(IConfigurationProvider p providers)
p.Load();
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
两次,那么所有这些都会发生两次。一般来说,从配置源获取数据一次以上并无大碍,但这是不必要的工作,而且经常涉及到(相对缓慢的)文件读取等。
这是一种常见的模式,所以在 .NET 6 中引入了一个新的类型来避免这种“重新构建”,即 ConfigurationManager
。
和 IConfigurationRoot
。通过将这两种实现结合在一个单一的类型中,.NET 6 可以优化上一节中展示的常见模式。有了 ConfigurationManager
,当 IConfigurationSource
被添加时(例如当你调用 AddJsonFile()
时),提供者被立即加载,配置被更新。这可以避免在部分构建的情况下不得不多次加载配置源。
由于 IConfigurationBuilder
接口将源作为 IList<IConfigurationSource>
公开,因此实现这一点比听起来要难一些:
IList<IConfigurationSource> Sources 的角度来看,这个问题是 IList<>
暴露了 Add()
和 Remove()
函数。如果使用一个简单的 List<>
,消费者可以在 ConfigurationManager
不知道的情况下添加和删除配置提供者。为了解决这个问题,ConfigurationManager
使用一个自定义的 IList<>
实现。这包含对 ConfigurationManager
实例的引用,这样任何变化都可以反映在配置中:
: List<IConfigurationSource> _sources = ConfigurationManager _config;
_config = config;
_sources.Add(source);
_config.AddSource(source);
removed = _sources.Remove(source);
_config.ReloadSources(); removed;
实现,ConfigurationManager
确保每当有新的源被添加时就调用 AddSource()
。这就是 ConfigurationManager
的优势所在:调用 AddSource()
可以立即加载源:
(_providerLock)
IConfigurationProvider provider = source.Build( _providers.Add(provider);
provider.Load();
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => provider.GetReloadToken(), () => RaiseChanged()));
RaiseChanged();
这个方法立即在 IConfigurationSource
上调用 Build
来创建 IConfigurationProvider
,并将其添加到提供者列表中。
接下来,该方法调用 IConfigurationProvider.Load()
。这将把数据加载到提供者中,(例如从环境变量、JSON 文件或 Azure Key Vault),这是“昂贵”的步骤,而这一切就是为了加载数据 在“正常”情况下,你只需向 IConfigurationBuilder
添加源,并可能需要多次构建它,这就给出了“最佳”方法——源被加载一次,且只有一次。
ConfigurationManager
中 Build()
的实现现在什么都没做,只是返回它自己:
函数,如 Clear()
、Remove()
或索引器,ConfigurationManager
就必须调用 ReloadSources()
: (_providerLock)
DisposeRegistrationsAndProvidersUnsynchronized();
_changeTokenRegistrations.Clear();
_providers.Clear();
(source _sources)
_providers.Add(source.Build(
(p _providers)
p.Load();
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
RaiseChanged();
正如你所看到的,如果任何一个源改变了,ConfigurationManager
必须删除所有的东西并重新开始,迭代每个源,重新加载它们。如果你要对配置源进行大量的操作,这很快就会变得很昂贵,而且会完全否定 ConfigurationManager
的原始优势。
当然,删除源是非常罕见的,所以 ConfigurationManager
是为最常见的情况而优化的。谁能猜到呢?
下表给出了使用 ConfigurationBuilder
和 ConfigurationManager
的各种操作的相对成本的最终总结:
还是 ConfigurationBuilder
?也许不应该。
在 .NET 6 中引入的新的 WebApplicationBuilder
使用 ConfigurationManager
,它优化了我上面描述的使用情况,即你需要“部分构建”你的配置。
然而,ASP.NET Core 早期版本中引入的 WebHostBuilder
或 HostBuilder
在 .NET 6 中仍然非常受支持,它们继续在幕后使用 ConfigurationBuilder
和 ConfigurationRoot
类型。
我认为唯一需要注意的情况是,如果你在某个地方依赖 IConfigurationBuilder
或 IConfigurationRoot
作为具体类型的 ConfigurationBuilder
或 ConfigurationRoot
。这在我看来是非常不太可能发生的,如果你依赖这一点,我很想知道原因。
但除了这个小众的例外,“老”类型不会消失,所以没有必要担心。如果你需要进行“部分构建”,并且你使用了新的 WebApplicationBuilder
,那么你的应用程序将会有更高的性能,这一点你应该感到高兴。
类型,并在最小(Minimal) API 示例中被新的 WebApplicationBuilder
所使用。引入 ConfigurationManager
是为了优化一种常见的情况,即你需要“部分构建”配置。这通常是因为配置提供者本身需要一些配置,例如,从 Azure Key Vault 加载 secrects,需要配置表明要使用哪个 Vault 库。ConfigurationManager
优化了这种情况:它在添加源时立即加载,而不是等到你调用 Build()
。这就避免了在“部分构建”情况下“重建”配置的需要,其代价是其他不常见操作(如删除一个源)可能变得更昂贵的。
原文:bit.ly/3227vka
作者:Andrew Lock
翻译:精致码农
慎入秘境探索之一个.NET 对象从内存分配到内存回收
以上是关于[探索 .NET 6]01 揭开 ConfigurationManager 的面纱的主要内容,如果未能解决你的问题,请参考以下文章