Net6 Configuration & Options 原始碼分析 Part1

一身大膘發表於2022-03-17
Net6 Configuration & Options 原始碼分析 Part1

在Net6中配置系統一共由兩個部分組成Options 模型配置系統.它們是兩個完全獨立的系統。
第一部分主要記錄配置系統

下面演示的幾個例項具有一個共同的特徵( 1. 將配置繫結為Options物件),即都採用配置系統來提供繫結Options物件的原始資料,實際上,Options 框架具有一個完全獨立的模型,可以稱為Options 模型。這個獨立的Options 模型本身並不依賴於配置系統,讓配置系統來提供配置資料僅僅是通過 Options 模型的一個擴充套件點實現的。在很多情況下,可能並不需要將應用的配置選項定義在配置檔案中,在應用啟動時直接初始化可能是一種更方便、快捷的方式
以上引用 ASP.NET Core 3 框架揭祕

使用

IConfiguration IConfigurationBuilder IConfigurationSource

讀取的配置資訊最終會轉換成一個IConfiguration物件供應用程式使用。IConfigurationBuilder 物件是IConfiguration物件的構建者,而構建IConfiguration是要的資料來源用IConfigurationSource物件表示,它代表配置資料最原始的來源.以鍵值對的形式讀取配置。以上是在使用層面,其實在IConfigurationSource還有個IConfigurationProvider。

MemoryConfiguration 使用

以下示例建立一個ConfigurationBuilder(IConfigurationBuilder介面的預設實現型別)物件,併為之註冊一個或者多個 IConfigurationSource 物件,最後利用它來建立我們需要的IConfiguration物件作為對外的資料的操作介面。

var source = new Dictionary<string, string>{
    ["key"] ="hello", 
};
var configuration = new ConfigurationBuilder().Add (new MemoryConfigurationSource(InitialData = source )).Build();
public class TestOptions {
    string name;
    public DateTime TestOptions (IConfiguration configuration){
       name = configuration["key"]; 
    }
}
讀取結構化的配置/樹形層次結構

IConfigurationRoot與IConfigurationSection組成了一個邏輯上樹形結構資料。兩者均實現了IConfiguration。前者作為根節點。後者作為普通節點。

var source = new Dictionary<string, string>{
    {"TestOptions:Key1" ,"TestOptions key1"},
    {"TestOptions:Key2" ,"TestOptions key2"},
    {"UserInfo:key1" ,"UserInfo"},
};
var rootConfiguration = new ConfigurationBuilder().Add(new MemoryConfigurationSource() { InitialData = source }).Build();
configurationSection = configuration.GetSection("TestOptions");
configurationSection = configuration.GetSection("UserInfo");
繫結到POCO物件 ConfigurationBinder 也可以叫做配置繫結

包:Microsoft.Extensions.Configuration.Binder
ConfigurationBinder是一個幫助類是對IConfiguration的擴充套件類,內部就是通過反射tpye 然後在利用IConfiguration繫結節點到type並返回例項。
值得注意的是,如果你的節點沒有對應的type屬性會報錯比如你的配置源中有個叫Name的節點,但對應的POCO物件並沒有這個屬性就會拋異常。但這是通過BinderOptions設定的。

var configuration = new ConfigurationBuilder().Add(new MemoryConfigurationSource() { InitialData = source }).Build();
var testOption = configuration.GetSection("TestOptions").Get<TestOpetion>();
Console.WriteLine(testOption.Key1);

JsonConfigurationSource

一般不需要手動建立這個 JsonConfigurationSource物件,只需要呼叫 IConfiguration Builder介面的AddJsonFile擴充套件方法新增指定的JSON檔案即可

var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").AddJsonFile($"appsettings.{environment}.json.Build();//兩個appsettings內容會合並。
var test = configuration.GetSection("TestOptions").Get<>(TestOptions);

其它資料來源

  1. CommandLineConfigurationSource
  2. EnvironmentVariablesConfigurationSource
    環境變數儲存位置:系統/使用者/當前程式的環境變數(系統和使用者級別的環境變數儲存在登錄檔)
    Environment靜態類用於操作環境變數。GetEnvironmentVariables返回當前所有環境變數
  3. FileConfigurationSource

繫結配置項的值 TypeConverter/資料結構及其轉換

配置的同步 ConfigurationReloadToken

ConfigurationReloadToken本質上就是對 CancellationTokenSource的封裝。
註冊個回撥事件當配置源發生改變。

