三、ConfigurationProvider
  为配置模型提供原始配置数据的ConfigurationProvider是对所有实现了IConfigurationProvider接口的所有类型及其对象的统称。从配置数据结构转换的角度来看,ConfigurationProvider的目的在于将配置数据从原始结构转换成物理结构,由于配置数据的物理结构体现为一个简单的二维数据字典,所以我们会发现定义在IConfigurationProvider接口中的方法大都体现为针对字典对象的相关操作。
  1: public interface IConfigurationProvider
  2: {
  3:    void Load();
  4:
  5:    bool TryGet(string key, out string value);
  6:    void Set(string key, string value);
  7:    IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath, string delimiter)
  8: }
  配置数据的加载通过调用ConfigurationProvider的Load方法来完成。我们可以调用TryGet方法获取有指定的Key所标识的配置项的值。从数据持久化的角度来讲,ConfigurationProvider基本上都是只读的,也是说ConfigurationProvider只负责从持久化资源中读取配置数据,而不负责更新保存在持久化资源的配置数据,所以它提供的Set方法设置的配置数据一般只会保存在内存中。ConfigurationProvider的GetChildKeys方法用于获取指定路径对应配置节的所有子节点的Key。
  每种不同类型的配置源都具有对应的ConfigurationProvider,它们对应的类型大都不会直接实现IConfigurationProvider接口,而是继承另一个名为ConfigurationProvider的抽象类。这个抽象类的定义其实很简单,从如下的代码片段可以看出它仅仅是对一个IDictionary<string, string>对象的封装,其Set和TryGetValue方法终操作的都是这个字典对象。它实现了Load方法并将其定义成虚方法,具体的ConfigurationProvider可以通过重写这个方法从相应的数据源中读取配置数据并对这个字典对象进行初始化。
1: public abstract class ConfigurationProvider : IConfigurationProvider
2: {
3:     protected IDictionary<string, string> Data { get; set; }
4:
5:     public IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath, string delimiter)
6:     {
7:         //省略实现
8:     }
9:
10:     public virtual void Load()
11:     {}
12:
13:     public void Set(string key, string value)
14:     {
15:         this.Data[key] = value;
16:     }
17:
18:     public bool TryGet(string key, out string value)
19:     {
20:         return this.Data.TryGetValue(key, out value);
21:     }
22:     //其他成员
23: }
  接下来我们简单介绍一下定义在这个抽象类中GetChildKeys方法的逻辑。采用基于路径的Key让数据字典在逻辑上具有了树形化层次结构,而这个方法用于获取将指定配置节作为父节点的所有配置节的Key。指定的父配置节通过参数parentPath表示的路径来体现,另一个参数delimiter则表示路径采用的分隔符。除此之外,这个方法还具有一个字符串集合类型的参数earlierKeys,它表示预先解析出来的Key,这个列表会包含在返回的结果中。
