天天看點

揭開.NET 6 ConfigurationManager 的面紗

作者:DotNET技術圈

在這個系列中,我将探索一下 .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

代表最終“層”的配置值,結合了每個配置源的所有值,以提供所有配置值的最終“平面”視圖。

揭開.NET 6 ConfigurationManager 的面紗
後者配置提供者(環境變量)覆寫了前者配置提供者(

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($"https://{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

的各種操作的相對成本的最終總結:

揭開.NET 6 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

翻譯:精緻碼農

繼續閱讀