[探索 .NET 6]01 揭開 ConfigurationManager 的面紗
在這個系列中,我將探索一下 .NET 6 中的一些新特性。已經有很多關於 .NET 6 的內容,包括很多來自 .NET 和 ASP.NET 團隊本身的文章。在這個系列中,我將探索一下這些特性背後的一些程式碼。
在這第一篇文章中,來研究一下 ConfigurationManager
類,講一下為什麼要新增這個類,並看一下它的的一些實現程式碼。
1 什麼是 ConfigurationManager
如果你的第一反應是“什麼是 ConfigurationManager”,那麼不用擔心,你沒有錯過一個重要的公告:
加入 ConfigurationManager
是為了支援 ASP.NET Core 的新 WebApplication
模型,用於簡化 ASP.NET Core 的啟動程式碼。然而 ConfigurationManager
在很大程度上是一個實現細節。它的引入是為了優化一個特定的場景(我很快會講),但在大多數情況下,你不需要(也不會)知道你在使用它。
在我們討論 ConfigurationManager
本身之前,我們先來看看它所取代的東西和原因。
2 .NET 5 中的配置
.NET 5 圍繞配置暴露了多種型別,但在你的應用程式中直接使用的兩個主要型別是:
-
IConfigurationBuilder
- 用來新增配置源。在構建器上呼叫Build()
讀取每個配置源,並構建最終的配置。 -
IConfigurationRoot - 代表最終“構建”好的配置。
IConfigurationBuilder
介面主要是一個圍繞配置源列表的封裝器。配置提供者通常包括擴充套件方法(如 AddJsonFile()
和 AddAzureKeyVault()
),將配置源新增到 Sources
列表中。
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();
// add static values
builder.AddInMemoryCollection(new Dictionary<string, string>
{
{ "MyKey", "MyValue" },
});
// add values from a json file
builder.AddJsonFile("appsettings.json");
// create the IConfigurationRoot instance
IConfigurationRoot config = builder.Build();
string value = config["MyKey"]; // get a value
IConfigurationSection section = config.GetSection("SubSection"); //get a section
在一個典型的 ASP.NET Core 應用程式中,你不會自己建立 ConfigurationBuilder
,或呼叫 Build()
,但除此之外,這就是幕後發生的事情。這兩種型別之間有明確的分離,而且在大多數情況下,配置系統執行良好,那麼為什麼我們在.NET 6 中需要一個新型別呢?
3 .NET 5 中“部分構建”配置的問題
這種設計的主要問題是在你需要“部分”構建配置的時候。當你將配置儲存在 Azure Key Vault 等服務中,甚至是資料庫中時,這是一個常見的問題。
例如,以下是在 ASP.NET Core 中的 ConfigureAppConfiguration()
裡面從 Azure Key Vault 讀取 secrects 的建議方式:
.ConfigureAppConfiguration((context, config) =>
{
// "normal" configuration etc
config.AddJsonFile("appsettings.json");
config.AddEnvironmentVariables();
if (context.HostingEnvironment.IsProduction())
{
IConfigurationRoot partialConfig = config.Build(); // build partial config
string keyVaultName = partialConfig["KeyVaultName"]; // read value from configuration
var secretClient = new SecretClient(
new Uri($"http://{keyVaultName}.vault.azure.net/"),
new DefaultAzureCredential());
config.AddAzureKeyVault(secretClient, new KeyVaultSecretManager()); // add an extra configuration source
// The framework calls config.Build() AGAIN to build the final IConfigurationRoot
}
})
配置 Azure Key Vault 提供者需要一個配置值,所以你陷入了一個雞和蛋的問題--在你建立配置之前,你無法新增配置源。
解決辦法是:
-
新增“初始”配置值;
-
通過呼叫
IConfigurationBuilder.Build()
構建“部分”配置結果; -
從生成的
IConfigurationRoot
中檢索所需的配置值; -
使用這些值來新增剩餘的配置源;
-
框架隱含地呼叫
IConfigurationBuilder.Build()
,生成最終的IConfigurationRoot
並將其用於最終的應用配置。
這整個過程有點亂,但它本身並沒有什麼問題,那麼缺點是什麼呢?
缺點是我們必須呼叫 Build()
兩次:一次是隻使用第一個源來構建 IConfigurationRoot
,另一次是使用所有源來構建 IConfiguartionRoot
,包括 Azure Key Vault 源。
在預設的 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()));
}
}
// ... remainder of implementation
}
如果你在應用啟動時呼叫 Build()
兩次,那麼所有這些都會發生兩次。
一般來說,從配置源獲取資料一次以上並無大礙,但這是不必要的工作,而且經常涉及到(相對緩慢的)檔案讀取等。
這是一種常見的模式,所以在 .NET 6 中引入了一個新的型別來避免這種“重新構建”,即 ConfigurationManager
。
4 .NET 6 中的配置管理器
作為 .NET 6 中“簡化”應用模型的一部分,.NET 團隊增加了一個新的配置型別-- ConfigurationManager
。這種型別同時實現了 IConfigurationBuilder
和 IConfigurationRoot
。通過將這兩種實現結合在一個單一的型別中,.NET 6 可以優化上一節中展示的常見模式。
有了 ConfigurationManager
,當 IConfigurationSource
被新增時(例如當你呼叫 AddJsonFile()
時),提供者被立即載入,配置被更新。這可以避免在部分構建的情況下不得不多次載入配置源。
由於 IConfigurationBuilder
介面將源作為 IList<IConfigurationSource>
公開,因此實現這一點比聽起來要難一些:
public interface IConfigurationBuilder
{
IList<IConfigurationSource> Sources { get; }
// .. other members
}
從 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); // add the source to the ConfigurationManager
}
public bool Remove(IConfigurationSource source)
{
var removed = _sources.Remove(source);
_config.ReloadSources(); // reset sources in the ConfigurationManager
return removed;
}
// ... additional implementation
}
通過使用一個自定義的 IList<>
實現, ConfigurationManager
確保每當有新的源被新增時就呼叫 AddSource()
。這就是 ConfigurationManager
的優勢所在:呼叫 AddSource()
可以立即載入源:
ublic 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
的各種操作的相對成本的最終總結:
5 是否需關心 ConfigurationManager
那麼讀了這麼多,你是否應該關心你是使用 ConfigurationManager
還是 ConfigurationBuilder
?
也許不應該。
在 .NET 6 中引入的新的 WebApplicationBuilder
使用 ConfigurationManager
,它優化了我上面描述的使用情況,即你需要“部分構建”你的配置。
然而,ASP.NET Core 早期版本中引入的 WebHostBuilder
或 HostBuilder
在 .NET 6 中仍然非常受支援,它們繼續在幕後使用 ConfigurationBuilder
和 ConfigurationRoot
型別。
我認為唯一需要注意的情況是,如果你在某個地方依賴 IConfigurationBuilder
或 IConfigurationRoot
作為具體型別的 ConfigurationBuilder
或 ConfigurationRoot
。這在我看來是非常不太可能發生的,如果你依賴這一點,我很想知道原因。
但除了這個小眾的例外,“老”型別不會消失,所以沒有必要擔心。如果你需要進行“部分構建”,並且你使用了新的 WebApplicationBuilder
,那麼你的應用程式將會有更高的效能,這一點你應該感到高興。
6 總結
在這篇文章中,我描述了在 .NET 6 中引入的新的 ConfigurationManager
型別,並在最小(Minimal) API 示例中被新的 WebApplicationBuilder
所使用。引入 ConfigurationManager
是為了優化一種常見的情況,即你需要“部分構建”配置。這通常是因為配置提供者本身需要一些配置,例如,從 Azure Key Vault 載入 secrects,需要配置表明要使用哪個 Vault 庫。
ConfigurationManager
優化了這種情況:它在新增源時立即載入,而不是等到你呼叫 Build()
。這就避免了在“部分構建”情況下“重建”配置的需要,其代價是其他不常見操作(如刪除一個源)可能變得更昂貴的。
原文:bit.ly/3227vka
作者:Andrew Lock
翻譯:精緻碼農
- C# 面向物件程式設計之里氏替換原則實戰解析
- C# 面向物件程式設計之開閉原則實戰解析
- C# 面向物件程式設計之單一職責原則實戰解析
- C#單元測試的使用(三)
- .NET基礎知識快速通關(9)
- .NET基礎知識快速通關(6)
- .NET基礎知識快速通關(5)
- 面試寶典之.NET基礎知識快速通關(1)
- 填坑 | .NET 在Docker中訪問MSSQL報錯
- 如何分析.NET Core HttpClient請求異常
- 理解C#泛型原理
- 萬字長文講解:什麼是「抽象」?
- 簡述使用REST API 的最佳實踐
- 如何基於.NET Core構建分散式檔案儲存系統?
- 利用SOS擴充套件庫進入高階.NET6程式的除錯
- 淺議開發者面臨的資訊偏差影響因素
- 【新書速遞】龍芯開源LoongArch版,學會造計算機!
- .NET誕生20週年 .NET 7有什麼新東西?
- 基於.NET 製作一個氣象站 IoT 應用
- [探索 .NET 6]02 比較 WebApplicationBuilder 和 Host