細聊.NET6 ConfigurationManager的實現

yi念之間發表於2021-12-27

前言

友情提示:建議閱讀本文之前先了解下.Net Core配置體系相關,也可以參考本人之前的文章《.Net Core Configuration原始碼探究 》然後對.Net Core的Configuration體系有一定的瞭解,使得理解起來更清晰。

    在.Net6中關於配置相關多出一個關於配置相關的類ConfigurationManager,如果大概瞭解過Minimal API中的WebApplicationBuilder類相信你肯定發現了,在Minimal API中的配置相關屬性Configuration正是ConfigurationManager的物件。ConfigurationManager本身並沒有引入新的技術,也不是一個體系,只是在原來的基礎上進行了進一步的封裝,使得配置體系有了一個新的外觀操作,暫且可以理解為新瓶裝舊酒。本文我們就來了解下ConfigurationManager類,來看下微軟為何在.Net6中會引入這麼一個新的操作。

使用方式

關於.Net6中ConfigurationManager的使用方式,我們先通過簡單的示例演示一下

ConfigurationManager configurationManager = new();
configurationManager.AddJsonFile("appsettings.json",true,reloadOnChange:true);
string serviceName = configurationManager["ServiceName"];
Console.WriteLine(serviceName);

當然,關於獲取值得其他方式。比如GetSection、GetChildren相關方法還是可以繼續使用的,或者使用Binder擴充套件包相關的Get<string>()GetValue<NacosOptions>("nacos")類似的方法也照樣可以使用。那它和之前的.Net Core上的配置使用起來有什麼不一樣呢,我們看一下之前配置相關的使用方式,如下所示

IConfigurationBuilder configurationBuilder = new ConfigurationBuilder().AddJsonFile("appsettings.json");
IConfiguration configuration = configurationBuilder.Build();
string serviceName = configuration["ServiceName"];
Console.WriteLine(serviceName);

這裡需要注意的是,如果你是使用ConfigurationManager或者是IConfiguration封裝的Helper類相關,並沒有通過框架體系預設注入的時候,一定要注意將其設定為單例模式。其實這個很好理解,先不說每次用的時候都去例項化帶來的記憶體CPU啥的三高問題。讀取配置檔案本質不就是把資料讀到記憶體中嗎?記憶體中有一份快取這就好了,每次都去重新例項去讀本身就是一種不規範的方式。許多時候如果你實在不知道該定義成什麼樣的生命週期,可以參考微軟的實現方式,以ConfigurationManager為例,我們可以參考WebApplicationBuilder類中對ConfigurationManager註冊的生命週期[點選檢視原始碼?]

public ConfigurationManager Configuration { get; } = new();
//這裡註冊為了單例模式
Services.AddSingleton<IConfiguration>(_ => Configuration);

通過上面我們演示的示例可以看出在ConfigurationManager的時候註冊配置和讀取配置相關都只是使用了這一個類。而在之前的配置體系中,註冊配置需要使用IConfigurationBuilder,然後通過Build方法得到IConfiguration例項,然後讀取是通過IConfiguration例項進行的。本身操作配置的時候IConfigurationBuilder和IConfiguration是滿足單一職責原則沒問題,像讀取配置這種基礎操作,應該是越簡單越好,所以微軟才進一步封裝了ConfigurationManager來簡化配置相關的操作。

在.Net6中微軟並沒有放棄IConfigurationBuilder和IConfiguration,因為這是操作配置檔案的基礎類,微軟只是藉助了它們兩個在上面做了進一層封裝而已,這個是需要我們瞭解的。

原始碼探究

上面我們瞭解了新的ConfigurationManager的使用方式,這裡其實我們有疑問了,為什麼ConfigurationManager可以進行註冊和讀取操作。上面我提到過ConfigurationManager本身就是新瓶裝舊酒,而且它只是針對原有的配置體系做了一個新的外觀,接下來哦我們就從原始碼入手,看一下它的實現方式。

定義入手

首先來看一下ConfigurationManager的的定義,如下所示[點選檢視原始碼?]

