Configuration總體介紹
微軟在.NET Core裡設計出了全新的配置體系,並以非常靈活、可擴充套件的方式實現。從其原始碼來看,其執行機制大致是,根據其Source,建立一個Builder例項,並會向其新增Provider,在我們使用配置資訊的時候,會從記憶體中獲取相應的Provider例項。
.NET Core採用了統一的呼叫方式來載入不同型別的配置資訊,並通過統一的抽象介面IConfigurationSource對配置源進行管理,這也是剛剛所說的靈活。而其擴充套件性就是我們可以自己自定義新的Provider例項,而不會改變其原來的呼叫方式。接下來的文章將會基於Consul,擴充套件一個新的Provider例項。
在ASP.NET Core 中,我們的應用配置是基於IConfigurationProvider的鍵值對。 我們先看一下思維導圖:
基於上圖,我們可以看到主要有鍵值對源有多種,分別是:
環境變數
命令列引數
各種形式的配置檔案
記憶體物件
使用者自定義擴充套件源
核心物件
在介紹.NET Core配置功能之前,先簡要說明一下Microsoft.Extensions.Configuration.Abstractions,該元件抽象了.NET Core的配置功能,並對自定義擴充套件制定了新的標準。以下介紹的四個核心物件全部來自於該元件。
IConfiguration
該介面表示一組鍵/值應用程式配置屬性,應用程式使用配置時的入口物件,.NET Core對其有多種擴充套件,其派生類包括位於統一類庫的IConfigurationSection,以及Microsoft.Extensions.Configuration類庫中的ConfigurationRoot、ConfigurationSection、IConfigurationRoot。我們可以通過DI獲取IConfiguration例項。
它主要有以下三個方法:
- GetChildren():獲取直接子配置子節
- GetReloadToken():返回一個IChangeToken,可用於確定何時重新載入配置
- GetSection(String):獲取指定鍵的子節點
我們來看一下原始碼:
1: /// <summary>
2: /// Represents a set of key/value application configuration properties.
3: /// </summary>
4: public interface IConfiguration
5: {
6: /// <summary>
7: /// Gets or sets a configuration value.
8: /// </summary>
9: /// <param name="key">The configuration key.</param>
10: /// <returns>The configuration value.</returns>
11: string this[string key] { get; set; }
12:
13: /// <summary>
14: /// Gets a configuration sub-section with the specified key.
15: /// </summary>
16: /// <param name="key">The key of the configuration section.</param>
17: /// <returns>The <see cref="IConfigurationSection"/>.</returns>
18: /// <remarks>
19: /// This method will never return <c>null</c>. If no matching sub-section is found with the specified key,
20: /// an empty <see cref="IConfigurationSection"/> will be returned.
21: /// </remarks>
22: IConfigurationSection GetSection(string key);
23:
24: /// <summary>
25: /// Gets the immediate descendant configuration sub-sections.
26: /// </summary>
27: /// <returns>The configuration sub-sections.</returns>
28: IEnumerable<IConfigurationSection> GetChildren();
29:
30: /// <summary>
31: /// Returns a <see cref="IChangeToken"/> that can be used to observe when this configuration is reloaded.
32: /// </summary>
33: /// <returns>A <see cref="IChangeToken"/>.</returns>
34: IChangeToken GetReloadToken();
35: }
通常我們要求配置檔案要有足夠的靈活性,尤其是我們所擴充套件的配置資訊存放在了其他伺服器,當修改的時候我們很需要一套監控功能,以及時靈活的應對配置資訊的修改。現在.NET Core為我們提供了這樣一個功能,我們只需要自定義少量程式碼即可完成配置資訊的同步。這個方法就是GetReloadToken(),其返回值是IChangeToken。此處對配置資訊的同步只做一個引子,後面的文章會詳細說明。
由於ConfigurationRoot、ConfigurationSection聚集於IConfiguration介面,此處也對這兩個類進行討論,方便我們對.NET Core的配置功能有個更加形象的印象。這兩個介面,本質上就是.NET Core關於配置資訊的讀取方式。
XML是使用比較廣泛的一種資料結構,我們在配置XML時,一般會使用根節點、父節點、子節點之類的術語,此處也一樣。
ConfigurationRoot是配置的根節點,也實現了IConfigurationRoot,此介面只有一個方法,其主要功能就是實現對配置資訊的重新載入,另外還包括一個IConfigurationProvider型別的集合屬性。其原始碼如下
1: /// <summary>
2: /// Represents the root of an <see cref="IConfiguration"/> hierarchy.
3: /// </summary>
4: public interface IConfigurationRoot : IConfiguration
5: {
6: /// <summary>
7: /// Force the configuration values to be reloaded from the underlying <see cref="IConfigurationProvider"/>s.
8: /// </summary>
9: void Reload();
10:
11: /// <summary>
12: /// The <see cref="IConfigurationProvider"/>s for this configuration.
13: /// </summary>
14: IEnumerable<IConfigurationProvider> Providers { get; }
15: }
下面是ConfigurationRoot關於Reload()方法的實現
1: /// <summary>
2: /// Force the configuration values to be reloaded from the underlying sources.
3: /// </summary>
4: public void Reload()
5: {
6: foreach (var provider in _providers)
7: {
8: provider.Load();
9: }
10:
11: RaiseChanged();
12: }
通過原始碼我們知道,如果呼叫了Reload()方法,所有型別的Provider都會重新載入。
前面有ConfigurationRoot表示配置的根節點,那麼ConfigurationSection則表示非跟節點,畢竟父節點、子節點都是相對,所以此處使用非根節點。ConfigurationSection繼承於IConfigurationSection,該介面只有三個只讀屬性,分別表示配置資訊的Key、Value以及路徑資訊,需要指出的是,此處的路徑資訊主要指從根節點到當前節點的路徑,以表示當前節點的位置,類似於A:B:C可以表示節點C的位置,其中A、B、C都是ConfigurationSection的Key。以下是ConfigurationSection的原始碼
1: /// <summary>
2: /// Represents a section of application configuration values.
3: /// </summary>
4: public interface IConfigurationSection : IConfiguration
5: {
6: /// <summary>
7: /// Gets the key this section occupies in its parent.
8: /// </summary>
9: string Key { get; }
10:
11: /// <summary>
12: /// Gets the full path to this section within the <see cref="IConfiguration"/>.
13: /// </summary>
14: string Path { get; }
15:
16: /// <summary>
17: /// Gets or sets the section value.
18: /// </summary>
19: string Value { get; set; }
20: }
IConfigurationBuilder
該介面主要用於建立IConfigurationProvider,其派生類包括Microsoft.Extensions.Configuration.ConfigurationBuilder。其成員包括
兩個只讀屬性:
- Properties:獲取可用於在IConfigurationBuilder之間共享資料的鍵/值集合
- Sources:該屬性用於快取不同的配置源,以用於相對應的Provider的建立
兩個方法:
- Add(IConfigurationSource source):新增IConfigurationSource,並新增到屬性中Sources中
- Build():該方法遍歷Sources屬性,並呼叫IConfigurationSource的Build()方法,通過獲取Provider集合,最終建立IConfigurationRoot物件
ConfigurationBuilder原始碼如下
1: /// <summary>
2: /// Used to build key/value based configuration settings for use in an application.
3: /// </summary>
4: public class ConfigurationBuilder : IConfigurationBuilder
5: {
6: /// <summary>
7: /// Returns the sources used to obtain configuration values.
8: /// </summary>
9: public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();
10:
11: /// <summary>
12: /// Gets a key/value collection that can be used to share data between the <see cref="IConfigurationBuilder"/>
13: /// and the registered <see cref="IConfigurationProvider"/>s.
14: /// </summary>
15: public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>();
16:
17: /// <summary>
18: /// Adds a new configuration source.
19: /// </summary>
20: /// <param name="source">The configuration source to add.</param>
21: /// <returns>The same <see cref="IConfigurationBuilder"/>.</returns>
22: public IConfigurationBuilder Add(IConfigurationSource source)
23: {
24: if (source == null)
25: {
26: throw new ArgumentNullException(nameof(source));
27: }
28:
29: Sources.Add(source);
30: return this;
31: }
32:
33: /// <summary>
34: /// Builds an <see cref="IConfiguration"/> with keys and values from the set of providers registered in
35: /// <see cref="Sources"/>.
36: /// </summary>
37: /// <returns>An <see cref="IConfigurationRoot"/> with keys and values from the registered providers.</returns>
38: public IConfigurationRoot Build()
39: {
40: var providers = new List<IConfigurationProvider>();
41: foreach (var source in Sources)
42: {
43: var provider = source.Build(this);
44: providers.Add(provider);
45: }
46: return new ConfigurationRoot(providers);
47: }
48: }
此處令人感慨頗多,我們最終呼叫 ConfigurationRoot 的建構函式,究其原因是Provider提供了統一的資料訪問方式,不管是基於何種型別的Provider,我們都可以呼叫其Load()方法載入配置項。此外,IConfigurationBuilder本身有很多的擴充套件方法來註冊資料來源,比如AddJsonFile()擴充套件方法。我們來看一下,我們常見的寫法,
1: var builder = new ConfigurationBuilder()
2:
3: .SetBasePath(env.ContentRootPath)
4:
5: .AddJsonFile("appsettings1.json", false, true)
6:
7: .AddJsonFile("appsettings2.json", false, true);
8:
9: Configuration = builder.Build();
IConfigurationSource
該介面表示應用程式配置的鍵值對。其派生類包括Microsoft.Extensions.Configuration.ChainedConfigurationSource、Microsoft.Extensions.Configuration.Memory.MemoryConfigurationSource。另外該派生類還會在檔案類配置場景下依賴Microsoft.Extensions.Configuration.FileExtensions元件。
它是所有配置源的抽象表示,包括JSON、XML、INI、環境變數等等。通過上文我們也知道了,IConfigurationBuilder會註冊多個IConfigurationSource例項。它只有一個方法,就是Build()方法,並返回IConfigurationProvider,由此可見,IConfigurationProvider的建立依賴於IConfigurationSource,這也是一一對應的關係。所有不同的源最終都會轉化成統一的鍵值對錶示。
以下為
1: /// <summary>
2: /// Represents a source of configuration key/values for an application.
3: /// </summary>
4: public interface IConfigurationSource
5: {
6: /// <summary>
7: /// Builds the <see cref="IConfigurationProvider"/> for this source.
8: /// </summary>
9: /// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param>
10: /// <returns>An <see cref="IConfigurationProvider"/></returns>
11: IConfigurationProvider Build(IConfigurationBuilder builder);
12: }
以下是MemoryConfigurationSource的原始碼
1: /// <summary>
2: /// Represents in-memory data as an <see cref="IConfigurationSource"/>.
3: /// </summary>
4: public class MemoryConfigurationSource : IConfigurationSource
5: {
6: /// <summary>
7: /// The initial key value configuration pairs.
8: /// </summary>
9: public IEnumerable<KeyValuePair<string, string>> InitialData { get; set; }
10:
11: /// <summary>
12: /// Builds the <see cref="MemoryConfigurationProvider"/> for this source.
13: /// </summary>
14: /// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param>
15: /// <returns>A <see cref="MemoryConfigurationProvider"/></returns>
16: public IConfigurationProvider Build(IConfigurationBuilder builder)
17: {
18: return new MemoryConfigurationProvider(this);
19: }
20: }
IConfigurationProvider
通過上文的介紹,我們可以知道IConfigurationProvider是統一的對外介面,對使用者提供配置的查詢、重新載入等功能。其派生類包括Microsoft.Extensions.Configuration.ConfigurationProvider、Microsoft.Extensions.Configuration.ChainedConfigurationProvider、Microsoft.Extensions.Configuration.Memory.MemoryConfigurationProvider。另外該派生類還會在檔案類配置場景下依賴Microsoft.Extensions.Configuration.FileExtensions元件。
以下是Microsoft.Extensions.Configuration.ConfigurationProvider的原始碼:
1: /// <summary>
2: /// Base helper class for implementing an <see cref="IConfigurationProvider"/>
3: /// </summary>
4: public abstract class ConfigurationProvider : IConfigurationProvider
5: {
6: private ConfigurationReloadToken _reloadToken = new ConfigurationReloadToken();
7:
8: /// <summary>
9: /// Initializes a new <see cref="IConfigurationProvider"/>
10: /// </summary>
11: protected ConfigurationProvider()
12: {
13: Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
14: }
15:
16: /// <summary>
17: /// The configuration key value pairs for this provider.
18: /// </summary>
19: protected IDictionary<string, string> Data { get; set; }
20:
21: /// <summary>
22: /// Attempts to find a value with the given key, returns true if one is found, false otherwise.
23: /// </summary>
24: /// <param name="key">The key to lookup.</param>
25: /// <param name="value">The value found at key if one is found.</param>
26: /// <returns>True if key has a value, false otherwise.</returns>
27: public virtual bool TryGet(string key, out string value)
28: => Data.TryGetValue(key, out value);
29:
30: /// <summary>
31: /// Sets a value for a given key.
32: /// </summary>
33: /// <param name="key">The configuration key to set.</param>
34: /// <param name="value">The value to set.</param>
35: public virtual void Set(string key, string value)
36: => Data[key] = value;
37:
38: /// <summary>
39: /// Loads (or reloads) the data for this provider.
40: /// </summary>
41: public virtual void Load()
42: { }
43:
44: /// <summary>
45: /// Returns the list of keys that this provider has.
46: /// </summary>
47: /// <param name="earlierKeys">The earlier keys that other providers contain.</param>
48: /// <param name="parentPath">The path for the parent IConfiguration.</param>
49: /// <returns>The list of keys for this provider.</returns>
50: public virtual IEnumerable<string> GetChildKeys(
51: IEnumerable<string> earlierKeys,
52: string parentPath)
53: {
54: var prefix = parentPath == null ? string.Empty : parentPath + ConfigurationPath.KeyDelimiter;
55:
56: return Data
57: .Where(kv => kv.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
58: .Select(kv => Segment(kv.Key, prefix.Length))
59: .Concat(earlierKeys)
60: .OrderBy(k => k, ConfigurationKeyComparer.Instance);
61: }
62:
63: private static string Segment(string key, int prefixLength)
64: {
65: var indexOf = key.IndexOf(ConfigurationPath.KeyDelimiter, prefixLength, StringComparison.OrdinalIgnoreCase);
66: return indexOf < 0 ? key.Substring(prefixLength) : key.Substring(prefixLength, indexOf - prefixLength);
67: }
68:
69: /// <summary>
70: /// Returns a <see cref="IChangeToken"/> that can be used to listen when this provider is reloaded.
71: /// </summary>
72: /// <returns></returns>
73: public IChangeToken GetReloadToken()
74: {
75: return _reloadToken;
76: }
77:
78: /// <summary>
79: /// Triggers the reload change token and creates a new one.
80: /// </summary>
81: protected void OnReload()
82: {
83: var previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken());
84: previousToken.OnReload();
85: }
86:
87: /// <summary>
88: /// Generates a string representing this provider name and relevant details.
89: /// </summary>
90: /// <returns> The configuration name. </returns>
91: public override string ToString() => $"{GetType().Name}";
92: }
通過原始碼,我們可以知道ConfigurationProvider以字典型別快取了多個Provider物件,有需要的時候,從記憶體中獲取即可,配置的載入通過Load()方法實現,在ConfigurationRoot裡我們介紹了其Reload,並且說明其方法是在迴圈呼叫ConfigurationProvider的Load方法,但是此處只提供了一個虛方法,其目的是要交給其他具體的Provider,比如環境變數、JSON、XML等,這些具體的Provider可以從相應的配置源中獲取配置資訊。所有的子節點KEY通過GetChildKeys方法實現,其重新載入方式通過ConfigurationReloadToken例項完成。
另外需要說明一下,在ConfigurationProvider建構函式裡,對字典進行了初始化,並同時設定了字典Key不受大小寫限制,這是一個需要注意的細節。
Configuration元件結構
通過檢視.NET配置功能的原始碼,所有依賴均基於Microsoft.Extensions.Configuration.Abstractions,在其上有一層實現,即Microsoft.Extensions.Configuration,其內部也多數是抽象實現,並提供了多個虛方法交給其派生元件,比如環境變數、命令列引數、各種檔案型配置等,當然各種檔案型配置還要依賴Microsoft.Extensions.Configuration.FileExtensions元件。
以下是.NET Core 3.0預覽版裡的Configuration各個元件的結構圖: