1. 程式人生 > 其它 >.Net Core配置Configuration原始碼研究

.Net Core配置Configuration原始碼研究

最近又研究了一下.NetCore配置選項的原始碼實現,又學習到了不少東西。這篇文章先寫一下IConfiguration的學習成果,Options的後面補上


核心類

ConfigurationBuilder:IConfigurationBuilder (構建IConfiguration)

IConfigurationSource (配置資料來源)

IConfigurationProvider (將配置源的原始結構轉為為IDictionary<string, string>)

ConfigurationRoot:IConfigurationRoot:IConfiguration (配置根節點)



構建


ConfigurationBuilder

下面是ConfigurationBuilder中的主要程式碼

可以看到ConfigurationBuilder的主要功能就是配置資料來源到集合中

在Build時依次呼叫IConfigurationSourceBuild函式,並將返回的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

ConfigurationBuilderBuild函式中,我們生成了一個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函式,這會給ProviderData賦值
  2. 讀取ProviderReloadToken,每個ProviderReload事件都會觸發ConfigurationRoot自己的ReloadTokenReload事件

至此配置的資料來源構建這塊就分析完啦!



查詢

常規的配置查詢有兩種基本方式 :索引器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);
    }
}