重新整理 .net core 實踐篇————配置系統之盟約[五]

不問前世發表於2021-05-31

前言

在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"]);
}

雖然不提倡,但是可以這麼幹。

下一節,配置檔案之軍令(命令列)

以上只是個人整理,如有錯誤,望請指出,謝謝。

相關文章