關於Asp.net core配置資訊讀取的原始碼分析梳理

realyrare發表於2021-11-05

概述

我們都知道asp.net core配置資訊的讀取離不開IConfigurationSource和IConfigurationProvider這兩個類,ConfigurationSource可以提供一個ConfigurationProvider,然後去讀取資訊。究竟他們之間有著怎樣的千絲萬縷,我們一起來看看原始碼。

首先我們來建立一個.net core控制檯專案,來執行以下程式碼:

 class Program
    {
        static void Main(string[] args)
        {
            ConfigurationBuilder configBuilder = new ConfigurationBuilder();
            configBuilder.SetBasePath(Directory.GetCurrentDirectory())   
                   .AddJsonFile("appsettings.json");
            var configFile = configBuilder.Build();

            Console.ReadKey();
        }
    }

短短几行 程式碼看起來很簡單,就是用來讀取appsettings.json檔案中的配置資訊。然而我們今天想搞清楚其背後執行的原理,要花點時間。

首先、我們根據程式碼ConfigurationBuilder configBuilder = new ConfigurationBuilder();知道建立了一個configBuilder物件;

其次,configBuilder.SetBasePath(Directory.GetCurrentDirectory()) 該程式碼的呼叫我們也能大概見名知義,獲取當前的目錄;

接下來,重點來了,configBuilder.AddJsonFile("appsettings.json")的實現究竟是怎樣的?我們來看下原始碼的實現:

f12進去後原始碼如下:

/// <summary>Extension methods for adding <see cref="T:Microsoft.Extensions.Configuration.Json.JsonConfigurationProvider" />.</summary>
  public static class JsonConfigurationExtensions
  {
    /// <summary>Adds the JSON configuration provider at <paramref name="path" /> to <paramref name="builder" />.</summary>
    /// <param name="builder">The <see cref="T:Microsoft.Extensions.Configuration.IConfigurationBuilder" /> to add to.</param>
    /// <param name="path">Path relative to the base path stored in
    /// <see cref="P:Microsoft.Extensions.Configuration.IConfigurationBuilder.Properties" /> of <paramref name="builder" />.</param>
    /// <returns>The <see cref="T:Microsoft.Extensions.Configuration.IConfigurationBuilder" />.</returns>
    public static IConfigurationBuilder AddJsonFile(
      this IConfigurationBuilder builder,
      string path)
    {
      return builder.AddJsonFile((IFileProvider) null, path, false, false);
    }
}

緊接著f12再看實現的原始碼,依然在JsonConfigurationExtensions這個擴充套件類裡面:

    public static IConfigurationBuilder AddJsonFile(
      this IConfigurationBuilder builder,
      IFileProvider provider,
      string path,
      bool optional,
      bool reloadOnChange)
    {
      if (builder == null)
        throw new ArgumentNullException(nameof (builder));
      if (string.IsNullOrEmpty(path))
        throw new ArgumentException(SR.Error_InvalidFilePath, nameof (path));
      return builder.AddJsonFile((Action<JsonConfigurationSource>) (s =>
      {
        s.FileProvider = provider;
        s.Path = path;
        s.Optional = optional;
        s.ReloadOnChange = reloadOnChange;
        s.ResolveFileProvider();
      }));
    }

