【橙子老哥】.NetCore IConfiguration配置源码剖析解读

hello,大家好,今天还是橙子老哥的分享时间,希望大家一起学习,一起进步。

欢迎加入.net意社区,第一时间了解我们的动态

官方地址:https://ccnetcore.com

微信公众号:搜索意.Net / 或添加橙子老哥微信:chegnzilaoge520

管道也走了,日志也走了,不得来手配置?本章就带代价玩一玩IConfiguration,我们如何自定义扩展自己的配置,以及配置的自动刷新是个什么回事?

1、自定义配置

当然,一如既往,IConfiguration如何使用,相信大家已了如指掌。

var config= app.Services.GetRequiredService<IConfiguration>();
var test=config.GetValue<string>("Test");

这里就不在过多赘述了,我们先看看它如何新增自己的配置源

  1. 创建自定义配置提供程序首先,创建一个继承自 IConfigurationProvider 的类。
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;

public class CustomConfigurationSource : IConfigurationSource
{
    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        return new CustomConfigurationProvider();
    }
}

public class CustomConfigurationProvider : ConfigurationProvider
{
    public override void Load()
    {
        // 这里可以加载你的自定义配置
        // 示例: 从数据库、文件、API 等
        Data = new Dictionary<string, string>
        {
            {"MyApp:Setting1", "Value1"},
            {"MyApp:Setting2", "Value2"}
        };
    }
}
  1. 扩展 IConfigurationBuilder
    在自定义配置源后,你需要创建一个扩展方法,以方便地向 IConfigurationBuilder 添加你的配置源。
public static class CustomConfigurationExtensions
{
    public static IConfigurationBuilder AddCustomConfiguration(this IConfigurationBuilder builder)
    {
        builder.Sources.Insert(0, new CustomConfigurationSource());
        return builder;
    }
}

是不是非常简单?这里我们要提供扩展的核心对象,IConfigurationSourceIConfigurationProvider

通过IConfigurationSource的Build方法去创建我们自定义的IConfigurationProvider

2、IConfiguration 原理

我们从入口开始查看,config.GetValue他的GetValue方法


//扩展方法1,没做什么
    public static T? GetValue<T>(
      this IConfiguration configuration,
      string key)
    {
      return configuration.GetValue<T>(key, default (T));
    }
    
    //扩展方法2,没做什么
    public static T? GetValue<T>(
      this IConfiguration configuration,
      string key,
      T defaultValue)
    {
      return (T) configuration.GetValue(typeof (T), key, (object) defaultValue);
    }

//扩展方法3,通过GetSection获取IConfigurationSection ,IConfigurationSection 再去获取到真正的值,最后通过ConfigurationBinder将值绑定转换
    public static object? GetValue(
      this IConfiguration configuration,
      Type type,
      string key,
      object? defaultValue)
    {
      ThrowHelper.ThrowIfNull((object) configuration, nameof (configuration));
      ThrowHelper.ThrowIfNull((object) type, nameof (type));
      IConfigurationSection section = configuration.GetSection(key);
      string str = section.Value;
      return str != null ? ConfigurationBinder.ConvertValue(type, str, section.Path) : defaultValue;
    }

//扩展方法3,通过GetSection获取IConfigurationSection ,IConfigurationSection 再去获取到真正的值,最后通过ConfigurationBinder将值绑定转换

看到这里,大致就能猜到这套玩法的大概了,我们不能直接通过IConfiguration去获取到配置的值,需要通过一个key获取到它的节点IConfigurationSection ,这个节点才能获取到对应的配置值

但是由于配置文件直接读取出来,肯定就是一个字符串,还需要通过ConfigurationBinder将字符串转换成对应的我们需要的类型,就是这个思路。

上述的往里走的GetSectionIConfiguration接口下的方法,它有三个实现:

  1. ConfigurationManager (依赖注入的实现,入口)
  2. ConfigurationRoot (通过Manager进行创建)
  3. ConfigurationSection (在ConfigurationRoot 上包一层)

我们按照这个顺序去看,最核心的就是这一句话

	ConfigurationManager 类:
    public IConfigurationSection GetSection(string key)
    {
      return (IConfigurationSection) new ConfigurationSection((IConfigurationRoot) this, key);
    }

通过Manager的GetSection转换成IConfigurationRoot,丢到IConfigurationSection包一层

ConfigurationManager 主要用于管理配置的操作,我们新增配置源全靠它

 public interface IConfigurationBuilder
  {
    IDictionary<string, object> Properties { get; }
    IList<IConfigurationSource> Sources { get; }
    IConfigurationBuilder Add(IConfigurationSource source);
    IConfigurationRoot Build();
  }

ConfigurationRoot 主要用于配置的重载,以及管理配置的提供者

  public interface IConfigurationRoot : IConfiguration
  {
    void Reload();
    IEnumerable<IConfigurationProvider> Providers { get; }
  }

ConfigurationSection 主要用于获取实际的配置值

 public interface IConfigurationSection : IConfiguration
  {
    string Key { get; }
    string Path { get; }
    string? Value { get; set; }
  }

好家伙,绕来绕去,一个IConfiguration操作的ConfigurationManager 、ConfigurationRoot 、ConfigurationSection 就是一层套一层,不得不说,这样设计,真的很是巧妙

这种设计,我们是不是可以用到自己业务场景中?像需要扩展数据源相关的业务,用这种设计是非常合适的,例如: 不同plc数据源统一的读写操作等等、对接第三方系统数据等,不打算来尝试下?

3、IConfigurationSource与IConfigurationProvider原理

我们上面把整个配置的核心对象转了一圈,清楚了如何使用的原理,但是具体我们怎么去从json的配置文件中获取呢?这里我们再走下去

我们获取具体的配置值,肯定是通过ConfigurationSection对象的value属性进行获取的

    public string? Value
    {
      get => this._root[this.Path];
      set => this._root[this.Path] = value;
    }

这里又很巧妙,ConfigurationSection又调用了IConfigurationRoot中的索引方法,我们再到ConfigurationRoot中看看

   public string? this[string key]
    {
      get => ConfigurationRoot.GetConfiguration(this._providers, key);
      set => ConfigurationRoot.SetConfiguration(this._providers, key, value);
    }

//真正获取值的地方
    internal static string? GetConfiguration(IList<IConfigurationProvider> providers, string key)
    {
      for (int index = providers.Count - 1; index >= 0; --index)
      {
        string configuration;
        if (providers[index].TryGet(key, out configuration))
          return configuration;
      }
      return (string) null;
    }

这里,又是一个倒叙遍历的操作,但和管道模型意义不一样,它的目的是为了后续新增的配置源,会有限覆盖前面的配置源,所以从越后面的配置源开始找,找到了直接就return 啦

仔细看,这个providers,不就是我们之前自定义新增的吗?那我们是如何新增到providers集合里面的呢?

这里,我们看他通过ConfigurationBuilder将providers塞进去的

    public IConfigurationRoot Build()
    {
      List<IConfigurationProvider> providers = new List<IConfigurationProvider>();
      foreach (IConfigurationSource source in this._sources)
      {
        IConfigurationProvider configurationProvider = source.Build((IConfigurationBuilder) this);
        providers.Add(configurationProvider);
      }
      return (IConfigurationRoot) new ConfigurationRoot((IList<IConfigurationProvider>) providers);
    }

而它的Provider又是通过_sources来的,我们还要再往上,看看_sources是怎么加进去的

    public IDictionary<string, object> Properties { get; } = (IDictionary<string, object>) new Dictionary<string, object>();

    public IConfigurationBuilder Add(IConfigurationSource source)
    {
      ThrowHelper.ThrowIfNull((object) source, nameof (source));
      this._sources.Add(source);
      return (IConfigurationBuilder) this;
    }

到这里,就很清楚了,我们是通过IConfigurationBuilder去新增我们的IConfigurationSource,然后通过Build()之后,将sources中的Provider塞到root中
(IConfigurationRoot) new ConfigurationRoot((IList<IConfigurationProvider>) providers)
往后就形成了闭环,Sorce、Provider、IConfiguration

4、默认实现

结束了吗?还没有,我们前面讲了,如何新增自己的配置,多个配置之间如何进行获取到值,以及他们值是如何进行管理的,一样的,即使我们什么实现也不加,默认.netcore中也能json的配置文件,我们看看他是怎么实现的(猜也猜的到,读取file,转json)

JsonConfigurationProvider 是json的实现,它继承于FileConfigurationProvider

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 override void Load(Stream stream)
    {
      try
      {
        this.Data = JsonConfigurationFileParser.Parse(stream);
      }
      catch (JsonException ex)
      {
        throw new FormatException(SR.Error_JSONParseError, (Exception) ex);
      }
    }
  }

嗯,对的,就这么几行,stream是通过FileConfigurationProvider来的,通过json的一个静态类,转成key、value的键值对,给data就行了

5、隐藏知识-IOptionsMonitor实时读取问题

大家是否对 IOptionsMonitor为什么能实时对配置文件进行一个监控?配置文件变了,IOptionsMonitor的值也就变了?但是!有时候好像又不会生效,比如立马更改完之后去读取,好像又读取不到最新的值

这是有bug?我们一探究竟

本着中国那句古话,来都来了,不妨我们看看,FileConfigurationProvider,它是怎么看监控到文件的?

我们看看它的构造函数:

    public FileConfigurationProvider(FileConfigurationSource source)
    {
      ThrowHelper.ThrowIfNull((object) source, nameof (source));
      this.Source = source;
      if (!this.Source.ReloadOnChange || this.Source.FileProvider == null)
        return;
      this._changeTokenRegistration = ChangeToken.OnChange((Func<IChangeToken>) (() => this.Source.FileProvider.Watch(this.Source.Path)), (Action) (() =>
      {
        Thread.Sleep(this.Source.ReloadDelay);
        this.Load(true);
      }));
    }

初始化的时候,使用了this.Source.FileProvider.Watch(this.Source.Path)去监控文件的变化。这个是来自于
IFileProvider这个接口的能力,但是这里我们也能注意到,在重新load配置文件的时候,执行了一个Thread.Sleep(this.Source.ReloadDelay);

这个属性,是在FileConfigurationSource中,我们可以进行更改

 public int ReloadDelay { get; set; } = 250;

思考下,为什么会有一个250ms的限制?因为配置文件不是用来实时存数据的!!!就这么简单,好像是这个道理,如果有大量又实时更新的数据,为什么不丢数据库?你说是吧~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值