var config = new ConfigurationBuilder().Add(new MemoryConfigurationSource() { InitialData = source }).Build();
ChangeToken.OnChange(() => config.GetReloadToken(), () =>
    {
        Console.WriteLine("config change call back!");
    });
}

使用篇總結

:1.IConfigurationSource內由IConfigurationProvider提供資料, 2.IConfigurationBuilder build出來的IConfigurationRoot 作為根節點,IConfigurationRoot內部維護了一個IConfigurationProvider集合,是由IConfigurationBuilder 從自身的IConfigurationSource集合整理出來的。IConfigurationRoot與IConfigurationSection組成了樹形結構資料,但是IConfigurationSection的資料均是從根節點獲取的。

介面 實現 註釋
IConfigurationProvider ConfigurationProvider-MemoryConfigurationProvider 資料提供者
IConfigurationSource MemoryConfigurationSource 資料來源
IConfigurationBuilder ConfigurationBuilder Builder類收集資料來源建立IConfiguration
IConfiguration IConfigurationRoot/IConfigurationSection 讀取資料操作

原始碼分析

Microsoft.Extensions.Configuration.Abstractions

Microsoft.Extensions.Configuration

Microsoft.Extensions.Configuration.Binder

從三個方面入手原始碼,

  1. 資料來源收集與構建:IConfigurationBuilder ConfigurationBuilder 作為資料來源採集,然後建立出ConfigurationRoot,
  2. 對外操作類:IConfiguration 用來對外提供資料,實現了它的類跟介面有,ConfigurationSection IConfigurationSection,ConfigurationRoot IConfigurationRoot
  3. 資料來源 :IConfigurationSource ConfigurationProvider MemoryConfigurationSource MemoryConfigurationProvider

資料來源收集與構建 ConfigurationBuilder :IConfigurationBuilder

ConfigurationBuilder用來收集IConfigurationSource,並根據資料來源提供的provider用其build方法構建出ConfigurationRoot。

public class ConfigurationBuilder : IConfigurationBuilder
{
    // 返回用於獲取配置值的源。你通過Add方法新增的IConfigurationSource都存在這裡了
    public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();
    // 屬性則以字典的形式存放任意的自定義屬性。
    public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>();
    // Adds a new configuration source.
    public IConfigurationBuilder Add(IConfigurationSource source!!)
    {
        Sources.Add(source); return this;
    }
    // 方法很簡單,直接呼叫收集到的Source的build同名方法,然後拿到對應的provider 最後用此providers 集合生成 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);
    }
}

對外操作類 IConfiguration

表示一組鍵/值應用程式配置屬性。 用於讀取配置它對應了連個實現類IConfigurationSection與ConfigurationRoot

/// 表示一組鍵/值應用程式配置屬性。  
public interface IConfiguration
{
    // Gets or sets a configuration value. 當執行這個索引的時候,它會按照與 GetSection方法完全一致的邏輯得到一個 IConfigurationSection物件,並返回其 Value屬性
    string? this[string key] { get; set; }

    // 獲取具有指定鍵的配置子節。
    IConfigurationSection GetSection(string key);

    // 獲取直接子代配置子節。
    IEnumerable<IConfigurationSection> GetChildren();

    // 返回一個<see cref="IChangeToken"/>,用於觀察該配置何時被重新載入。  
    IChangeToken GetReloadToken();
}

ConfigurationSection:IConfigurationSection:IConfiguration

表示普通節點,其資料還是以IConfigurationRoot為源頭,其實就是對IConfigurationRoot的封裝讓使用這從使用邏輯上由一個樹形結構的資料結構 概念。利用屬性path 與屬性key 拼接成 字典key在內部找資料,所以不是所有section都會有值

// 表示應用程式配置值的一節。  
public interface IConfigurationSection : IConfiguration
{
    string Key { get; }
    // 節點的路徑.
    string Path { get; }
    //節點對應的資料。( 因為data是個字典所以你給出的key(路徑) 一定要是字典的key才會有值否則為null很正常)
    string? Value { get; set; }
}

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? this[string key]
    {
        get return _root[ConfigurationPath.Combine(Path, key)];
        set _root[ConfigurationPath.Combine(Path, key)] = value;
    }
    public IConfigurationSection GetSection(string key) => _root.GetSection(ConfigurationPath.Combine(Path, key));
    public IEnumerable<IConfigurationSection> GetChildren() => _root.GetChildrenImplementation(Path);
    public IChangeToken GetReloadToken() => _root.GetReloadToken();
    public string Path => _path;
    public string Key { get return _key;    }
    public string? Value { get  return _root[Path]; set _root[Path] = value; }
}

