.Net Core 配置源碼學習 (一)


一 背景

相比.Net Framework , .NET Core的配置系統 ,有一些明顯的優點 ,如:

1 支持更豐富的配置源

2 讀取配置時, 可以用相同的方式讀取, 並且非常方便

3 修改配置后,不用重啟應用 

本期對配置相關的源碼簡單梳理一下。 只說主邏輯 ,並且從用的角度說起 ,逐步深入。

二 配置讀取

2.1 舉個栗子

進入上代碼環節 ,新建控制台應用 , 只需要引入 "Microsoft.Extensions.Configuration" 1個nuget包 ,就可以使用基本的配置了 。

(不得不說,相較於.NET Framework,.Net Core 的組件化設計實現的相當徹底 ) 

 github地址

//數據源
var
dicData = new Dictionary<string, string>() { { "Key1", "Value1" }, { "Key2", "Value2" } }; IConfigurationRoot configurationRoot = new ConfigurationBuilder()   .Add(new MemoryConfigurationSource() { InitialData = dicData }) // 添加配置源   .Build(); // 構建配置 Console.WriteLine("Key1=" + configurationRoot["Key1"]);//輸出內容 Key1=Value1

 

我們好奇 configurationRoot["Key1"] 是怎么讀取到 "Value1" 呢 ? 這就需要看源碼了。 IConfigurationRoot到底是什么 , 以及 ["key1"] 的代碼執行邏輯是怎樣的 。

(注意: 為了方便了解主線邏輯 , 本文的中 .Net Core 源碼有刪減 )

 

2.2 IConfigurationRoot 相關源碼

