preface
Last article We demonstrated adding Etcd data source for Configuration, and learned that it is very simple to extend the custom data source for Configuration. The core is to read the data from the data source into the specified dictionary according to certain rules, which benefits from the rationality and convenience of Microsoft design. In this article, we will explore Configuration source code together to understand how Configuration works.
ConfigurationBuilder
I believe that the students who use. Net Core or have seen the source code of. Net Core are very clear. Net Core uses a lot of Builder mode. Many core operations are used in Builder mode. Microsoft uses many design modes that are not used in the traditional. Net framework in. Net Core, which also makes. Net Core more convenient to use and code more reasonable. Configuration as the core function of. Net Core is no exception.
In fact, there is no Configuration class. This is just a synonym for Configuration module. Its core is the IConfiguration interface, which is built by IConfiguration builder. We find IConfigurationBuilder source code It is roughly defined as follows
public interface IConfigurationBuilder { IDictionary<string, object> Properties { get; } IList<IConfigurationSource> Sources { get; } IConfigurationBuilder Add(IConfigurationSource source); IConfigurationRoot Build(); }
The Add method we used in the last article is to Add the ConfigurationSource data source for the ConfigurationBuilder. The added data source is stored in the Sources property. When we want to use IConfiguration, we can get IConfiguration instance through the method of Build. IConfigurationRoot interface is inherited from IConfiguration interface. We will explore this interface later.
We found the default implementation class of IConfigurationBuilder ConfigurationBuilder The approximate code is as follows
public class ConfigurationBuilder : IConfigurationBuilder { /// <summary> ///The added data source is stored here /// </summary> public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>(); public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>(); /// <summary> ///Add IConfigurationSource data source /// </summary> /// <returns></returns> public IConfigurationBuilder Add(IConfigurationSource source) { if (source == null) { throw new ArgumentNullException(nameof(source)); } Sources.Add(source); return this; } public IConfigurationRoot Build() { //Get IConfigurationProvider in all IConfigurationSource added var providers = new List<IConfigurationProvider>(); foreach (var source in Sources) { var provider = source.Build(this); providers.Add(provider); } //Using providers to instantiate ConfigurationRoot return new ConfigurationRoot(providers); } }
The definition of this class is very simple. I believe you can understand it. In fact, the whole workflow of IConfigurationBuilder is very simple: add IConfigurationSource to Sources, and then build IConfigurationRoot through the Provider in Sources.
Configuration
We have learned from the above that the implementation class built by ConfigurationBuilder is not a direct implementation of IConfiguration, but another interface IConfigurationRoot
ConfigurationRoot
We can know from the source code that IConfigurationRoot is inherited from IConfiguration. The specific definition relationship is as follows
public interface IConfigurationRoot : IConfiguration { /// <summary> ///Force refresh data /// </summary> /// <returns></returns> void Reload(); IEnumerable<IConfigurationProvider> Providers { get; } } public interface IConfiguration { string this[string key] { get; set; } /// <summary> ///Get the specified name child data node /// </summary> /// <returns></returns> IConfigurationSection GetSection(string key); /// <summary> ///Get all child data nodes /// </summary> /// <returns></returns> IEnumerable<IConfigurationSection> GetChildren(); /// <summary> ///Get IChangeToken to notify external users when there is data change in the data source /// </summary> /// <returns></returns> IChangeToken GetReloadToken(); }
Next let's look at the IConfigurationRoot implementation class ConfigurationRoot The code has been deleted
public class ConfigurationRoot : IConfigurationRoot, IDisposable { private readonly IList<IIConfigurationProvider> _providers; private readonly IList<IDisposable> _changeTokenRegistrations; private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken(); public ConfigurationRoot(IList<IConfigurationProvider> providers) { _providers = providers; _changeTokenRegistrations = new List<IDisposable>(providers.Count); //Call the Load method of ConfigurationProvider in a convenient way to Load the data into the dictionary of each ConfigurationProvider foreach (var p in providers) { p.Load(); //Listen to the ReloadToken implementation of each ConfigurationProvider. If the data source changes, refresh the Token to notify the external of the change _changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged())); } } //// <summary> ///Read or set configuration related information /// </summary> public string this[string key] { get { //Through this, we can know that the order of reading depends on the order of registering the Source, which is the way that the latecomer ranks first //The registered ones will be read first. If they are read, they will return directly for (var i = _providers.Count - 1; i >= 0; i--) { var provider = _providers[i]; if (provider.TryGet(key, out var value)) { return value; } } return null; } set { //The setting here is just to put the value in memory, and will not be persisted to the relevant data source foreach (var provider in _providers) { provider.Set(key, value); } } } public IEnumerable<IConfigurationSection> GetChildren() => this.GetChildrenImplementation(null); public IChangeToken GetReloadToken() => _changeToken; public IConfigurationSection GetSection(string key) => new ConfigurationSection(this, key); //// <summary> ///Calling this method manually can also achieve the effect of forced refresh /// </summary> public void Reload() { foreach (var provider in _providers) { provider.Load(); } RaiseChanged(); } //// <summary> ///It is highly recommended that students who are not familiar with Interlocked study the specific usage of Interlocked /// </summary> private void RaiseChanged() { var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken()); previousToken.OnReload(); } }
The above shows the core implementation of ConfigurationRoot. In fact, there are two main points
- The way of reading is actually to match the data in each registered provider circularly, which is the pattern of later generations. The registered one with the same key will be read first and then directly returned
- Only when the ConfigurationRoot is constructed can the data be loaded into memory, and a listening callback is set for each registered provider
ConfigurationSection
In fact, through the above code, we will have a question. To get the data of child nodes, we return another interface type iconfiguration section. Let's see the specific definition
public interface IConfigurationSection : IConfiguration { string Key { get; } string Path { get; } string Value { get; set; } }
This interface also inherits IConfiguration, which is strange that there is only one set of Configuration IConfiguration. Why distinguish IConfigurationRoot and IConfiguration section? In fact, it's not hard to understand that Configuration can carry many different Configuration sources at the same time. IConfigurationRoot is the root node that carries all Configuration information, and Configuration is a hierarchical structure. The child nodes acquired in the root Configuration are another system that can carry a set of related configurations, so IConfiguration section is used alone to For example, we have the following json data format
{ "OrderId":"202005202220", "Address":"Mars in the solar system of the Milky way", "Total":666.66, "Products":[ { "Id":1, "Name":"Paguma larvata", "Price":66.6, "Detail":{ "Color":"brown", "Weight":"1000g" } }, { "Id":2, "Name":"bat", "Price":55.5, "Detail":{ "Color":"black", "Weight":"200g" } } ] }
We know that json is a structured storage structure. Its storage elements are divided into three types: simple type, object type and collection type. But the dictionary is a KV structure, and there is no structural relationship. This is how to configure the system in. Net Corez. For example, this is the structure of the above information stored in the dictionary
Key | Value |
OrderId | 202005202220 |
Address | Mars in the solar system of the Milky way |
Products:0:Id | 1 |
Products:0:Name | Paguma larvata |
Products:0:Detail:Color | brown |
Products:1:Id | 2 |
Products:1:Name | bat |
Products:1:Detail:Weight | 200g |
IConfigurationSection productSection = configuration.GetSection("Products:0")
Analogy here, IConfigurationRoot stores all the data of the order. The obtained sub node IConfigurationSection represents the information of the first product in the order. This product is also a complete data system that describes the product information, so it can distinguish the Configuration structure more clearly. Let's take a look at the Configuration section General realization
public class ConfigurationSection : IConfigurationSection { private readonly IConfigurationRoot _root; private readonly string _path; private string _key; public ConfigurationSection(IConfigurationRoot root, string path) { _root = root; _path = path; } public string Path => _path; public string Key { get { return _key; } } public string Value { get { return _root[Path]; } set { _root[Path] = value; } } public string this[string key] { get { //Getting the data under the current Section is actually a combination of Path and Key return _root[ConfigurationPath.Combine(Path, key)]; } set { _root[ConfigurationPath.Combine(Path, key)] = value; } } //Get the identity Key of a sub node under the current node, which is also a combination of the current Path and the sub node public IConfigurationSection GetSection(string key) => _root.GetSection(ConfigurationPath.Combine(Path, key)); //To get all the child nodes under the current node is to get all the keys containing the current Path string in the dictionary public IEnumerable<IConfigurationSection> GetChildren() => _root.GetChildrenImplementation(Path); public IChangeToken GetReloadToken() => _root.GetReloadToken(); }
Here we can see that since there is a Key that can get the corresponding Value in the dictionary, why do we need Path? Through the code in ConfigurationRoot, we can know that the initial Value of Path is actually to get the Key of ConfigurationSection, which is to say how to get the Path of current iconfiguration section. such as
//The Path of the current productSection is Products:0 IConfigurationSection productSection = configuration.GetSection("Products:0"); //The Path of the current productDetailSection is Products:0:Detail IConfigurationSection productDetailSection = productSection.GetSection("Detail"); //The full path to pColor is Products:0:Detail:Color string pColor = productDetailSection["Color"];
And get all the sub nodes of the Section
GetChildrenImplementation from Extension method of IConfigurationRoot
internal static class InternalConfigurationRootExtensions { //// <summary> ///In fact, get all the values of the given Path contained in the Key in the data source dictionary /// </summary> internal static IEnumerable<IConfigurationSection> GetChildrenImplementation(this IConfigurationRoot root, string path) { return root.Providers .Aggregate(Enumerable.Empty<string>(), (seed, source) => source.GetChildKeys(seed, path)) .Distinct(StringComparer.OrdinalIgnoreCase) .Select(key => root.GetSection(path == null ? key : ConfigurationPath.Combine(path, key))); } }
I believe that when we talk about this, we have a certain understanding of the Configuration section or the overall idea of Configuration, and there are many detailed designs. However, the overall implementation idea is relatively clear. Another important extension method for Configuration is to bind the Configuration to the specific POCO extension method, which is hosted in ConfigurationBinder extension class , because the implementation is complex and not the focus of this article, interested students can refer to it by themselves, so we will not explore it here.
summary
In fact, we can summarize Configuration configuration into two core abstract interfaces, iconfigurationbuilder and iconfiguration. The overall structure relationship can be roughly expressed as follows

All of the above explanations are my conclusions through practice and reading the source code. There may be some deviations or misunderstandings in understanding, but I still want to share my understanding with you and hope you can forgive me a lot. If you have different opinions or deeper understanding, you can leave more comments in the comment area.