ConfigurationRoot:IConfigurationRoot:IConfiguration 表示根節點

它由ConfigurationBuilder建立出來,同時ConfigurationBuilder把收集到的IConfigurationProvider集合作為引數傳入,在構造方法內它會呼叫他們的load方法進行初始化對應的IConfigurationProvider的data屬性。用於後續呼叫Get方法使用。同時註冊了RaiseChanged這樣 仍和 一個provider發生了change 都會執行都會執行註冊在此root節點的ReloadToken回撥

我們將IConfigurationRoot 物件看作一棵配置樹的跟接單

  1. GetConfiguration 後來在居上
  2. this[string key] -> GetConfiguration
  3. IChangeToken GetReloadToken() => _changeToken; 獲取token 後可以註冊此root下任何一個provider 發生change時的回撥。
/// <summary>
/// The root node for a configuration.
/// </summary>
public class ConfigurationRoot : IConfigurationRoot, IDisposable
{
    private readonly IList<IConfigurationProvider> _providers;
    private readonly IList<IDisposable> _changeTokenRegistrations;
    private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();

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

    internal static string? GetConfiguration(IList<IConfigurationProvider> providers, string key)
    {
        for (int i = providers.Count - 1; i >= 0; i--)
        {
            IConfigurationProvider provider = providers[i];

            if (provider.TryGet(key, out string? value))
            {
                return value;
            }
        }
        return null;
    }

    // Gets or sets the value corresponding to a configuration key.
    public string? this[string key]
    {
        get => GetConfiguration(_providers, key);
        set => SetConfiguration(_providers, key, value);
    }

    public IEnumerable<IConfigurationSection> GetChildren() => this.GetChildrenImplementation(null);
    public IChangeToken GetReloadToken() => _changeToken;
    public IConfigurationSection GetSection(string key) => new ConfigurationSection(this, key);
    ....      
}

資料來源

netcore 的資料來源是由Source 以及 Proivder 組成。前者負責建立後者。後者提供具體的資料來源。

IConfigurationSource

此介面只有一個Build建立對應的provider.實現它的有常用類有FileConfigurationSource 以及MemoryConfigurationSource 請看具體的實現類

public interface IConfigurationSource
{
    IConfigurationProvider Build(IConfigurationBuilder builder);
}

ConfigurationProvider:IConfigurationProvider

作為其它provider 的基類就如微軟所的一樣“Base helper class for implementing an IConfigurationProvider”,負責了儲存子類整理好的資料來源以及根據此資料來源的一些基礎操作如get /GetChildKeys / GetReloadToken
以及一個比較重要的虛方法方法Load,由具體的子類實現如FiletConfigurationPorivder / MemoryConfigurationProvider(他沒實現Load方法因為他在建構函式就把這事情做了)以及構造方法中對
這裡的 OnReload()會觸發 Reload 通知並重新生成一個新的ReloadToken 注意新生成的ReloadToekn 是沒註冊任何回撥事件的。 可以通過GetReloadToken() 獲得對應的token 然後通過ChangeToken.OnChange方式註冊個callback

public abstract class ConfigurationProvider : IConfigurationProvider
{
    protected IDictionary<string, string?> Data { get; set; }
    private ConfigurationReloadToken _reloadToken = new ConfigurationReloadToken();
    protected ConfigurationProvider()
    {
        Data = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
    }
    public virtual bool TryGet(string key, out string? value) => Data.TryGetValue(key, out value);
    public virtual void Set(string key, string? value) => Data[key] = value;
    public virtual void Load(){ }

    // 當前節點下的所有子節點的key 這裡不包含孫子節點.
    public virtual IEnumerable<string> GetChildKeys(
        IEnumerable<string> earlierKeys,
        string? parentPath)
    {
        var results = new List<string>();

        if (parentPath is null)
        {
            foreach (KeyValuePair<string, string?> kv in Data)
            {
                results.Add(Segment(kv.Key, 0));
            }
        }
        else
        {
            Debug.Assert(ConfigurationPath.KeyDelimiter == ":");

            foreach (KeyValuePair<string, string?> kv in Data)
            {
                if (kv.Key.Length > parentPath.Length &&
                    kv.Key.StartsWith(parentPath, StringComparison.OrdinalIgnoreCase) &&
                    kv.Key[parentPath.Length] == ':')
                {
                    results.Add(Segment(kv.Key, parentPath.Length + 1));
                }
            }
        }

        results.AddRange(earlierKeys);

        results.Sort(ConfigurationKeyComparer.Comparison);

        return results;
    }

