重新整理 .net core 实践篇————配置系统之盟约[五]
阅读原文时间:2021年05月31日阅读:1

前言

在asp .net core 中我们会看到一个appsettings.json 文件,它就是我们在服务中的各种配置,是至关重要的一部门。

不管是官方自带的服务,还是我们自己编写的服务都是用它来实现自己服务的动态配置,这就是约定。

配置文件之所以会成为约定,最主要的原因就是好用,不然可能第三方的配置文件管理的就会出来替代官方的配置文件管理系统,官方也提供了对应的接口来让第三方接入。

正文

官方提供了 Microsoft.Extensions.Configuration.Abstration接口。

同时提供了Microsoft.Extensions.Configuration 来作为实现。

所以我们如果想自己写第三方的包,那么就可以对 Microsoft.Extensions.Configuration.Abstration 的某一部分或者全部实现。

初学.net core的时候,陷入了一个误区,当时认为配置系统就是从json中进行读取,实际上配置系统是可以从命令行中获取、从环境变量中获取,只要他们符合key-value这种字符串键值对的方式。

配置文件系统有四个主要的接口,也可以理解为四个主要的模块功能。后面用代码解释一下这几个的作用。

  1. IConfiguration

  2. IConfigurationRoot

  3. IConfigurationSection

  4. IConfigurationBuilder

配置扩展点:

1.IConfigurationSource

2.IConfigurationProvider

扩展上诉两个可以帮助扩展不同配置来源。

static void Main(string[] args)
{
    IConfigurationBuilder builder = new ConfigurationBuilder();
    builder.AddInMemoryCollection(new Dictionary<string,string>()
    {
        {"key1","value1"},
        {"key2","value2"},
    });
    IConfigurationRoot configurationRoot = builder.Build();

    Console.WriteLine(configurationRoot["key1"]);
    Console.WriteLine(configurationRoot["key2"]);
}

看下ConfugurationBuilder:

/// <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>
    /// Gets a key/value collection that can be used to share data between the <see cref="IConfigurationBuilder"/>
    /// and the registered <see cref="IConfigurationProvider"/>s.
    /// </summary>
    public IDictionary<string, object> Properties { get; } = 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="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 (IConfigurationSource source in Sources)
        {
            IConfigurationProvider provider = source.Build(this);
            providers.Add(provider);
        }
        return new ConfigurationRoot(providers);
    }
}

在看一个东西的功能的时候一定要看下顶部这句话:

/// <summary>
/// Used to build key/value based configuration settings for use in an application.
/// </summary>
public class ConfigurationBuilder : IConfigurationBuilder

翻译过来就是用于构建在应用程序中使用的基于键/值的配置设置。

那么这个时候我们可以重点看下build,毕竟是用来构建的。

从上述语义中,大概知道是IConfigurationSource 转换为 IConfigurationProvider。

那么重点看下这两个。

先看IConfigurationSource 接口。

/// <summary>
/// Represents a source of configuration key/values for an application.
/// </summary>
public interface IConfigurationSource
{
    /// <summary>
    /// Builds the <see cref="IConfigurationProvider"/> for this source.
    /// </summary>
    /// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param>
    /// <returns>An <see cref="IConfigurationProvider"/></returns>
    IConfigurationProvider Build(IConfigurationBuilder builder);
}

表示一个配置文件的来源。里面只有一个方法就是Provider,这时候猜想IConfigurationProvider就是用于统一获取值的方式的。

看下Provider:

/// <summary>
/// Provides configuration key/values for an application.
/// </summary>
public interface IConfigurationProvider
{
    /// <summary>
    /// Tries to get a configuration value for the specified key.
    /// </summary>
    /// <param name="key">The key.</param>
    /// <param name="value">The value.</param>
    /// <returns><c>True</c> if a value for the specified key was found, otherwise <c>false</c>.</returns>
    bool TryGet(string key, out string value);

    /// <summary>
    /// Sets a configuration value for the specified key.
    /// </summary>
    /// <param name="key">The key.</param>
    /// <param name="value">The value.</param>
    void Set(string key, string value);

    /// <summary>
    /// Returns a change token if this provider supports change tracking, null otherwise.
    /// </summary>
    /// <returns>The change token.</returns>
    IChangeToken GetReloadToken();

    /// <summary>
    /// Loads configuration values from the source represented by this <see cref="IConfigurationProvider"/>.
    /// </summary>
    void Load();

    /// <summary>
    /// Returns the immediate descendant configuration keys for a given parent path based on this
    /// <see cref="IConfigurationProvider"/>s data and the set of keys returned by all the preceding
    /// <see cref="IConfigurationProvider"/>s.
    /// </summary>
    /// <param name="earlierKeys">The child keys returned by the preceding providers for the same parent path.</param>
    /// <param name="parentPath">The parent path.</param>
    /// <returns>The child keys.</returns>
    IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath);
}

查看头部:

/// <summary>
/// Provides configuration key/values for an application.
/// </summary>
public interface IConfigurationProvider

