.Net Core Configuration原始碼探究
阿新 • • 發佈:2020-06-23
### 前言
[上篇文章](https://www.cnblogs.com/wucy/p/13157245.html)我們演示了為Configuration新增Etcd資料來源,並且瞭解到為Configuration擴充套件自定義資料來源還是非常簡單的,核心就是把資料來源的資料按照一定的規則讀取到指定的字典裡,這些都得益於微軟設計的合理性和便捷性。本篇文章我們將一起探究Configuration原始碼,去了解Configuration到底是如何工作的。
### ConfigurationBuilder
相信使用了.Net Core或者看過.Net Core原始碼的同學都非常清楚,.Net Core使用了大量的Builder模式許多核心操作都是是用來了Builder模式,微軟在.Net Core使用了許多在傳統.Net框架上並未使用的設計模式,這也使得.Net Core使用更方便,程式碼更合理。Configuration作為.Net Core的核心功能當然也不例外。
其實並沒有Configuration這個類,這只是我們對配置模組的代名詞。其核心是IConfiguration介面,IConfiguration又是由IConfigurationBuilder構建出來的,我們找到[IConfigurationBuilder原始碼](https://github.com/dotnet/extensions/blob/v3.1.5/src/Configuration/Config.Abstractions/src/IConfigurationBuilder.cs)大致定義如下
```cs
public interface IConfigurationBuilder
{
IDictionary Properties { get; }
IList Sources { get; }
IConfigurationBuilder Add(IConfigurationSource source);
IConfigurationRoot Build();
}
```
Add方法我們上篇文章曾使用過,就是為ConfigurationBuilder新增ConfigurationSource資料來源,新增的資料來源被存放在Sources這個屬性裡。當我們要使用IConfiguration的時候通過Build的方法得到IConfiguration例項,IConfigurationRoot介面是繼承自IConfiguration介面的,待會我們會探究這個介面。
我們找到IConfigurationBuilder的預設實現類[ConfigurationBuilder](https://github.com/dotnet/extensions/blob/v3.1.5/src/Configuration/Config/src/ConfigurationBuilder.cs)大致程式碼實現如下
```cs
public class ConfigurationBuilder : IConfigurationBuilder
{
///
/// 新增的資料來源被存放到了這裡
///
public IList Sources { get; } = new List();
public IDictionary Properties { get; } = new Dictionary();
///
/// 新增IConfigurationSource資料來源
///
///
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();
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。
### Configuration
通過上面我們瞭解到通過ConfigurationBuilder構建出來的並非是直接實現IConfiguration的實現類而是另一個介面[IConfigurationRoot](https://github.com/dotnet/extensions/blob/v3.1.5/src/Configuration/Config.Abstractions/src/IConfigurationRoot.cs)
#### ConfigurationRoot
通過原始碼我們可以知道IConfigurationRoot是繼承自IConfiguration,具體定義關係如下
```cs
public interface IConfigurationRoot : IConfiguration
{
///
/// 強制重新整理資料
///
///
void Reload();
IEnumerable Providers { get; }
}
public interface IConfiguration
{
string this[string key] { get; set; }
///
/// 獲取指定名稱子資料節點
///
///
IConfigurationSection GetSection(string key);
///
/// 獲取所有子資料節點
///
///
IEnumerable GetChildren();
///
/// 獲取IChangeToken用於當資料來源有資料變化時,通知外部使用者
///
///
IChangeToken GetReloadToken();
}
```
接下來我們檢視IConfigurationRoot實現類[ConfigurationRoot](https://github.com/dotnet/extensions/blob/v3.1.5/src/Configuration/Config/src/ConfigurationRoot.cs)的大致實現,程式碼有刪減
```cs
public class ConfigurationRoot : IConfigurationRoot, IDisposable
{
private readonly IList _providers;
private readonly IList _changeTokenRegistrations;
private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();
public ConfigurationRoot(IList providers)
{
_providers = providers;
_changeTokenRegistrations = new List(providers.Count);
//通過便利的方式呼叫ConfigurationProvider的Load方法,將資料載入到每個ConfigurationProvider的字典裡
foreach (var p in providers)
{
p.Load();
//監聽每個ConfigurationProvider的ReloadToken實現如果資料來源發生變化去重新整理Token通知外部發生變化
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
}
}
////
/// 讀取或設定配置相關資訊
///
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 GetChildren() => this.GetChildrenImplementation(null);
public IChangeToken GetReloadToken() => _changeToken;
public IConfigurationSection GetSection(string key)
=> new ConfigurationSection(this, key);
////
/// 手動呼叫該方法也可以實現強制重新整理的效果
///
public void Reload()
{
foreach (var provider in _providers)
{
provider.Load();
}
RaiseChanged();
}
////
/// 強烈推薦不熟悉Interlocked的同學研究一下Interlocked具體用法
///
private void RaiseChanged()
{
var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
previousToken.OnReload();
}
}
```
上面展示了ConfigurationRoot的核心實現其實主要就是兩點
如果我想獲取Products節點下的第一條商品資料直接
```cs
IConfigurationSection productSection = configuration.GetSection("Products:0")
```
類比到這裡的話根配置IConfigurationRoot裡儲存了訂單的所有資料,獲取下來的子節點IConfigurationSection表示了訂單裡第一個商品的資訊,而這個商品也是一個完整的描述商品資訊的資料系統,所以這樣可以更清晰的區分Configuration的結構,我們來看一下ConfigurationSection的大致實現
```cs
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 GetChildren() => _root.GetChildrenImplementation(Path);
public IChangeToken GetReloadToken() => _root.GetReloadToken();
}
```
這裡我們可以看到既然有Key可以獲取字典裡對應的Value了,為何還需要Path?通過ConfigurationRoot裡的程式碼我們可以知道Path的初始值其實就是獲取ConfigurationSection的Key,說白了其實就是如何獲取到當前IConfigurationSection的路徑。比如
```cs
//當前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的擴充套件方法](https://github.com/dotnet/extensions/blob/v3.1.5/src/Configuration/Config/src/InternalConfigurationRootExtensions.cs)
```cs
internal static class InternalConfigurationRootExtensions
{
////
/// 其實就是在資料來源字典裡獲取Key包含給定Path的所有值
///
internal static IEnumerable GetChildrenImplementation(this IConfigurationRoot root, string path)
{
return root.Providers
.Aggregate(Enumerable.Empty(),
(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擴充套件類了](https://github.com/dotnet/extensions/blob/v3.1.5/src/Configuration/Config.Binder/src/ConfigurationBinder.cs),由於實現比較複雜,也不是本篇文章的重點,有興趣的同學可以自行查閱,這裡就不做探究了。
### 總結
通過以上部分的講解,其實我們可以大概的將Configuration配置相關總結為兩大核心抽象介面IConfigurationBuilder,IConfiguration,整體結構關係可大致表示成如下關係
配置相關的整體實現思路就是IConfigurationSource作為一種特定型別的資料來源,它提供了提供當前資料來源的提供者ConfigurationProvider,Provider負責將資料來源的資料按照一定的規則放入到字典裡。IConfigurationSource新增到IConfigurationBuilder的容器中,後者使用Provide構建出整個程式的根配置容器IConfigurationRoot。通過獲取IConfigurationRoot子節點得到IConfigurationSection負責維護子節點容器相關。這二者都繼承自IConfiguration,然後通過他們就可以獲取到整個配置體系的資料資料操作了。
以上講解都是本人通過實踐和閱讀原始碼得出的結論,可能會存在一定的偏差或理解上的誤區,但是我還是想把我的理解分享給大家,希望大家能多多包涵。如果有大家有不同的見解或者更深的理解,可以在評論區多多留言。
- 讀取的方式其實是迴圈匹配註冊進來的每個provider裡的資料,是後來者居上的模式,同名key後註冊進來的會先被讀取到,然後直接返回
- 構造ConfigurationRoot的時候才把資料載入到記憶體中,而且為註冊進來的每個provider設定監聽回撥
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 |
以上講解都是本人通過實踐和閱讀原始碼得出的結論,可能會存在一定的偏差或理解上的誤區,但是我還是想把我的理解分享給大家,希望大家能多多包涵。如果有大家有不同的見解或者更深的理解,可以在評論區多多留言。