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