public sealed class ConfigurationManager : IConfigurationBuilder, IConfigurationRoot, IDisposable
{
}

其實只看它的定義就可以解答我們心中的大部分疑惑了,之所以ConfigurationManager能夠滿足IConfigurationBuilder和IConfigurationRoot這兩個操作的功能是因為它本身就是實現了這兩個介面,集它們的功能於一身了,IConfigurationRoot介面本身就整合自IConfiguration介面。因此如果給ConfigurationManager換個馬甲的話你就會發現還是原來的配方還是原來的味道

ConfigurationManager configurationManager = new();
IConfigurationBuilder configurationBuilder = configurationManager.AddJsonFile("appsettings.json", true, reloadOnChange: true);
//儘管放心的呼叫Build完全不影響啥
IConfiguration configuration = configurationBuilder.Build();
string serviceName = configuration["ServiceName"];
Console.WriteLine(serviceName);

這種寫法只是為了更好的看清它的本質,如果真實操作這麼寫,確實有點畫蛇添足了,因為ConfigurationManager本身就是為了簡化我們的操作。

認識IConfigurationBuilder和IConfiguration

通過上面我們瞭解到ConfigurationManager可以直接註冊過配置檔案就可以直接去操作配置檔案裡的內容,這一步是肯定通過轉換得到的,畢竟之前的方式我們是通過IConfigurationBuilder的Build操作得到的IConfiguration的例項,那麼我們就先來看下原始的方式是如何實現的。這裡需要從IConfigurationBuilder的預設實現類ConfigurationBuilder說起,它的實現很簡單[點選檢視原始碼?]

public class ConfigurationBuilder : IConfigurationBuilder
{
    /// <summary>
    /// 新增的資料來源被存放到了這裡
    /// </summary>
    public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();

    public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>();

    /// <summary>
    /// 新增IConfigurationSource資料來源
    /// </summary>
    /// <returns></returns>
    public IConfigurationBuilder Add(IConfigurationSource source)
    {
        if (source == null)
        {
            throw new ArgumentNullException(nameof(source));
        }
        Sources.Add(source);
        return this;
    }

    public IConfigurationRoot Build()
    {
        //獲取所有新增的IConfigurationSource裡的IConfigurationProvider
        var providers = new List<IConfigurationProvider>();
        foreach (var source in Sources)
        {
            var provider = source.Build(this);
            providers.Add(provider);
        }
        //用providers去例項化ConfigurationRoot
        return new ConfigurationRoot(providers);
    }
}

這裡我們來解釋一下,其實我們註冊配置相關的時候比如AddJsonFile()、AddEnvironmentVariables()、AddInMemoryCollection()等等它們其實都是擴充套件方法,本質就是新增IConfigurationSource例項,而IConfigurationBuilder的Build本質操作其實就是在IConfigurationSource集合中得到IConfigurationProvider集合,因真正從配置讀取到的資料都是包含在IConfigurationProvider例項中的,ConfigurationRoot通過一系列的封裝,讓我們可以更便捷的得到配置裡相關的資訊。這就是ConfigurationBuilder的工作方式,也是配置體系的核心原理。
我們既然知道了新增配置的本質其實就是IConfigurationBuilder.Add(IConfigurationSource source)那麼我就來看一下ConfigurationManager是如何實現這一步的。我們知道ConfigurationManager實現了IConfigurationBuilder介面,所以必然重寫了IConfigurationBuilder的Add方法,找到原始碼位置[點選檢視原始碼?]

private readonly ConfigurationSources _sources = new ConfigurationSources(this); ;
IConfigurationBuilder IConfigurationBuilder.Add(IConfigurationSource source)
{
    _sources.Add(source ?? throw new ArgumentNullException(nameof(source)));
    return this;
}

這裡返回了this也就是當前ConfigurationManager例項是為了可以進行鏈式程式設計,ConfigurationSources這個類是個新物種,原來的類叫ConfigurationSource,這裡多了個s表明了這是一個集合類,我們就來看看它是個啥操作,找到原始碼位置[點選檢視原始碼?]