public interface IConfiguration
    {        
        string this[string key] { get; set; }        
    }
     
    public interface IConfigurationRoot : IConfiguration
    {  
        IEnumerable<IConfigurationProvider> Providers { get; }
    }

     public class ConfigurationRoot : IConfigurationRoot, IDisposable
    {
        private readonly IList<IConfigurationProvider> _providers;
              
        public IEnumerable<IConfigurationProvider> Providers => _providers;
        
        /// <summary>
        /// 根據指定的Key獲取或者設置對應的Value
        /// </summary>
        /// <param name="key">The configuration key.</param>
        /// <returns>The configuration value.</returns>
        public string this[string key]
        {
            get
            {
                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
            {
                if (!_providers.Any())
                {
                    throw new InvalidOperationException(Resources.Error_NoSources);
                }

                foreach (var provider in _providers)
                {
                    provider.Set(key, value);
                }
            }
        }

 

1 通過上面的代碼發現 ,IConfigurationRoot 有個 Providers 屬性 ;

2 讀取配置是通過ConfigurationRoot類中的索引器實現的 , 更具體的說就是通過 遍歷Providers ,調用其.TryGet(key, out var value) 獲取的 。

3 細心的同學會發現,一旦獲取到配置就結束並返回了。那么就有了從哪先獲取的問題 ,我們發現遍歷時是LIFO(后入先出)的順序,所以后面添加的Provider有更高的優先級 。

到目前為止 ,我們只需要記住一個主知識點獲取配置是由多個 Provider供應者) provide (供應) 的 ,並且越后面的 Provider 優先級越高,具有覆蓋性。

 

再想深入就得研究一下 : IConfigurationProvider是怎么回事 , 特別是其中的 TryGet 方法是怎么個邏輯 。

2.3 IConfigurationProvider 相關源碼

public interface IConfigurationProvider
    {
        bool TryGet(string key, out string value);
        void Load();
    }

 public abstract class ConfigurationProvider : IConfigurationProvider
    {    
        /// <summary>
        /// The configuration key value pairs for this provider.
        /// </summary>
        protected IDictionary<string, string> Data { get; set; } 
       
        /// <summary>
        /// 根據指定的 鍵 獲取對應的 值, 獲取到返回true,否則返回 false
        /// </summary>      
        public virtual bool TryGet(string key, out string value)
            => Data.TryGetValue(key, out value);
        
        /// <summary>
        /// 加載數據
        /// </summary>
        public virtual void Load()
        {
        }
    }

 

代碼非常簡單 ,每個Provider 維護了一個 Data (字典類型) 屬性 , 配置就是從這個字典拿到的。 到此基本的讀取配置邏輯我們已經知道了 ,給自己點個贊吧 💪

2.4 使用便利性

這時 ,我們可以再看開頭那個栗子,讀取配置其實還有這種寫法 :

configurationRoot.Providers.First().TryGet("Key1", out string Val);
Console.WriteLine("Key1=" + Val); //輸出 Val=Value1

 

但是這種寫法就感覺有些累贅了,還是 configurationRoot["Key1"] 更方便一些 , 有時我們甚至連"[" 和 "]" 都不想打 !

從使用的角度考慮,框架做了一些封裝,並提供了不少擴展方法。但是基本的讀取邏輯都是一樣的

如果我們引入 "Microsoft.Extensions.Configuration.Binder" nuget包 ,讀取配置操作時會發現一些新的方法。

比如配置和強模型綁定,並結合泛型使用 ,如:

public static T Get<T>(this IConfiguration configuration)

public static T GetValue<T>(this IConfiguration configuration, string key, T defaultValue)

三 配置寫入

 

現在我們需要找Provider中的Data 屬性是怎么寫入的 ,還從開頭的控制台程序入手 , 其中有這樣一段代碼 :

new ConfigurationBuilder().Add(new MemoryConfigurationSource() { InitialData = dicData }).Build();

 

我們重點看Add方法

3.1 IConfigurationBuilder 相關源碼

public interface IConfigurationBuilder
    {
        IList<IConfigurationSource> Sources {get;}

        IConfigurationBuilder Add(IConfigurationSource source);

        IConfigurationRoot Build();
    }

    /// <summary>
    /// Used to build key/value based configuration settings for use in an application.
    /// </summary>
    public class ConfigurationBuilder : IConfigurationBuilder
    {
        /// <summary>
        /// Returns the sources used to obtain configuration values.
        /// </summary>
        public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();

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

            Sources.Add(source);
            return this;
        }

        /// <summary>
        /// Builds an <see cref="IConfiguration"/> with keys and values from the set of providers registered in
        /// <see cref="Sources"/>.
        /// </summary>
        /// <returns>An <see cref="IConfigurationRoot"/> with keys and values from the registered providers.</returns>
        public IConfigurationRoot Build()
        {
            var providers = new List<IConfigurationProvider>();
            foreach (var source in Sources)
            {
                var provider = source.Build(this);
                providers.Add(provider);
            }
            return new ConfigurationRoot(providers);
        }
    }

 

Add就是把配置源 Source ,添加到內部的Sources列表。IConfigurationBuilder 除了Add方法還有一個 Build 方法。

IConfigurationBuilder 就像一個車間 ,Add一堆零件后 ,Build一下, 整出來一輛 汽車 (IConfigurationRoot) ,  很明顯IConfigurationBuilder是一個構建者模式 。

我們繼續探索 IConfigurationSource ,看 Build 方法的實現邏輯 .

 

3.2 IConfigurationSource 相關源碼

 

    public interface IConfigurationSource
    {
        IConfigurationProvider Build(IConfigurationBuilder builder);
    }

    public class MemoryConfigurationSource : IConfigurationSource
    {
        /// <summary>
        /// The initial key value configuration pairs.
        /// </summary>
        public IEnumerable<KeyValuePair<string, string>> InitialData { get; set; }

        /// <summary>
        /// Builds the <see cref="MemoryConfigurationProvider"/> for this source.
        /// </summary>
        /// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param>
        /// <returns>A <see cref="MemoryConfigurationProvider"/></returns>
        public IConfigurationProvider Build(IConfigurationBuilder builder)
        {
            return new MemoryConfigurationProvider(this);
        }
    }
    
 

 可以看到 ,IConfigurationSource 只干了一件事 :其 Build 方法返回一個Provider

還拿開頭的栗子來講 ,MemoryConfigurationSource.Build 方法返回了 一個新的 MemoryConfigurationProvider 實例。

