Dotnet Core 技術之Dotnet 6.0 深度探索

語言: CN / TW / HK

Dotnet 6.0 大家都裝了沒?

我打算開個專題,系統地寫一寫 Dotnet 6.0 在各個方面的特性,以及全新的開發方式。也是因為最近討論 6.0 比較多,看到很多人的畏難情緒,所以打算寫寫相關的內容。

瞭解了,就不怕了。

要寫的內容很多,我會分幾篇來寫。

今天是第一篇:ConfigurationManager,配置管理器。

ConfigurationManager 是幹什麼用的?

引用微軟官方的說法:ConfigurationManager 是用來支援 ASP.Net Core 的新的 WebApplication 模型。這個模型主要的作用是在一些特定的場景下(後面我們會說到),用來簡化 ASP.NET Core 的啟動程式碼。

當然,如果我們去看 MSDN 的文件,會發現 ConfigurationManager 本身實現還是挺複雜的。好在,大多數情況下,這是一個半隱藏的東西,你可能都意識不到你已經用到了它。

那它到底是幹什麼用的?

這得從 .Net 5.0 的 Configuration 說起。

.Net 5.0 裡的 Configuration

Configuration 配置,從 3.1 到 5.0,增加了很多很多的配置型別,如果你去 MSDN 上看,有好幾大篇。

這裡面,我們接觸最多的是兩個:

  • IConfigurationBuilder - 這個介面主要用來增加配置源,並在構建器上呼叫 Build() 來讀取每個配置源,並形成最終的配置
  • IConfigurationRoot - 這就是上面 Build() 完成後形成的配置,我們會從這裡面讀配置值

在實際應用中,IConfigurationBuilder 通常被我們用做配置源列表的包裝器,最常用的是通過 AddJsonFile(),將配置源新增到源列表中。看到 AddJsonFile(),你是不是想到了什麼?

簡單來說,IConfigurationBuilder 是這樣的:

public interface IConfigurationBuilder 
{ 
    IDictionary<string, object> Properties { get; } 
    IList<IConfigurationSource> Sources { get; } 
    IConfigurationBuilder Add(IConfigurationSource source); 
    IConfigurationRoot Build(); 
} 

而 IConfigurationRoot,裡面放的是經過合併的配置值。這個合併需要注意一下,多個配置源逐個加入時,相同名稱的項,後面的配置會覆蓋前面的項。

在 .Net 5.0 以前,IConfigurationBuilder 和 IConfigurationRoot 介面分別由 ConfigurationBuilder 和 ConfigurationRoot 實現。使用時通常是這麼寫:

var builder = new ConfigurationBuilder(); 
 
// 加入靜態值 
builder.AddInMemoryCollection(new Dictionary<string, string> 
{ 
    { "MyKey", "MyValue" }, 
}); 
 
// 加入檔案 
builder.AddJsonFile("appsettings.json"); 
 
IConfigurationRoot config = builder.Build(); 
 
string value = config["MyKey"]; // 取一個值 
IConfigurationSection section = config.GetSection("SubSection"); // 取一個節 

這是在 Console 程式中。

在 ASP.NET Core 中,通常不需要這麼顯式的 new 和 Build(),但事實上也是呼叫的這個介面。

在預設的 ConfigurationBuilder 實現中,呼叫 Build() 將遍歷所有的源,載入 Provider 程式,並將它們傳遞給一個新的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 5.0 之前,都是這麼做。

可喜的是,在 .Net 6.0 裡,微軟也注意到這個問題,並引入了一個新的型別:ConfigurationManager。

.Net 6.0 裡的 ConfigurationManager

ConfigurationManager 是一個 .Net 6.0 中新的配置型別。這個型別也同樣實現了兩個介面:IConfigurationBuilder 和 IConfigurationRoot。那麼,通過這兩個介面的實現,我們可以簡化上一節講到的 .Net 5.0 中的通用模式。

不過,還是有一點點區別。這裡 IConfigurationBuilder 將源儲存為 IList:

public interface IConfigurationBuilder 
{ 
    IList<IConfigurationSource> Sources { get; } 
    // ... 
} 

這樣做有一個好處,就是對於源 IList,就有了 Add() 和 Remove() 方法,我們可以在不知道 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); // 增加源 
    } 
 
    public bool Remove(IConfigurationSource source) 
    { 
        var removed = _sources.Remove(source); // 刪除源 
        _config.ReloadSources(); // 重新載入源 
        return removed; 
    } 
 
    // ...  
} 

這樣做可以確保 ConfigurationManager 在改變源的 IList 時,能自動載入源的配置資料。

看一下 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() 方法,將資料載入到 Provider。

這個方法解決了一件事,就是當我們需要從不同的位置向 IConfigurationBuilder 加入各種源時,源只需要載入一次,而且只會載入一次。

上面的程式碼是增加源。當我們需要 Remove() 源,或者乾脆清除掉全部的源 Clear() 時,就需要呼叫 ReloadSource():

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(); 
} 

當然,看懂上面的程式碼,也就明白兩件事:

  • 增加源是程式碼最小的,加到列表中就行了 ;
  • 刪除或更改原始碼會有點大,需要重新遍歷載入所有源。

如果需要對配置的源進行大量的操作,這樣的代價會比較大。不過,這種情況會很不常見。

總結一下

.Net 6.0 引入了一個新的 ConfigurationManager,用來優化配置的構建。

ConfigurationManager 同樣實現了 ConfigurationBuilder 和 ConfigurationRoot。這算是個相容性的設定,主要是為了支援 WebHostBuilder 和 HostBuilder 中對配置的呼叫。同時,也相容了早期程式碼中的呼叫方式。所以,程式碼升級時,相關配置呼叫的部分,如果不想改程式碼,是完全可以的。而如果想做點改動,就換成使用 ConfigurationManager,或者通過 WebApplicationBuilder 來載入(會自動呼叫 ConfigurationManager),應用程式會有更好的效能。

這算是一個小禮物,相信也是微軟權衡以後的結果。