1: class Program
2: {
3:     static void Main(string[] args)
4:     {
5:         Dictionary<string, string> source = new Dictionary<string, string>
6:         {
7:             ["A:B:C"]     = "",
8:             ["A:B:D"]     = "",
9:             ["A:E"]         = "",
10:         };
11:
12:         MemoryConfigurationProvider provider = new MemoryConfigurationProvider(source);
13:         Console.WriteLine("{0, -20}{1}", "Parent Path", "Child Keys");
14:         Console.WriteLine("------------------------------------------");
15:
16:         Print("Null", provider.GetChildKeys(new string[] { "X", "Y", "Z" }, null, ":"));
17:         Print("A", provider.GetChildKeys(new string[] { "x", "y", "z" }, "A", ":"));
18:         Print("A:B", provider.GetChildKeys(new string[] { "X", "Y", "Z }, "A:B", ":"));
19:         Print("A:B:C",provider.GetChildKeys(new string[] { "X", "Y", "Z }, "A:B:C", ":"));
20:     }
21:
22:     static void Print(string parentPath, IEnumerable<string> keys)
23:     {
24:         Console.WriteLine("{0, -20}{1}", parentPath, string.Join(", ", keys.ToArray()));
25:     }
26: }
  为了让读者朋友们能够更加直观地理解GetChildKeys方法的逻辑,我们编写了如上一段实例程序。我们创建了一个MemoryConfigurationProvider对象,由塔封装的配置数据字段包含三个元素,它们对应的Key分别是“A:B:C”、“A:B:D”和“A:E”。我们调用它的GetChildKeys方法并将表示父节点的路径分别指定为“A”、“A:B和“A:B:C”以获取相应子节点的Key。除此之外,我们采用冒号(“:”)作为分隔符,并将earlierKeys指定为包含“X”、“Y”和“Z”三个元素的数组。这段程序执行之后会在控制台上产生如下的输出结果,我们从中可以看出一个细节,返回的结构并没有将重复的Key剔除。
  1: Parent Path         Child Keys
  2: ------------------------------------------
  3: Null                X, Y, Z
  4: A                   B, B, E, X, Y, Z:
  5: A:B                 C, D, X, Y, Z
  6: A:B:C               X, Y, Z
  四、ConfigurationBuilder
  ConfigurationBuilder泛指所有实现了IConfigurationBuilder接口的类型及其对象,它在配置模型中的作用是利用注册的ConfigurationProvider提取转换成数据字典的配置数据并创建对应的Configuration对象,具体来说创建的是一个体现配置树的ConfigurationRoot对象。注册到ConfigurationBuilder上的ConfigurationProvider体现为IConfigurationBuilder接口的Providers属性,我们可以调用Add方法将ConfigurationProvider添加到这个集合中。
  1: public interface IConfigurationBuilder
  2: {
  3:     IEnumerable<IConfigurationProvider>     Providers { get; }
  4:     Dictionary<string, object>         Properties { get; }
  5:
  6:     IConfigurationBuilder Add(IConfigurationProvider provider);
  7:     IConfigurationRoot Build();
  8: }
  除此之外,IConfigurationBuilder还具有一个字典类型的只读属性Properties,我们可以将任意自定义的属性附加当一个ConfigurationBuilder对象上,并通过对应的Key得到这些属性值。ConfigurationRoot的创建终通过Build方法完成。
  原生的配置模型中提供了一个实现IConfigurationBuilder接口的类型,那是在我们之前演示的实例中多次使用的ConfigurationBuilder类,配置模型默认的配置生成机制体现在它实现的Build方法中。具体来说,实现在ConfigurationBuilder类中的Build方法返回对象的真实类型为ConfigurationRoot,该对象通过一个类型为ConfigurationSection对象表示非根配置节。右图所示的UML展示了配置模型中以Configuration、ConfigurationProvider和ConfigurationBuilder为核心的相关接口/类型以及它们之前的关系。

  ConfigurationRoot和ConfigurationSection这个两个类型的定义体现配置模型默认采用怎样的机制读取配置数据,这是我们本节论述的重点内容。虽然配置模型终提供的配置数据通过Configuration对象来体现,但是不论ConfigurationRoot还是ConfigurationSection对象,它们自身本没有封装任何的形式的配置数据,所有针对它们的数据读写操作终都会转移到相应的ConfigurationProvider上。由于Configuration对象仅仅体现为ConfigurationProvider的代理,所以由同一个ConfigurationBuilder创建的所有ConfigurationRoot对象都是等效的,下面的代码片段体现了这样的等效性。
  1: IConfigurationBuilder builder = new ConfigurationBuilder().Add(new MemoryConfigurationProvider());
  2:
  3: IConfiguration config1 = builder.Build();
  4: IConfiguration config2 = builder.Build();
  5:
  6: config1["Foobar"] = "ABC";
  7: Debug.Assert(config2["Foobar"] == "ABC");
  xxx组成配置树的ConfigurationRoot和ConfigurationSection不但自身封装和配置数据,配置节父子关系的维护也并不直接通过对象之间的引用关系来维系。如右图所示,对于一个表示配置树中某个非根配置节的ConfigurationSection对象来说,它仅仅保留着对根节点的引用,后者是一个类型为ConfigurationRoot的对象。当我们调用ConfigurationSection方法获取或者设置配置数据的时候,它会直接将调用请求转发给表示配置树根的ConfigurationRoot对象。

  具体来说,当我们试图通过某个ConfigurationSection对象得到对应配置节点的值时,该对象会将配置数据的读取请求转发给它所引用的表示数值树根的ConfigurationRoot对象,同时将自身的路径一并传递给后者。ConfigurationRoot终利用ConfigurationProvider根据指定的路径得到对应配置项的值,下图揭示了这样的流程。

  在对实现在ConfigurationRoot和ConfigurationSection这两个类中针对配置的读写机制有了大概的了解之后,我们从代码实现的角度来进一步地来认识这两个类型,在这之前我们需要先来认识一个名为ConfigurationPath的工具类。顾名思义,ConfigurationPath帮助我们实现针对配置树路径相关的计算,其中Combine方法将多个片段合并成一个完整的路径,GetSectionKey方法会根据指定的路径得到对应的Key,而GetParentPath则根据指定的路径得到上一级的路径。
  1: public static class ConfigurationPath
  2: {
  3:     public static string Combine(params string[] pathSegements) ;
  4:     public static string Combine(IEnumerable<string> pathSegements) ;
  5:     public static string GetSectionKey(string path) ;
  6:     public static string GetParentPath(string path) ;
  7: }
  ConfigurationRoot
  ConfigurationRoot真实的实现逻辑基本上体现在如下所示的代码片段中。一个ConfigurationRoot对象维护着一组提供原始配置数据的ConfigurationProvider对象,每一次对配置数据的读写操作终都会转移到它们头上。当调用它的索引指定相应的Key获取对应配置项的值时,我们会将这组ConfigurationProvider对象进行逆向排序,并将指定的Key作为参数依次调用每个ConfigurationProvider的TryGet方法直到该方法返回True,并将这个方法返回的值为索引终的返回值。当我们利用索引对指定配置项的值进行设置的时候,实际上会调用每个ConfigurationProvider的Set方法。
1: public class ConfigurationRoot: IConfigurationRoot
2: {
3:     private IList<IConfigurationProvider> providers;
4:
5:     public ConfigurationRoot(IList<IConfigurationProvider> providers)
6:     {
7:         this.providers = providers;
8:         providers.ForEach(provider => provider.Load());
9:     }
10:
11:     public string this[string key]
12:     {
13:         get
14:         {
15:             string value = null;
16:             return providers.Reverse().Any(p => p.TryGet(key, out value))
17:                 ? value: null;
18:         }
19:         set
20:         {
21:             providers.ForEach(provider => provider.Set(key, value));
22:         }
23:     }
24:
25:     public IEnumerable<IConfigurationSection> GetChildren()=> this.GetChildren(null);
26:
27:     public IConfigurationSection GetSection(string key) => new ConfigurationSection(this, key);
28:
29:     public void Reload() => providers.ForEach(provider => provider.Load());
30:
31:     internal IEnumerable<IConfigurationSection> GetChildrenCore(string path)
32:     {
33:         return providers.Aggregate(Enumerable.Empty<string>(),
34:             (seed, source) => source.GetChildKeys(seed, path, ":"))
35:             .Distinct()
36:             .Select(key => GetSection(ConfigurationPath.Combine(path, key)));
37:     }
38:     //其它成员
39: }
  ConfigurationRoot实现在索引中读取配置的逻辑体现了配置模型一个重要的特性,那是如果某个配置项的数据具有多个来源,那么后添加到ConfigurationBuilder中的ConfigurationProvider具有更高的优先级,我们姑且将这个特性称为ConfigurationProvider“后来居上”的原则。如果希望覆盖应用现有的某个配置,我们只需要将提供新配置的ConfigurationProvider添加到ConfigurationBuilder之上即可。
  我们定义了一个辅助方法GetChildrenCore来获取某个配置节的所有子配置节,这个指定的配置节通过作为参数的路径来表示。当这个方法执行之后,所有ConfigurationProvider的GetChildKeys方法会被调用以获取所有子配置节的Key,我们利用它们生成表示配置节的ConfigurationSection对象。在实现的GetChildren方法中,我们会调用这个方法来获取隶属于自己的所有子配置节。而另一个GetSection方法中,我们直接返回根据指定路径(对于表示根配置节来说,参数key表示配置节的路径)创建的ConfigurationSection对象。
  ConfigurationSection
  在上面关于用于模拟ConfigurationRoot类型定义的代码中我们知道终表示非根配置节的ConfigurationSection对象是根据它的路径和作为根配置节的ConfigurationRoot对象创建的。ConfigurationRoot将配置的读写操作递交给相应的ConfigurationProvider来完成,而ConfigurationSection则将委托自己的根配置节来完成读写配置的操作,这样的策略体现在如下所示的代码中。
1: public class ConfigurationSection: IConfigurationSection
2: {
3:     private ConfigurationRoot root;
4:     private string key;
5:
6:     public string Key
7:     {
8:         get { return key ?? (key = ConfigurationPath.GetSectionKey(this.Path)); }
9:     }
10:     public string Path { get; private set; }
11:     public string Value { get;  set; }
12:     public string this[string key]
13:     {
14:         get
15:         {
16:             return root[this.Path];
17:         }
18:
19:         set
20:         {
21:             root[this.Path] = Value;
22:         }
23:     }
24:
25:     public ConfigurationSection(ConfigurationRoot root, string path)
26:     {
27:         this.root = root;
28:         this.Path = path;
29:     }
30:
31:     public IConfigurationSection GetSection(string key) => root.GetSection(ConfigurationPath.Combine(this.Path, key));
32:     public IEnumerable<IConfigurationSection> GetChildren() => root.GetChildren(this.Path);
33:     //其它成员
34: }
  [1] ForEach是为了让代码尽量精练而为类型IEnumerable<T>定义的扩展方法,在后续章节中我们会经常使用到它。