再看下 MemoryConfigurationProvider

 public class MemoryConfigurationProvider : ConfigurationProvider, IEnumerable<KeyValuePair<string, string>>
    {
        private readonly MemoryConfigurationSource _source;

        /// <summary>
        /// Initialize a new instance from the source.
        /// </summary>
        /// <param name="source">The source settings.</param>
        public MemoryConfigurationProvider(MemoryConfigurationSource source)
        {
            if (source == null)
            {
                throw new ArgumentNullException(nameof(source));
            }

            _source = source;

            if (_source.InitialData != null)
            {
                foreach (var pair in _source.InitialData)
                {
                    Data.Add(pair.Key, pair.Value);
                }
            }
        }

        /// <summary>
        /// Add a new key and value pair.
        /// </summary>
        /// <param name="key">The configuration key.</param>
        /// <param name="value">The configuration value.</param>
        public void Add(string key, string value)
        {
            Data.Add(key, value);
        } 
    }

 

MemoryConfigurationProvider 的構造函數完成了數據的初始化 。

到此為止,開頭的那個栗子我們就梳理完了。

也許是我們開頭那個例子過於簡單了 , 寫配置這塊有點虎頭蛇尾的感覺 。

我們再看一下主角 IConfigurationProvider , 發現其中定義的 Load 方法 ,我們還知道 ConfigurationProvider 是一個虛擬類 ,其 Load 方法還是個虛方法。

我們在翻一下源碼看有沒有其它類繼承它呢 ? 如果有的話 ,它們是不是覆蓋了 Load 方法呢?

果然我們發現了 不少 Provider   , 如 JsonConfigurationProvider,XmlConfigurationProvider,EnvironmentVariablesConfigurationProvider 等。

為方便講解,我們以EnvironmentVariablesConfigurationProvider 為例,並精簡了代碼。 Load 方法負責了負載配置的任務。男團其他成員也是如此。所以說 Provider們 實力擔當。

public class EnvironmentVariablesConfigurationProvider : ConfigurationProvider
    {
        private readonly string _prefix;

        /// <summary>
        /// Initializes a new instance.
        /// </summary>
        public EnvironmentVariablesConfigurationProvider() : this(string.Empty)
        { }

        /// <summary>
        /// Initializes a new instance with the specified prefix.
        /// </summary>
        /// <param name="prefix">A prefix used to filter the environment variables.</param>
        public EnvironmentVariablesConfigurationProvider(string prefix)
        {
            _prefix = prefix ?? string.Empty;
        }

        /// <summary>
        /// Loads the environment variables.
        /// </summary>
        public override void Load()
        {
            Load(Environment.GetEnvironmentVariables());
        }

        internal void Load(IDictionary envVariables)
        {
            var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

            var filteredEnvVariables = envVariables
                .Cast<DictionaryEntry>()
                .Where(entry => ((string)entry.Key).StartsWith(_prefix, StringComparison.OrdinalIgnoreCase));

            foreach (var envVariable in filteredEnvVariables)
            {
                var key = ((string)envVariable.Key).Substring(_prefix.Length);
                data[key] = (string)envVariable.Value;
            }

            Data = data;
        } 
    }

 

但是 IConfigurationSource 呢 ,是不是可有可無 ?其實不是。IConfigurationSource 作用是提供原始配置,並將必要的信息傳遞給 Provider 。

我們現在大概知道 IConfigurationBuilder , IConfigurationProvider , IConfigurationRoot ,IConfigurationSource 之間的關系。我們總結一下,

1 各種配置原始數據( IConfigurationSource) 通過 IConfigurationProvider 轉成鍵值對格式的配置數據

2 IConfigurationBuilder 內部維護多個 IConfigurationSource,IConfigurationBuilder 中的 Build方法 構建出 IConfigurationRoot

3 IConfigurationRoot 內部維護多個 IConfigurationProvider,並通過 IConfigurationProvider 讀取配置數據

闖盪天下的天外流星全套劍譜奉上

 

 

 

限於篇幅 ,本篇介紹 配置讀取配置寫入 2個知識點。

下一篇會介紹 配置變更監控 ,自定義配置 2個知識點。

 

感覺寫的還湊合就點個贊吧 😊


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM