.NET Core 3.0之深刻源碼理解Configuration(一)

Configuration整體介紹

微軟在.NET Core裏設計出了全新的配置體系,並以很是靈活、可擴展的方式實現。從其源碼來看,其運行機制大體是,根據其Source,建立一個Builder實例,並會向其添加Provider,在咱們使用配置信息的時候,會從內存中獲取相應的Provider實例。html

.NET Core採用了統一的調用方式來加載不一樣類型的配置信息,並經過統一的抽象接口IConfigurationSource對配置源進行管理,這也是剛剛所說的靈活。而其擴展性就是咱們能夠本身自定義新的Provider實例,而不會改變其原來的調用方式。接下來的文章將會基於Consul,擴展一個新的Provider實例。json

在ASP.NET Core 中,咱們的應用配置是基於IConfigurationProvider的鍵值對。 咱們先看一下思惟導圖:緩存

image

基於上圖,咱們能夠看到主要有鍵值對有多種,分別是:服務器

 環境變量數據結構

命令行參數app

各類形式的配置文件ide

內存對象函數

用戶自定義擴展源 ui

核心對象

在介紹.NET Core配置功能以前,先簡要說明一下Microsoft.Extensions.Configuration.Abstractions,該組件抽象了.NET Core的配置功能,並對自定義擴展制定了新的標準。如下介紹的四個核心對象所有來自於該組件。this

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各個組件的結構圖:

相關文章
相關標籤/搜索