.Net Core Configuration源碼探究

前言

    上篇文章咱們演示了爲Configuration添加Etcd數據源,而且瞭解到爲Configuration擴展自定義數據源仍是很是簡單的,核心就是把數據源的數據按照必定的規則讀取到指定的字典裏,這些都得益於微軟設計的合理性和便捷性。本篇文章咱們將一塊兒探究Configuration源碼,去了解Configuration究竟是如何工做的。html

ConfigurationBuilder

    相信使用了.Net Core或者看過.Net Core源碼的同窗都很是清楚,.Net Core使用了大量的Builder模式許多核心操做都是是用來了Builder模式,微軟在.Net Core使用了許多在傳統.Net框架上並未使用的設計模式,這也使得.Net Core使用更方便,代碼更合理。Configuration做爲.Net Core的核心功能固然也不例外。
    其實並無Configuration這個類,這只是咱們對配置模塊的代名詞。其核心是IConfiguration接口,IConfiguration又是由IConfigurationBuilder構建出來的,咱們找到IConfigurationBuilder源碼大體定義以下git

public interface IConfigurationBuilder
{
    IDictionary<string, object> Properties { get; }

    IList<IConfigurationSource> Sources { get; }

    IConfigurationBuilder Add(IConfigurationSource source);

    IConfigurationRoot Build();
}

Add方法咱們上篇文章曾使用過,就是爲ConfigurationBuilder添加ConfigurationSource數據源,添加的數據源被存放在Sources這個屬性裏。當咱們要使用IConfiguration的時候經過Build的方法獲得IConfiguration實例,IConfigurationRoot接口是繼承自IConfiguration接口的,待會咱們會探究這個接口。
咱們找到IConfigurationBuilder的默認實現類ConfigurationBuilder大體代碼實現以下github

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

這個類的定義很是的簡單,相信你們都能看明白。其實整個IConfigurationBuilder的工做流程都很是簡單就是將IConfigurationSource添加到Sources中,而後經過Sources裏的Provider去構建IConfigurationRoot。json

Configuration

經過上面咱們瞭解到經過ConfigurationBuilder構建出來的並不是是直接實現IConfiguration的實現類而是另外一個接口IConfigurationRoot設計模式

ConfigurationRoot

經過源代碼咱們能夠知道IConfigurationRoot是繼承自IConfiguration,具體定義關係以下框架

public interface IConfigurationRoot : IConfiguration
{
    /// <summary>
    /// 強制刷新數據
    /// </summary>
    /// <returns></returns>
    void Reload();

    IEnumerable<IConfigurationProvider> Providers { get; }
}

public interface IConfiguration
{
    string this[string key] { get; set; }

    /// <summary>
    /// 獲取指定名稱子數據節點
    /// </summary>
    /// <returns></returns>
    IConfigurationSection GetSection(string key);

    /// <summary>
    /// 獲取全部子數據節點
    /// </summary>
    /// <returns></returns>
    IEnumerable<IConfigurationSection> GetChildren();
    
    /// <summary>
    /// 獲取IChangeToken用於當數據源有數據變化時,通知外部使用者
    /// </summary>
    /// <returns></returns>
    IChangeToken GetReloadToken();
}

接下來咱們查看IConfigurationRoot實現類ConfigurationRoot的大體實現,代碼有刪減ide

public class ConfigurationRoot : IConfigurationRoot, IDisposable
{
    private readonly IList<IIConfigurationProvider> _providers;
    private readonly IList<IDisposable> _changeTokenRegistrations;
    private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();

    public ConfigurationRoot(IList<IConfigurationProvider> providers)
    {
        _providers = providers;
        _changeTokenRegistrations = new List<IDisposable>(providers.Count);
        //經過便利的方式調用ConfigurationProvider的Load方法,將數據加載到每一個ConfigurationProvider的字典裏
        foreach (var p in providers)
        {
            p.Load();
            //監聽每一個ConfigurationProvider的ReloadToken實現若是數據源發生變化去刷新Token通知外部發生變化
            _changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
        }
    }

