前言
友情提示:建議閱讀本文之前先了解下.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對節點資料的讀取操作。