/// <summary>
/// 本身是一個IConfigurationSource集合
/// </summary>
private class ConfigurationSources : IList<IConfigurationSource>
{
    private readonly List<IConfigurationSource> _sources = new();
    private readonly ConfigurationManager _config;

    /// <summary>
    /// 因為是ConfigurationManager的內部類所以傳遞了當前ConfigurationManager例項
    /// </summary>
    /// <param name="config"></param>
    public ConfigurationSources(ConfigurationManager config)
    {
        _config = config;
    }

    /// <summary>
    /// 根據索引獲取其中一個IConfigurationSource例項
    /// </summary>
    /// <returns></returns>
    public IConfigurationSource this[int index]
    {
        get => _sources[index];
        set
        {
            _sources[index] = value;
            _config.ReloadSources();
        }
    }

    public int Count => _sources.Count;

    public bool IsReadOnly => false;

    /// <summary>
    /// 這是重點新增配置源
    /// </summary>
    /// <param name="source"></param>
    public void Add(IConfigurationSource source)
    {
        //給自己的IConfigurationSource集合新增
        _sources.Add(source);
        //呼叫了ConfigurationManager的AddSource方法
        _config.AddSource(source);
    }

    /// <summary>
    /// 實現IList清除操作
    /// </summary>
    public void Clear()
    {
        _sources.Clear();
        //這裡可以看到ConfigurationManager的ReloadSources方法很重要
        //通過名字可以看出是重新整理配置資料用的
        _config.ReloadSources();
    }

    public void Insert(int index, IConfigurationSource source)
    {
        _sources.Insert(index, source);
        _config.ReloadSources();
    }

    public bool Remove(IConfigurationSource source)
    {
        var removed = _sources.Remove(source);
        _config.ReloadSources();
        return removed;
    }

    public void RemoveAt(int index)
    {
        _sources.RemoveAt(index);
        _config.ReloadSources();
    }

    //這裡省略了實現了實現IList介面的其他操作
    //ConfigurationSources本身就是IList<IConfigurationSource>
}

正如我們看到的那樣ConfigurationSources本身就是一個IConfigurationSource的集合,在新的.Net體系中微軟喜歡把集合相關的操作封裝一個Collection類,這樣的好處就是讓大家能更清晰的瞭解它是功能實現類,而不在用一個資料結構的眼光去看待。通過原始碼我們還看到了Add方法裡還呼叫了ConfigurationManager的AddSource方法,這究竟是一個什麼操作我們來看下[點選檢視原始碼?]

private readonly object _providerLock = new();
private readonly List<IConfigurationProvider> _providers = new();
private readonly List<IDisposable> _changeTokenRegistrations = new();
private void AddSource(IConfigurationSource source)
{
    lock (_providerLock)
    {
        //在IConfigurationSource中得到IConfigurationProvider例項
        var provider = source.Build(this);
        //新增到_providers集合中
        //我們提到過從配置源得到的配置都是通過IConfigurationProvider得到的
        _providers.Add(provider);

        //IConfigurationProvider的Load方法是從配置源中得到配置資料載入到程式記憶體中
        provider.Load();
        //註冊更改令牌操作,使得配置可以進行動態重新整理載入
        _changeTokenRegistrations.Add(ChangeToken.OnChange(() => provider.GetReloadToken(), () => RaiseChanged()));
    }
    //新增新的配置源要重新整理令牌操作
    RaiseChanged();
}

private ConfigurationReloadToken _changeToken = new();
private void RaiseChanged()
{
    //每次對配置源進行更改操作需要得到新的更改令牌例項,用於可重複通知配置變更相關
    var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
    previousToken.OnReload();
}

從上面的ConfigurationSources方法裡我們可以看到動態的針對ConfigurationSources裡的ConfigurationSource進行更改會每次都呼叫ReloadSources方法,我們來看一下它的實現[點選檢視原始碼?]