为应用提供key value配置。

那么这时候猜想ConfigurationRoot就是来整合配置的。

先看我们上文一组IConfigurationSource,IConfigurationProvider的具体实现MemoryConfigurationSource 和 MemoryConfigurationProvider。

MemoryConfigurationSource :

/// <summary>
/// Represents in-memory data as an <see cref="IConfigurationSource"/>.
/// </summary>
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);
    }
}

MemoryConfigurationProvider:

/// <summary>
/// In-memory implementation of <see cref="IConfigurationProvider"/>
/// </summary>
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 (KeyValuePair<string, string> 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);
    }

    /// <summary>
    /// Returns an enumerator that iterates through the collection.
    /// </summary>
    /// <returns>An enumerator that can be used to iterate through the collection.</returns>
    public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
    {
        return Data.GetEnumerator();
    }

    /// <summary>
    /// Returns an enumerator that iterates through the collection.
    /// </summary>
    /// <returns>An enumerator that can be used to iterate through the collection.</returns>
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

所以我们如果要扩展的来源的话,需要实现IConfigurationSource、IConfigurationProvider即可。

那么有时候我们会在配置文件中看到:

{
  "section1:key3":"value3"
}

是否这个解释含义是section1:key3作为key然后value3作为value呢?

其实不是,这是为了能够让配置分组。

static void Main(string[] args)
{
    IConfigurationBuilder builder = new ConfigurationBuilder();
    builder.AddInMemoryCollection(new Dictionary<string,string>()
    {
        {"key1","value1"},
        {"key2","value2"},
        {"section1:key3","values3"}
    });
    IConfigurationRoot configurationRoot = builder.Build();

    Console.WriteLine(configurationRoot["key1"]);
    Console.WriteLine(configurationRoot["key2"]);
    Console.WriteLine(configurationRoot["section1:key3"]);
    Console.WriteLine(configurationRoot.GetSection("section1")["key3"]);
}

可以看到configurationRoot 既可以把section1:key3 当作key,同时也可以把section1:key3,当作以section1为分组下面的key3。

这里我们就走进configurationRoot,看下它是如何让我们通过索引的方式获取值的。

configurationRoot 下的索引:

public string this[string key]
{
    get
    {
        for (int i = _providers.Count - 1; i >= 0; i--)
        {
            IConfigurationProvider provider = _providers[i];

            if (provider.TryGet(key, out string value))
            {
                return value;
            }
        }

        return null;
    }
    set
    {
        if (!_providers.Any())
        {
            throw new InvalidOperationException(SR.Error_NoSources);
        }

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

这个其实就是遍历我们的providers。那么来看下getsection部分。

public IConfigurationSection GetSection(string key)
            => new ConfigurationSection(this, key);

查看ConfigurationSection的主要部分:

public ConfigurationSection(IConfigurationRoot root, string path)
{
    if (root == null)
    {
        throw new ArgumentNullException(nameof(root));
    }

    if (path == null)
    {
        throw new ArgumentNullException(nameof(path));
    }

    _root = root;
    _path = path;
}
public string this[string key]
{
    get
    {
        return _root[ConfigurationPath.Combine(Path, key)];
    }

    set
    {
        _root[ConfigurationPath.Combine(Path, key)] = value;
    }
}

实例化主要是记录section的值,并且记录IConfigurationRoot来源。

然后其索引方式还是调用了IConfigurationRoot,只是setion的值和key值做了一些处理,这个处理是ConfigurationPath来完成的。

看下ConfigurationPath:

/// <summary>
/// The delimiter ":" used to separate individual keys in a path.
/// </summary>
public static readonly string KeyDelimiter = ":";

/// <summary>
/// Combines path segments into one path.
/// </summary>
/// <param name="pathSegments">The path segments to combine.</param>
/// <returns>The combined path.</returns>
public static string Combine(params string[] pathSegments)
{
    if (pathSegments == null)
    {
        throw new ArgumentNullException(nameof(pathSegments));
    }
    return string.Join(KeyDelimiter, pathSegments);
}

从上诉看,ConfigurationSection的原理还是很简单的。

getSetion 部分把前缀记录下来,然后和key值做一个拼接,还是调用ConfigurationRoot的索引部分。

经过简单的分析,我们完全可以玩套娃模式。

static void Main(string[] args)
{
    IConfigurationBuilder builder = new ConfigurationBuilder();
    builder.AddInMemoryCollection(new Dictionary<string,string>()
    {
        {"key1","value1"},
        {"key2","value2"},
        {"section1:key3","values3"},
        {"section2:section3:key4","values4"}
    });
    IConfigurationRoot configurationRoot = builder.Build();
    var section2 = configurationRoot.GetSection("section2");
    var section3 = section2.GetSection("section3");
    Console.WriteLine(section3["key4"]);
}

虽然不提倡,但是可以这么干。

下一节,配置文件之军令(命令行)

以上只是个人整理,如有错误,望请指出,谢谢。