    private static string Segment(string key, int prefixLength)
    {
        int indexOf = key.IndexOf(ConfigurationPath.KeyDelimiter, prefixLength, StringComparison.OrdinalIgnoreCase);
        return indexOf < 0 ? key.Substring(prefixLength) : key.Substring(prefixLength, indexOf - prefixLength);
    }

    public IChangeToken GetReloadToken()=>return _reloadToken;}

    /// Triggers the reload change token and creates a new one.
    protected void OnReload()
    {
        ConfigurationReloadToken previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken());
        previousToken.OnReload();
    }
}

MemoryConfigurationSource: 非常簡單

InitialData欄位就是你在建立MemoryConfigurationSource提供的字典物件,build方法則建立一個MemoryConfigurationProvider並把自身this作為引數傳入,其目的很簡單。在Proivder 物件中可以訪問到Source物件,其建構函式內根據Source的InitialData初始化Data屬性.

public class MemoryConfigurationSource : IConfigurationSource
{
   /// The initial key value configuration pairs. 資料來源 
   public IEnumerable<KeyValuePair<string, string?>>? InitialData { get; set; }

   /// Builds the <see cref="MemoryConfigurationProvider"/> for this source.
   public IConfigurationProvider Build(IConfigurationBuilder builder)
   {
       return new MemoryConfigurationProvider(this);
   }
}

MemoryConfigurationProvider

因為這是最簡單的Provider 賦值把MemoryConfigurationSource的data 整理好給父類Data屬性就好。然後用父類Data屬性對外提供資料。所以MemoryConfigurationProvider並沒有重寫父類的Load方法

/// <summary>
/// In-memory implementation of <see cref="IConfigurationProvider"/>
/// </summary>
public class MemoryConfigurationProvider : ConfigurationProvider, IEnumerable<KeyValuePair<string, string?>>
{
   private readonly MemoryConfigurationSource _source;
   public MemoryConfigurationProvider(MemoryConfigurationSource source!!)
   {
       _source = source;
       foreach (KeyValuePair<string, string?> pair in _source.InitialData)
       {
           Data.Add(pair.Key, pair.Value);
       }
   }
   public void Add(string key, string? value) { Data.Add(key, value); }
   public IEnumerator<KeyValuePair<string, string?>> GetEnumerator() { return Data.GetEnumerator(); }
   IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }
}

其它

FileConfiguration

FileConfiguration以Json File Configuration ,整個實際跟上面介紹的一樣作為資料來源同樣由 1.Source 2. Provider組成 source作為Porvider的build類用於建立Porivder,建立出來的Provider提供具體的資料來源

兩條線 1.Provider 2.Source
Provider的繼承與實現關係:JsonConfigurationProvider->FileConfigurationProvider-> ConfigurationProvider->IConfigurationProvider
Source 的繼承與實現關係:JsonConfigurationSource -> FileConfigurationSource ->IConfigurationSource

其它資料來源

EnvironmentVariablesConfigurationSource

CommandLineConfigurationSource

總結

在使用層面上
IConfiguration介面對外提供配置資料,實現了此介面的有 IConfigurationSection & IConfigurationRoot 也由此兩個介面在程式碼上實現了一個具有樹行結構邏輯資料來源。
所有的資料來源均以字典key形式儲存的。所以你不給一個完整的路徑是得不到資料的。

在構建資料來源與資料提供方向:
IConfigurationProvider/IConfigurationSource/IConfigurationBuilder
IConfigurationProvider介面提供了set\get\load();等介面,前兩給負責使用者提供資料來源,而load方法用於初始化載入資料到自身的data屬性,它呼叫的時機是建立ConfigurationRoot的建構函式內。
IConfigurationSource 僅僅有一個build方法 當呼叫IConfigurationBuilder的build方法建立IConfigurationRoot是,就是呼叫每個IConfigurationSource的Build創Porivder並把此集合作為引數去建立IConfigurationRoot

它的Get資料流向:
根節點:IConfigurationRoot-> IConfigurationProvider
非根節點:IConfigurationSection -> IConfigurationRoot-> IConfigurationProvider
其中IConfigurationRoot的IConfigurationProvider由IConfigurationBuilder整理自身的IConfigurationSource提供在構造IConfigurationRoot作為引數傳入,值得注意的是IConfigurationRoot並不儲存資料,而是從對應的povider中獲取。

相關文章