private readonly object _providerLock = new();
private void ReloadSources()
{
    lock (_providerLock)
    {
        //釋放原有操作
        DisposeRegistrationsAndProvidersUnsynchronized();

        //清除更改令牌
        _changeTokenRegistrations.Clear();
        //清除_providers
        _providers.Clear();

        //重新載入_providers
        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實現了IConfigurationBuilder介面所以也必然實現了它的Build方法少不了,看一下它的實現[點選檢視原始碼?]

IConfigurationRoot IConfigurationBuilder.Build() => this;

這波操作真的很真的很騷氣,我即是IConfigurationRoot我也是IConfigurationBuilder,反正操作都是我自己,所以這裡你可勁的Build也不影響啥,反正得到的也都是一個ConfigurationManager例項。到了這裡結合我們之前瞭解到的傳統的IConfigurationBuilder和IConfiguration關係,以及我們上面展示的展示的ConfigurationSources類的實現和ConfigurationManager的AddSource方法。其實我們可以發現我們上面展示的ConfigurationManager類的相關操作其實就是實現了之前ConfigurationBuilder類裡的操作。其實這裡微軟可以不用實現ConfigurationSources類完全基於ConfigurationBuilder也能實現一套,但是顯然微軟沒這麼做,具體想法我們們不得而知,估計是隻想以來抽象,而並不像以來原來的實現方式吧。

我們上面展示的這一部分的ConfigurationManager程式碼,其實就是替代了原來的ConfigurationBuilder類的功能。

讀取操作

上面我們看到了在ConfigurationManager中關於以前ConfigurationManager類的實現。接下來我們看一下讀取相關的操作,即在這裡ConfigurationManager成為了IConfiguration例項,所以我們先來看下IConfiguration介面的定義[點選檢視原始碼?]

public interface IConfiguration
{
    /// <summary>
    /// 通過配置名稱獲取值
    /// </summary>
    /// <returns></returns>
    string this[string key] { get; set; }

    /// <summary>
    /// 獲取一個配置節點
    /// </summary>
    /// <returns></returns>
    IConfigurationSection GetSection(string key);

    /// <summary>
    /// 獲取所有子節點
    /// </summary>
    /// <returns></returns>
    IEnumerable<IConfigurationSection> GetChildren();

    /// <summary>
    /// 重新整理資料通知
    /// </summary>
    /// <returns></returns>
    IChangeToken GetReloadToken();
}

通過程式碼我們看到了IConfiguration的定義,也就是在ConfigurationManager類中必然也實現也這幾個操作,首先便是通過索引器直接根據配置的名稱獲取值得操作[點選檢視原始碼?]

private readonly object _providerLock = new();
private readonly List<IConfigurationProvider> _providers = new();
/// <summary>
/// 可讀可寫的操作
/// </summary>
/// <returns></returns>
public string this[string key]
{
    get
    {
        lock (_providerLock)
        {
            //通過在IConfigurationProvider集合中獲取配置值
            return ConfigurationRoot.GetConfiguration(_providers, key);
        }
    }
    set
    {
        lock (_providerLock)
        {
            //也可以把值放到IConfigurationProvider集合中
            ConfigurationRoot.SetConfiguration(_providers, key, value);
        }
    }
}

其中_providers中的值是我們在AddSource方法中新增進來的,這裡的本質其實還是針對ConfigurationRoot做了封裝。ConfigurationRoot實現了IConfigurationRoot介面,IConfigurationRoot實現了IConfiguration介面。而ConfigurationRoot的GetConfiguration方法和SetConfiguration是最直觀體現ConfigurationRoot本質就是IConfigurationProvider包裝的證據。我們來看一下ConfigurationRoot這兩個方法的實現[點選檢視原始碼?]

internal static string GetConfiguration(IList<IConfigurationProvider> providers, string key)
{
    //倒序遍歷providers,因為Configuration採用的後來者居上的方式,即後註冊的Key會覆蓋先前註冊的Key
    for (int i = providers.Count - 1; i >= 0; i--)
    {
        IConfigurationProvider provider = providers[i];
        //如果找到Key的值就直接返回
        if (provider.TryGet(key, out string value))
        {
            return value;
        }
    }
    return null;
}

internal static void SetConfiguration(IList<IConfigurationProvider> providers, string key, string value)
{
    if (providers.Count == 0)
    {
        throw new InvalidOperationException("");
    }
    //給每個provider都Set這個鍵值,雖然浪費了一部分記憶體,但是可以最快的獲取
    foreach (IConfigurationProvider provider in providers)
    {
        provider.Set(key, value);
    }
}

關於GetSection的方法實現,本質上是返回ConfigurationSection例項,ConfigurationSection本身也是實現了IConfiguration介面,所有關於配置獲取的操作出口都是面向IConfiguration的。

public IConfigurationSection GetSection(string key) => new ConfigurationSection(this, key);

GetChildren方法是獲取配置的所有子節點的操作,本質是返回IConfigurationSection的集合,實現方式如如下

private readonly object _providerLock = new();
public IEnumerable<IConfigurationSection> GetChildren()
{
    lock (_providerLock)
    {
        //呼叫了GetChildrenImplementation方法
        return this.GetChildrenImplementation(null).ToList();
    }
}

這裡呼叫了GetChildrenImplementation方法,而GetChildrenImplementation是一個擴充套件方法,我們來看一下它的實現[點選檢視原始碼?]

internal static IEnumerable<IConfigurationSection> GetChildrenImplementation(this IConfigurationRoot root, string path)
{
    //在當前ConfigurationManager例項中獲取到所有的IConfigurationProvider例項
    //然後包裝成IConfigurationSection集合
    return root.Providers
        .Aggregate(Enumerable.Empty<string>(),
            (seed, source) => source.GetChildKeys(seed, path))
        .Distinct(StringComparer.OrdinalIgnoreCase)
        .Select(key => root.GetSection(path == null ? key : ConfigurationPath.Combine(path, key)));
}

通過這段程式碼再次應驗了那句話所有獲取配置資料都是面向IConfiguration介面的,資料本質都是來自於IConfigurationProvider讀取配置源中的資料。

ConfigurationBuilderProperties

在ConfigurationManager中還包含了一個Properties屬性,這個屬性本質來源於IConfigurationBuilder。在IConfigurationBuilder中它和IConfigurationSource是平行關係,IConfigurationSource用於在配置源中獲取資料,而Properties是在記憶體中獲取資料,本質是一個字典

private readonly ConfigurationBuilderProperties _properties = new ConfigurationBuilderProperties(this);
IDictionary<string, object> IConfigurationBuilder.Properties => _properties;

這裡我們們就不細說這個具體實現了,我們知道它本質是字典,然後操作都是純記憶體的操作即可,來看一下它的定義[點選檢視原始碼?]

private class ConfigurationBuilderProperties : IDictionary<string, object>
{
}

基本上許多快取機制即記憶體操作都是基於字典做的一部分實現,所以大家對這個實現的方式有一定的認識即可,即使在配置體系的核心操作ConfigurationProvider中讀取的配置資料也是存放在字典中的。這個可以去ConfigurationProvider類中自行了解一下[點選檢視原始碼?]

protected IDictionary<string, string> Data { get; set; }
protected ConfigurationProvider()
{
    Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}

總結

    通過本文我們瞭解到了.Net6配置體系中的新成員ConfigurationManager,它是一個新內容但不是一個新技術,因為它是在原有的配置體系中封裝了一個新的外觀,以簡化原來對配置相關的操作。原來對配置的操作需要涉及IConfigurationBuilder和IConfiguration兩個抽象操作,而新的ConfigurationManager只需要一個類,其本質是因為ConfigurationManage同時實現了IConfigurationBuilder和IConfiguration介面,擁有了他們兩個體系的能力。整體來說重寫了IConfigurationBuilder的實現為主,而讀取操作主要還是藉助原來的ConfigurationRoot對節點資料的讀取操作。

?歡迎掃碼關注我的公眾號? 細聊.NET6 ConfigurationManager的實現

相關文章