探索 .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 及更早版本中,IConfigurationBuilderIConfigurationRoot 接口分別由 ConfigurationBuilderConfigurationRoot 實現。如果您直接使用這些類型,則可以執行以下操作:

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。此類型同時實現 IConfigurationBuilderIConfigurationRoot。通過將這兩種實現組合到一個類型中,.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();
    }
}

此方法立即調用 IConfigurationSourceBuild 方法以創建 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 針對最常見的情況進行了非常優化。誰會猜到呢?😉

下表給出了使用 ConfigurationBuilderConfigurationManager 的各種操作的相對成本的最終摘要。

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中引入的 WebHostBuilderHostBuilder 在.NET 6中仍然非常受支持,並且它們繼續在幕后使用 ConfigurationBuilderConfigurationRoot 類型。

我能想到的唯一需要小心的情況是,如果你在某個地方依賴於 IConfigurationBuilderIConfigurationRoot 作為具體類型ConfigurationBuilderConfigurationRoot。這對我來說似乎不太可能,如果你依賴這一點,我很想知道為什么!

但除了這個例外,沒有"舊"類型不會消失,所以沒有必要擔心。只要知道,如果你需要做一個"部分構建",並且你正在使用新的 WebApplicationBuilder,你的應用程序將會提高一點點性能!

總結

在這篇文章中,我描述了.NET 6中引入的新 ConfigurationManager 類型,該類型由最小的API示例中使用的新 WebApplicationBuilder 使用。引入 ConfigurationManager 是為了優化需要"部分構建"配置的常見情況。這通常是因為配置提供程序本身需要一些配置,例如,從 Azure Key Vault 加載機密需要配置以指示要使用的保管庫。

ConfigurationManager 通過在添加源時立即加載源來優化此方案,而不是等到調用 Build()。這樣就無需在"部分生成"方案中"重新生成"配置。權衡是其他操作(例如刪除源)成本高昂。




Looking inside ConfigurationManager in .NET 6


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM