.Net Core配置Configuration具体实现

最近又研究了一下.NetCore配置选项的源码实现,又学习到了不少东西。这篇文章先写一下IConfiguration的学习成果,Options的后面补上

 

核心类

  • ConfigurationBuilder:IConfigurationBuilder (构建IConfiguration)
  • IConfigurationSource(配置数据来源)
  • IConfigurationProvider(将配置源的原始结构转为为IDictionary<string, string>)
  • ConfigurationRoot:IConfigurationRoot:IConfiguration (配置根节点)

 

构建

ConfigurationBuilder

下面是ConfigurationBuilder中的主要代码

可以看到ConfigurationBuilder的主要功能就是配置数据源到集合中

在Build时依次调用IConfigurationSource的Build函数,并将返回的IConfigurationProvider加入到List中

最后用IConfigurationProvider的集合构建一个ConfigurationRoot对象

public IList<IConfigurationSource> Sources = new List<IConfigurationSource>();

public IConfigurationBuilder Add(IConfigurationSource source)
{
  Sources.Add(source);
  return this;
}

public IConfigurationRoot Build()
{
  List<IConfigurationProvider> list = new List<IConfigurationProvider>();
  foreach (IConfigurationSource source in Sources)
  {
      IConfigurationProvider item = source.Build(this);
      list.Add(item);
  }

  return new ConfigurationRoot(list);
}

IConfigurationSource

public class EnvironmentVariablesConfigurationSource : IConfigurationSource
{
  public string Prefix;
  public IConfigurationProvider Build(IConfigurationBuilder builder)
  {
      return new EnvironmentVariablesConfigurationProvider(Prefix);
  }
  public EnvironmentVariablesConfigurationSource()
  {
  }
}
  
 
public class CommandLineConfigurationSource : IConfigurationSource
{
  public IDictionary<string, string> SwitchMappings;
  public IEnumerable<string> Args;
  public IConfigurationProvider Build(IConfigurationBuilder builder)
  {
      return new CommandLineConfigurationProvider(Args, SwitchMappings);
  }
  public CommandLineConfigurationSource()
  {
  }
}

//JsonConfigurationSource继承自FileConfigurationSource,我这里将其合为一个了
public abstract class JsonConfigurationSource : IConfigurationSource
{
public IFileProvider FileProvider { get; set; }
public string Path { get; set; }
public bool Optional { get; set; }
public bool ReloadOnChange { get; set; }
public int ReloadDelay { get; set; } = 250;

public Action<FileLoadExceptionContext> OnLoadException { get; set; }
  
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
FileProvider = FileProvider ?? builder.GetFileProvider();
OnLoadException = OnLoadException ?? builder.GetFileLoadExceptionHandler();
return new JsonConfigurationProvider(this);
}
  
public void ResolveFileProvider()
{
if (FileProvider == null && !string.IsNullOrEmpty(Path) && System.IO.Path.IsPathRooted(Path))
{
 string directoryName = System.IO.Path.GetDirectoryName(Path);
 string text = System.IO.Path.GetFileName(Path);
 while (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName))
 {
  text = System.IO.Path.Combine(System.IO.Path.GetFileName(directoryName), text);
  directoryName = System.IO.Path.GetDirectoryName(directoryName);
 }
 if (Directory.Exists(directoryName))
 {
  FileProvider = new PhysicalFileProvider(directoryName);
  Path = text;
 }
}
}
}

上面展示了比较常用的三种ConfigurationSource,代码都比较简单。

也很容易看出来ConfigurationSource的作用就是配置数据源,并不解析数据。

解析数据源的功能由 IConfigurationProvider完成

ConfigurationProvider

下面为IConfigurationProvider接口定义的5个函数

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

void Set(string key, string value);

IChangeToken GetReloadToken();

void Load();

IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath);
}

ConfigurationProvider是一个抽象类,继承了IConfigurationProvider接口