    //// <summary>
    /// 讀取或設置配置相關信息
    /// </summary>
    public string this[string key]
    {
        get
        {
            //經過這個咱們能夠了解到讀取的順序取決於註冊Source的順序,採用的是後來者居上的方式
            //後註冊的會先被讀取到,若是讀取到直接return
            for (var i = _providers.Count - 1; i >= 0; i--)
            {
                var provider = _providers[i];
                if (provider.TryGet(key, out var value))
                {
                    return value;
                }
            }
            return null;
        }
        set
        {
            //這裏的設置只是把值放到內存中去,並不會持久化到相關數據源
            foreach (var provider in _providers)
            {
                provider.Set(key, value);
            }
        }
    }

    public IEnumerable<IConfigurationSection> GetChildren() => this.GetChildrenImplementation(null);

    public IChangeToken GetReloadToken() => _changeToken;

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

    //// <summary>
    /// 手動調用該方法也能夠實現強制刷新的效果
    /// </summary>
    public void Reload()
    {
        foreach (var provider in _providers)
        {
            provider.Load();
        }
        RaiseChanged();
    }

    //// <summary>
    /// 強烈推薦不熟悉Interlocked的同窗研究一下Interlocked具體用法
    /// </summary>
    private void RaiseChanged()
    {
        var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
        previousToken.OnReload();
    }
}

上面展現了ConfigurationRoot的核心實現其實主要就是兩點ui

  • 讀取的方式實際上是循環匹配註冊進來的每一個provider裏的數據,是後來者居上的模式,同名key後註冊進來的會先被讀取到,而後直接返回
  • 構造ConfigurationRoot的時候才把數據加載到內存中,並且爲註冊進來的每一個provider設置監聽回調

ConfigurationSection

其實經過上面的代碼咱們會產生一個疑問,獲取子節點數據返回的是另外一個接口類型IConfigurationSection,咱們來看下具體的定義this

public interface IConfigurationSection : IConfiguration
{
    string Key { get; }

    string Path { get; }

    string Value { get; set; }
}

這個接口也是繼承了IConfiguration,這就奇怪了分明只有一套配置IConfiguration,爲何還要區分IConfigurationRoot和IConfigurationSection呢?其實不難理解由於Configuration能夠同時承載許多不一樣的配置源,而IConfigurationRoot正是表示承載全部配置信息的根節點,而配置又是能夠表示層級化的一種結構,在根配置裏獲取下來的子節點是能夠表示承載一套相關配置的另外一套系統,因此單獨使用IConfigurationSection去表示,會顯得結構更清晰,好比咱們有以下的json數據格式spa

{
  "OrderId":"202005202220",
  "Address":"銀河系太陽系火星",
  "Total":666.66,
  "Products":[
    {
      "Id":1,
      "Name":"果子狸",
      "Price":66.6,
      "Detail":{
          "Color":"棕色",
          "Weight":"1000g"
      }
    },
    {
      "Id":2,
      "Name":"蝙蝠",
      "Price":55.5,
      "Detail":{
          "Color":"黑色",
          "Weight":"200g"
      }
    }
  ]
}

咱們知道json是一個結構化的存儲結構,其存儲元素分爲三種一是簡單類型,二是對象類型,三是集合類型。可是字典是KV結構,並不存在結構化關係,在.Net Corez中配置系統是這麼解決的,好比以上信息存儲到字典中的結構就是這種

Key Value
OrderId 202005202220
Address 銀河系太陽系火星
Products:0:Id 1
Products:0:Name 果子狸
Products:0:Detail:Color 棕色
Products:1:Id 2
Products:1:Name 蝙蝠
Products:1:Detail:Weight 200g
若是我想獲取Products節點下的第一條商品數據直接
IConfigurationSection productSection = configuration.GetSection("Products:0")

類比到這裏的話根配置IConfigurationRoot裏存儲了訂單的全部數據,獲取下來的子節點IConfigurationSection表示了訂單裏第一個商品的信息,而這個商品也是一個完整的描述商品信息的數據系統,因此這樣能夠更清晰的區分Configuration的結構,咱們來看一下ConfigurationSection的大體實現

