探索 .NET 6 中的 ConfigurationManager
这是该系列的第一篇文章:探索 .NET 6:
- Part 1 - 探索 .NET 6 中的 ConfigurationManager(当前文章)
- Part 2 - 比较 WebApplicationBuilder 和 Generic Host
- Part 3 - 探索 WebApplicationBuilder 背后的代码
- Part 4 - 使用 WebApplication 构建中间件管道
- Part 5 - 使用 WebApplicationBuilder 支持 EF Core 迁移
- Part 6 - 在 .NET 6 中使用 WebApplicationFactory 支持集成测试
- Part 7 - 用于 .NET 6 中 ASP.NET Core的分析器
- Part 8 - 使用源代码生成器提高日志记录性能
- Part 9 - 源代码生成器更新:增量生成器
- Part 10 - .NET 6 中新的依赖关系注入功能
- Part 11 - [CallerArgumentExpression] and throw helpers
- Part 12 - 将基于 .NET 5 启动版本的应用升级到 .NET 6
在本系列中,我将介绍 .NET 6 中推出的一些新功能。在.NET 6上已经写了很多内容,包括来自.NET和 ASP.NET 团队本身的大量帖子。在本系列中,我将介绍其中一些功能背后的一些代码。
在系列文章的第一篇中,我们将探索ConfigurationManager
类,为什么添加它,以及用于实现它的一些代码。
稍等,什么是ConfigurationManager
?
如果你的第一反应是"什么是配置管理器",那么别担心,你没有错过一个大公告!
新增的 ConfigurationManager
是用来支持 ASP.NET Core 的新 WebApplication 模型,用于简化 ASP.NET Core 启动代码。但是,ConfigurationManager
在很大程度上是一个实现细节。引入它是为了优化特定方案(我稍后会介绍),但在大多数情况下,你不需要(也不会)知道你正在使用它。
在介绍ConfigurationManager
本身之前,我们将了解它要替换的内容及其原因。
.NET 5 中的配置
.NET 5 公开了有关配置的多个类型,但直接在应用中使用的两个主要类型是:
IConfigurationBuilder
- 用于添加配置源。在 builder 调用 Build() 将读取每个配置源,并生成最终配置。IConfigurationRoot
- 表示最终的"构建"配置。
IConfigurationBuilder
接口主要是围绕配置源列表的包装器。
配置提供程序通常包括将配置源添加到Sources
列表的扩展方法(如 AddJsonFile()
和 AddAzureKeyVault()
)。
public interface IConfigurationBuilder
{
IDictionary<string, object> Properties { get; }
IList<IConfigurationSource> Sources { get; }
IConfigurationBuilder Add(IConfigurationSource source);
IConfigurationRoot Build();
}
同时,IConfigurationRoot
表示最终的"分层"配置值,将来自每个配置源的所有值组合在一起,以提供所有配置值的最终"平面"视图。
后来的配置提供程序(环境变量)将覆盖早期配置提供程序(appsettings.json、sharedsettings.json)配置的值。
在 .NET 5 及更早版本中,IConfigurationBuilder
和 IConfigurationRoot
接口分别由 ConfigurationBuilder
和 ConfigurationRoot
实现。如果您直接使用这些类型,则可以执行以下操作:
var builder = new ConfigurationBuilder();
// 添加静态值
builder.AddInMemoryCollection(new Dictionary<string, string>
{
{ "MyKey", "MyValue" },
});
// 从 json 文件中添加值
builder.AddJsonFile("appsettings.json");
// 创建 IConfigurationRoot 实例
IConfigurationRoot config = builder.Build();
string value = config["MyKey"]; // 获取值
IConfigurationSection section = config.GetSection("SubSection"); //获取节
在典型的 ASP.NET Core应用程序中,您不会自己创建 ConfigurationBuilder
或调用 Build()
,否则这就是幕后发生的事情。这两种类型之间有明显的区别,并且在大多数情况下,配置系统运行良好,那么为什么我们需要在.NET 6中使用新类型呢?
.NET 5 中的"部分配置构建"问题
此设计的主要问题是何时需要"部分"构建配置。将配置存储在云服务(如 Azure Key Vault)中,甚至存储在数据库中时,这是一个常见问题。
例如,以下是在 ASP.NET Core中的 ConfigureAppConfiguration()
中从 Azure Key Vault读取机密的建议方法:
.ConfigureAppConfiguration((context, config) =>
{
// "标准" 配置等等
config.AddJsonFile("appsettings.json");
config.AddEnvironmentVariables();
if (context.HostingEnvironment.IsProduction())
{
IConfigurationRoot partialConfig = config.Build(); // 构建部分配置
string keyVaultName = partialConfig["KeyVaultName"]; // 从配置中读取值
var secretClient = new SecretClient(
new Uri($"https://{keyVaultName}.vault.azure.net/"),
new DefaultAzureCredential());
config.AddAzureKeyVault(secretClient, new KeyVaultSecretManager()); // 添加额外的配置源
// 框架再次调用配置 config.Build() 构建最终的 IConfigurationRoot
}
})
配置 Azure Key Vault 提供程序需要配置值,因此你会遇到先有鸡还是先有蛋的问题 - 在生成配置之前,无法添加配置源!
解决方案是:
- 添加"初始"配置值
- 通过调用
IConfigurationBuilder.Build()
来构建"部分"配置结果 - 从生成的
IConfigurationRoot
中检索所需的配置值 - 使用这些值添加剩余的配置源
- 框架隐式调用
IConfigurationBuilder.Build()
,生成最终的IConfigurationRoot
并将其用于最终的应用配置。
这整个舞蹈有点乱,但它本身没有错,那么缺点是什么?
缺点是,我们必须调用 Build()
两次:一次是仅使用第一个源生成 IConfigurationRoot
,然后再次使用所有源(包括 Azure Key Vault 源)生成 IConfiguartionRoot
。
在默认的 ConfigurationBuilder
实现中,将循环访问所有源调用 Build()
,加载提供程序,然后将这些提供程序传递到 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 6 中引入了一种新类型来避免这种"重新构建",即 ConfigurationManager
。
.NET 6 中的配置管理器
作为 .NET 6 中"简化"应用程序模型的一部分,.NET 团队添加了一个新的配置类型 ConfigurationManager
。此类型同时实现 IConfigurationBuilder
和 IConfigurationRoot
。通过将这两种实现组合到一个类型中,.NET 6 可以优化上一节中显示的通用模式。
使用 ConfigurationManager
,当添加 IConfigurationSource
(例如,当您调用 AddJsonFile()
时),将立即加载提供程序并更新配置。这可以避免在部分生成方案中多次加载配置源。
由于 IConfigurationBuilder
接口将源公开为 IList<IConfigurationSource>
类型,因此实现这一点比听起来要困难一些:
public interface IConfigurationBuilder
{
IList<IConfigurationSource> Sources { get; }
// ... 其它成员
}
从 ConfigurationManager
的角度来看,这样做的问题在于 IList<>
公开了 Add()
和 Remove()
方法。如果使用简单的 List<>
,则使用者可以在 ConfigurationManager
不知情的情况下添加和删除配置提供程序。
为了解决此问题,ConfigurationManager
使用自定义 IList<>
实现。它包含对 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); // 将源添加到 ConfigurationManager
}
public bool Remove(IConfigurationSource source)
{
var removed = _sources.Remove(source);
_config.ReloadSources(); // 在 ConfigurationManager 中重置源
return removed;
}
// ... 其它实现
}
通过使用自定义 IList<>
实现,ConfigurationManager
可确保在添加新源时调用 AddSource()
。这就是 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()
。这会将数据加载到提供程序中(例如,从环境变量、JSON 文件或 Azure Key Vault),这是"昂贵"的步骤!在"正常"情况下,您只需将源添加到IConfigurationBuilder
,并且可能需要多次构建它,这给出了"最佳"方法;源加载一次,并且只加载一次。
在 ConfigurationManager
中实现 Build()
现在是一个回路 ,只是返回自身。
IConfigurationRoot IConfigurationBuilder.Build() => this;
当然,软件开发都是关于权衡取舍的。如果只添加源,则在添加源时以增量方式构建源效果很好。但是,如果您调用任何IList<>
的其他函数(如 Clear()
、Remove()
或索引器,则 ConfigurationManager
必须调用 ReloadSources()
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();
}
如您所见,如果任何源发生更改,ConfigurationManager
必须删除所有内容并重新开始,遍历每个源,然后重新加载它们。如果您要对配置源进行大量操作,这可能会很快变得昂贵,并且会完全否定 ConfigurationManager
的原始优势。
当然,删除源是非常不寻常的 - 除了添加提供程序之外,通常没有理由做任何事情 - 因此 ConfigurationManager
针对最常见的情况进行了非常优化。谁会猜到呢?😉
下表给出了使用 ConfigurationBuilder
和 ConfigurationManager
的各种操作的相对成本的最终摘要。
Operation | ConfigurationBuilder |
ConfigurationManager |
---|---|---|
Add source | Cheap | Moderately Expensive |
Partially Build IConfigurationRoot |
Expensive | Very cheap (noop) |
Fully Build IConfigurationRoot |
Expensive | Very cheap (noop) |
Remove source | Cheap | Expensive |
Change source | Cheap | Expensive |
那么,我应该关心ConfigurationManager
吗?
因此,在阅读了所有这些内容之后,您应该关心您使用的是 ConfigurationManager
还是 ConfigurationBuilder
?
可能不需要
.NET 6 中引入的新 WebApplicationBuilder
使用 ConfigurationManager
,它针对我上面描述的需要部分构建配置的用例进行了优化。
但是,在早期版本的 ASP.NET Core中引入的 WebHostBuilder
或 HostBuilder
在.NET 6中仍然非常受支持,并且它们继续在幕后使用 ConfigurationBuilder
和 ConfigurationRoot
类型。
我能想到的唯一需要小心的情况是,如果你在某个地方依赖于 IConfigurationBuilder
或 IConfigurationRoot
作为具体类型ConfigurationBuilder
或 ConfigurationRoot
。这对我来说似乎不太可能,如果你依赖这一点,我很想知道为什么!
但除了这个例外,没有"旧"类型不会消失,所以没有必要担心。只要知道,如果你需要做一个"部分构建",并且你正在使用新的 WebApplicationBuilder
,你的应用程序将会提高一点点性能!
总结
在这篇文章中,我描述了.NET 6中引入的新 ConfigurationManager
类型,该类型由最小的API示例中使用的新 WebApplicationBuilder
使用。引入 ConfigurationManager
是为了优化需要"部分构建"配置的常见情况。这通常是因为配置提供程序本身需要一些配置,例如,从 Azure Key Vault 加载机密需要配置以指示要使用的保管库。
ConfigurationManager
通过在添加源时立即加载源来优化此方案,而不是等到调用 Build()
。这样就无需在"部分生成"方案中"重新生成"配置。权衡是其他操作(例如删除源)成本高昂。
Any fool can write code that a computer can understand. Good programmers write code that humans can understand. –Martin Fowler