在新建Provider时一般都会选择直接继承ConfigurationProvider,接下来看一下ConfigurationProvider的几个核心方法

public abstract class ConfigurationProvider : IConfigurationProvider
{
private ConfigurationReloadToken _reloadToken = new ConfigurationReloadToken();
  
protected IDictionary<string, string> Data= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

public virtual bool TryGet(string key, out string value)=>Data.TryGetValue(key, out value);

public virtual void Set(string key, string value)=>Data[key] = value;

public virtual void Load(){}

public IChangeToken GetReloadToken()
{
return _reloadToken;
}

protected void OnReload()
{
ConfigurationReloadToken configurationReloadToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken());
configurationReloadToken.OnReload();
}

可以推测出:

  • Load函数负责从源数据读取数据然后给字典Data赋值
  • ConfigurationProvider将数据存储在字典Data中,增加修改都是对字典的操作
  • 每个ConfigurationProvider都会生成一个IChangeToken,在OnReload函数被调用时生成新的Token,并调用原Token的OnReload函数

ConfigurationRoot

在ConfigurationBuilder的Build函数中,我们生成了一个ConfigurationRoot,并给他传递了所有的ConfigrationProvider列表,下面我们看看他用我们的Provider都做了啥吧

private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();

public ConfigurationRoot(IList<IConfigurationProvider> providers)
{
  _providers = providers;
  _changeTokenRegistrations = new List<IDisposable>(providers.Count);
  foreach (IConfigurationProvider p in providers)
  {
      p.Load();
      ChangeToken.OnChange(p.GetReloadToken, 
         delegate{
   var oldToken=Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
               oldToken.OnReload();
              })
  }
}

public IChangeToken GetReloadToken()=>_changeToken;

上面的代码也对部分地方进行了简化。可以看到ConfigurationRoot在生成时主要就做了两件事

  • 1.调用Provider的Load函数,这会给Provider的Data赋值
  • 2.读取Provider的ReloadToken,每个Provider的Reload事件都会触发ConfigurationRoot自己的ReloadToken的Reload事件

至此配置的数据源构建这块就分析完啦!

 

查询

常规的配置查询有两种基本方式 :索引器和GetSection(string key)

其余的GetValue等等都是一些扩展方法,本篇文章不对此进行展开研究

索引器

索引器的查询执行的方式是倒叙查询所有的Provider,然后调用Provider的TryGet函数,在查询时重名的Key,最后加入的会生效。

赋值则是依次调用每个Provider的Set函数

public string this[string key]
{
get
{
for (int num = _providers.Count - 1; num >= 0; num--)
{
 if (_providers[num].TryGet(key, out var value))
 {
  return value;
 }
}
return null;
}
set
{
foreach (IConfigurationProvider provider in _providers)
{
 provider.Set(key, value);
}
}
}

GetSection

public IConfigurationSection GetSection(string key)
{
return new ConfigurationSection(this, key);
}

public class ConfigurationSection : IConfigurationSection, IConfiguration
{
private readonly IConfigurationRoot _root;
private readonly string _path;
private string _key;
public string Value
{
get
{
 return _root[Path];
}
set
{
 _root[Path] = value;
}
}

  //ConfigurationPath.Combine = string.Join(":",paramList);
public string this[string key]
{
get
{
 return _root[ConfigurationPath.Combine(Path, key)];
}
set
{
 _root[ConfigurationPath.Combine(Path, key)] = value;
}
}

public ConfigurationSection(IConfigurationRoot root, string path)
{
_root = root;
_path = path;
}

public IConfigurationSection GetSection(string key)
{
return _root.GetSection(ConfigurationPath.Combine(Path, key));
}

public IEnumerable<IConfigurationSection> GetChildren()
{
return _root.GetChildrenImplementation(Path);
}

public IChangeToken GetReloadToken()
{
return _root.GetReloadToken();
}
}

可以看到GetSection会生成一个ConfigurationSection对象