這時候有沒有發現builder.AddJsonFile((Action<JsonConfigurationSource>)這個方法裡面出現了一個關鍵的資訊點:JsonConfigurationSource (JsonConfigurationSource 繼承抽象類FileConfigurationSource,而FileConfigurationSource:IConfigurationSource) 。 關係如下圖:

 看完上面這個關係圖後,我們緊接著上面builder.AddJsonFile()的實現原始碼繼續f12往下,原始碼如下:

  
//該程式碼依然在JsonConfigurationExtensions類裡面
public static IConfigurationBuilder AddJsonFile( this IConfigurationBuilder builder, Action<JsonConfigurationSource> configureSource) { return ConfigurationExtensions.Add<JsonConfigurationSource>(builder, (Action<M0>) configureSource); }

我們看到上面的擴充套件方法實現是ConfigurationExtensions.Add...,再往下看實現:

public static class ConfigurationExtensions
  {
    /// <summary>Adds a new configuration source.</summary>
    /// <param name="builder">The <see cref="T:Microsoft.Extensions.Configuration.IConfigurationBuilder" /> to add to.</param>
    /// <param name="configureSource">Configures the source secrets.</param>
    /// <typeparam name="TSource" />
    /// <returns>The <see cref="T:Microsoft.Extensions.Configuration.IConfigurationBuilder" />.</returns>
    public static IConfigurationBuilder Add<TSource>(
      this IConfigurationBuilder builder,
      Action<TSource> configureSource)
      where TSource : IConfigurationSource, new()
    {
      TSource source = new TSource();
      if (configureSource != null)
        configureSource(source);
      return builder.Add((IConfigurationSource) source);
    }
}

到這裡我們看到了其實就是IConfigurationBuilder呼叫了Add方法,新增了一個資料來源(JsonConfigurationSource),至於JsonConfigurationSource類裡面做了什麼,我們看下實現

  public class JsonConfigurationSource : FileConfigurationSource
  {
    /// <summary>Builds the <see cref="T:Microsoft.Extensions.Configuration.Json.JsonConfigurationProvider" /> for this source.</summary>
    /// <param name="builder">The <see cref="T:Microsoft.Extensions.Configuration.IConfigurationBuilder" />.</param>
    /// <returns>A <see cref="T:Microsoft.Extensions.Configuration.Json.JsonConfigurationProvider" /></returns>
    public override IConfigurationProvider Build(IConfigurationBuilder builder)
    {
      this.EnsureDefaults(builder);
      return (IConfigurationProvider) new JsonConfigurationProvider(this);
    }
  }

JsonConfigurationSource類面的Build方法提供了一個JsonConfigurationProvider類,這裡再貼下JsonConfigurationProvider類裡面的程式碼:

  /// <summary>A JSON file based <see cref="T:Microsoft.Extensions.Configuration.FileConfigurationProvider" />.</summary>
  public class JsonConfigurationProvider : FileConfigurationProvider
  {
    /// <summary>Initializes a new instance with the specified source.</summary>
    /// <param name="source">The source settings.</param>
    public JsonConfigurationProvider(JsonConfigurationSource source)
      : base((FileConfigurationSource) source)
    {
    }

    /// <summary>Loads the JSON data from a stream.</summary>
    /// <param name="stream">The stream to read.</param>
    public virtual void Load(Stream stream)
    {
      try
      {
        this.set_Data(JsonConfigurationFileParser.Parse(stream));
      }
      catch (JsonException ex)
      {
        throw new FormatException(SR.Error_JSONParseError, (Exception) ex);
      }
    }
  }

關於JsonConfigurationProvider裡面的Load就是去讀取資訊的實現,至於Load的具體實現我們不再深究。我們回到最初的控制檯configBuilder.Build(),看看其的實現:

  public class ConfigurationBuilder : IConfigurationBuilder
  {
    /// <summary>Returns the sources used to obtain configuration values.</summary>
    public IList<IConfigurationSource> Sources { get; } = (IList<IConfigurationSource>) new List<IConfigurationSource>();

    /// <summary>Gets a key/value collection that can be used to share data between the <see cref="T:Microsoft.Extensions.Configuration.IConfigurationBuilder" />
    /// and the registered <see cref="T:Microsoft.Extensions.Configuration.IConfigurationProvider" />s.</summary>
    public IDictionary<string, object> Properties { get; } = (IDictionary<string, object>) new Dictionary<string, object>();

    /// <summary>Adds a new configuration source.</summary>
    /// <param name="source">The configuration source to add.</param>
    /// <returns>The same <see cref="T:Microsoft.Extensions.Configuration.IConfigurationBuilder" />.</returns>
    public IConfigurationBuilder Add(IConfigurationSource source)
    {
      if (source == null)
        throw new ArgumentNullException(nameof (source));
      this.Sources.Add(source);
      return (IConfigurationBuilder) this;
    }

    /// <summary>Builds an <see cref="T:Microsoft.Extensions.Configuration.IConfiguration" /> with keys and values from the set of providers registered in
    /// <see cref="P:Microsoft.Extensions.Configuration.ConfigurationBuilder.Sources" />.</summary>
    /// <returns>An <see cref="T:Microsoft.Extensions.Configuration.IConfigurationRoot" /> with keys and values from the registered providers.</returns>
    public IConfigurationRoot Build()
    {
      List<IConfigurationProvider> configurationProviderList = new List<IConfigurationProvider>();
      foreach (IConfigurationSource source in (IEnumerable<IConfigurationSource>) this.Sources)
      {
        IConfigurationProvider configurationProvider = source.Build((IConfigurationBuilder) this);
        configurationProviderList.Add(configurationProvider);
      }
      return (IConfigurationRoot) new ConfigurationRoot((IList<IConfigurationProvider>) configurationProviderList);
    }
  }

看到這個原始碼的時候有沒有種豁然開朗的感覺,前面我們說到IConfigurationBuilder呼叫了Add方法新增一個資料來源,並沒說新增了一個資料來源存在了哪裡,到底有什麼用處,現在在上面ConfigurationBuilder類裡面看到存在了Sources 集合裡面。然後configBuilder.Build()

去呼叫的時候遍歷資料來源(Sources )集合,緊接著source (IConfigurationSource)呼叫了Build方法構建了一個configurationProvider物件存到configurationProviderList集合裡面,最後在返回一個ConfigurationRoot物件的建構函式裡面傳遞了configurationProviderList集合去執行。

貼上ConfigurationRoot的原始碼:

  public class ConfigurationRoot : IConfigurationRoot, IConfiguration, IDisposable
  {

    private readonly IList<IConfigurationProvider> _providers;
    private readonly IList<IDisposable> _changeTokenRegistrations;

    /// <summary>Initializes a Configuration root with a list of providers.</summary>
    /// <param name="providers">The <see cref="T:Microsoft.Extensions.Configuration.IConfigurationProvider" />s for this configuration.</param>
    public ConfigurationRoot(IList<IConfigurationProvider> providers)
    {
      if (providers == null)
        throw new ArgumentNullException(nameof (providers));
      this._providers = providers;
      this._changeTokenRegistrations = (IList<IDisposable>) new List<IDisposable>(providers.Count);
      foreach (IConfigurationProvider provider in (IEnumerable<IConfigurationProvider>) providers)
      {
        IConfigurationProvider p = provider;
        p.Load();
        this._changeTokenRegistrations.Add(ChangeToken.OnChange((Func<IChangeToken>) (() => p.GetReloadToken()), (Action) (() => this.RaiseChanged())));
      }
    }
}

看到沒,最後providers去呼叫了load方法。

結語

就上面的控制檯程式碼來說IConfigurationSource對應的實現是JsonConfigurationSource;IConfigurationProvider,抽象類ConfigurationProvider對應的實現為JsonConfigurationProvider。如果我們要換成別的檔案格式呢?比如ini,怎樣自定義配置源呢?大家可以先想想,其實也很簡單,下次跟大家分享。

最後說真的,.netCore原始碼真的特別優秀,很值得花一番時間去看看!從其中可以學到許多架構知識!

 

相關文章