public class ConfigurationSection : IConfigurationSection
{
    private readonly IConfigurationRoot _root;
    private readonly string _path;
    private string _key;

    public ConfigurationSection(IConfigurationRoot root, string path)
    {
        _root = root;
        _path = path;
    }

    public string Path => _path;

    public string Key
    {
        get
        {
            return _key;
        }
    }

    public string Value
    {
        get
        {
            return _root[Path];
        }
        set
        {
            _root[Path] = value;
        }
    }

    public string this[string key]
    {
        get
        {
            //獲取當前Section下的數據其實就是組合了Path和Key
            return _root[ConfigurationPath.Combine(Path, key)];
        }
        set
        {
            _root[ConfigurationPath.Combine(Path, key)] = value;
        }
    }
    
    //獲取當前節點下的某個子節點也是組合當前的Path和子節點的標識Key
    public IConfigurationSection GetSection(string key) => _root.GetSection(ConfigurationPath.Combine(Path, key));
    //獲取當前節點下的全部子節點其實就是在字典裏獲取包含當前Path字符串的全部Key
    public IEnumerable<IConfigurationSection> GetChildren() => _root.GetChildrenImplementation(Path);
    public IChangeToken GetReloadToken() => _root.GetReloadToken();
}

這裏咱們能夠看到既然有Key能夠獲取字典裏對應的Value了,爲什麼還須要Path?經過ConfigurationRoot裏的代碼咱們能夠知道Path的初始值其實就是獲取ConfigurationSection的Key,說白了其實就是如何獲取到當前IConfigurationSection的路徑。好比

//當前productSection的Path是 Products:0
IConfigurationSection productSection = configuration.GetSection("Products:0");
//當前productDetailSection的Path是 Products:0:Detail
IConfigurationSection productDetailSection = productSection.GetSection("Detail");
//獲取到pColor的全路徑就是 Products:0:Detail:Color
string pColor = productDetailSection["Color"];

而獲取Section全部子節點
GetChildrenImplementation來自於IConfigurationRoot的擴展方法

internal static class InternalConfigurationRootExtensions
{
    //// <summary>
    /// 其實就是在數據源字典裏獲取Key包含給定Path的全部值
    /// </summary>
    internal static IEnumerable<IConfigurationSection> GetChildrenImplementation(this IConfigurationRoot root, string path)
    {
        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)));
    }
}

相信講到這裏,你們對ConfigurationSection或者是對Configuration總體的思路有必定的瞭解,細節上的設計確實很多。可是總體實現思路仍是比較清晰的。關於Configuration還有一個比較重要的擴展方法就是將配置綁定到具體POCO的擴展方法,該方法承載在ConfigurationBinder擴展類了,因爲實現比較複雜,也不是本篇文章的重點,有興趣的同窗能夠自行查閱,這裏就不作探究了。

總結

    經過以上部分的講解,其實咱們能夠大概的將Configuration配置相關總結爲兩大核心抽象接口IConfigurationBuilder,IConfiguration,總體結構關係可大體表示成以下關係

    配置相關的總體實現思路就是IConfigurationSource做爲一種特定類型的數據源,它提供了提供當前數據源的提供者ConfigurationProvider,Provider負責將數據源的數據按照必定的規則放入到字典裏。IConfigurationSource添加到IConfigurationBuilder的容器中,後者使用Provide構建出整個程序的根配置容器IConfigurationRoot。經過獲取IConfigurationRoot子節點獲得IConfigurationSection負責維護子節點容器相關。這兩者都繼承自IConfiguration,而後經過他們就能夠獲取到整個配置體系的數據數據操做了。
    以上講解都是本人經過實踐和閱讀源碼得出的結論,可能會存在必定的誤差或理解上的誤區,可是我仍是想把個人理解分享給你們,但願你們能多多包涵。若是有你們有不一樣的看法或者更深的理解,能夠在評論區多多留言。

👇歡迎掃碼關注個人公衆號👇
相關文章
相關標籤/搜索