這是該系列的第一篇文章:探索 .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()。這樣就無需在"部分生成"方案中"重新生成"配置。權衡是其他操作(例如刪除源)成本高昂。