而ConfigurationSection在读取/设置值时实际上就是对查询的Key用:拼接,然后调用IConfigurationRoot(_root)的赋值或查询函数

关于Configuration的配置和读取的知识点大概就是以上这些了,还有更深入的涉及到对象的绑定这一块Get<> Bind<> GetChildren()等,比较难读,要一行一行代码看,以后有时间可能再研究一下

最后贴上一个从数据加载配置源并动态更新的小例子

 

DBConfiguration示例

public void Run()
{
   var builder = new ConfigurationBuilder();
   var dataProvider = new DBDataProvider();
   builder.Sources.Add(new DBConfigurationSource() { DataProvider = dataProvider, ReloadOnChange = true, Table = "config" });
   IConfigurationRoot config = builder.Build();

   Console.WriteLine(config["time"]);
   Task.Run(() =>
            {
                while (true)
                {
                    Thread.Sleep(2000);
                    dataProvider.Update("config");
                    Console.WriteLine($"读取配置时间:{config["time"]}");
                }
            });
   Thread.Sleep(20000);
}
public class DBConfigurationProvider : ConfigurationProvider
{
  private DBConfigurationSource Source { get; }
  public DBConfigurationProvider(DBConfigurationSource source)
  {
      Source = source;
  }

  public override void Load()
  {
      if (Source.ReloadOnChange)
      {
          ChangeToken.OnChange(() => Source.DataProvider.Watch(Source.Table), LoadData);
      }
      LoadData();
  }

  private void LoadData()
  {
      var data = Source.DataProvider.GetData(Source.Table);
      Load(data);
      OnReload();
  }

  public void Load(Dictionary<string, object> data)
  {
      var dic = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
      foreach (var element in data)
      {
          dic.Add(element.Key, element.Value?.ToString());
      }
      base.Data = dic;
  }
}

public class DBConfigurationSource : IConfigurationSource
{
  public DBDataProvider DataProvider { get; set; }
  public string Table { get; set; }
  public bool ReloadOnChange { get; set; }
  public bool Optional { get; set; }

  public DBConfigurationSource()
  {
  }

  public IConfigurationProvider Build(IConfigurationBuilder builder)
  {
      return new DBConfigurationProvider(this);
  }
}

public class DBDataProvider
{
  private ConcurrentDictionary<string, CancellationTokenSource> tableToken = new ConcurrentDictionary<string, CancellationTokenSource>();
  public DBDataProvider()
  {
  }

  public Dictionary<string, object> GetData(string table)
  {
      switch (table)
      {
          case "config":
              return GetConfig();
      }
      return new Dictionary<string, object>();
  }

  public void Update(string table)
  {
      Console.WriteLine($"更新数据库数据table:{table}");
      if (tableToken.TryGetValue(table, out CancellationTokenSource cts))
      {
          var oldCts = cts;
          tableToken[table] = new CancellationTokenSource();
          oldCts.Cancel();
      }
  }

  private Dictionary<string, object> GetConfig()
  {
      var valueDic = new Dictionary<string, object>();
      valueDic.TryAdd("time", DateTime.Now.ToString());
      valueDic.TryAdd("weather", "windy");
      valueDic.TryAdd("people_number:male", 100);
      valueDic.TryAdd("people_number:female", 150);
      return valueDic;
  }

  public IChangeToken Watch(string table)
  {
      var cts = tableToken.GetOrAdd(table, x => new CancellationTokenSource());
      return new CancellationChangeToken(cts.Token);
  }
}

关于.Net Core配置Configuration具体实现的文章就介绍至此,更多相关.Net Core Configuration内容请搜索编程宝库以前的文章,希望大家多多支持编程宝库

 依赖注入什么是依赖注入简单说,就是将对象的创建和销毁工作交给DI容器来进行,调用方只需要接收注入的对象实例即可。 微软官方文档-DI依赖注入有 ...