• 一 背景

相比.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);
                }
            }
        }

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

从使用的角度考虑,框架做了一些封装,并提供了不少扩展方法。但是基本的读取逻辑都是一样的

如果我们引入 “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 也是如此。所以说 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 读取配置数据。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注