ASP.NET Core 3框架揭秘] 配置[8]:多样化的配置源[下篇]

配置[8]:多样化的配置源[下篇] - 图1XML也是一种常用的配置定义形式,它对数据的表达能力甚至强于JSON,几乎所有类型的数据结构都可以通过XML表示出来。当我们通过一个XML元素表示一个复杂对象的时候,对象的数据成员定义成当前XML元素的子元素。虽然XML对数据结构的表达能力总体要强于JSON,但是作为配置模型的数据来源却有自己的局限性,比如它们对集合的表现形式有点不尽如人意。

XML也是一种常用的配置定义形式,它对数据的表达能力甚至强于JSON,几乎所有类型的数据结构都可以通过XML表示出来。当我们通过一个XML元素表示一个复杂对象的时候,对象的数据成员定义成当前XML元素的子元素。虽然XML对数据结构的表达能力总体要强于JSON,但是作为配置模型的数据来源却有自己的局限性,比如它们对集合的表现形式有点不尽如人意。

一、XML在针对集合表达上的缺陷

举个简单的例子,对于一个元素类型为Profile的集合,我们可以采用具有如下结构的XML来表现。

  1. <Profiles>
  2. <Profile Gender="Male" Age="18">
  3. <ContactInfo EmailAddress ="foo@outlook.com" PhoneNo="123"/>
  4. </Profile>
  5. <Profile Gender="Male" Age="25">
  6. <ContactInfo EmailAddress ="bar@outlook.com" PhoneNo="456"/>
  7. </Profile>
  8. <Profile Gender="Male" Age="36">
  9. <ContactInfo EmailAddress ="baz@outlook.com" PhoneNo="789"/>
  10. </Profile>
  11. </Profiles>

但是这段XML却不能正确地转换成配置字典,原因很简单,因为字典的Key必须是唯一的,这必然要求最终构成配置树的每个节点必须具有不同的路径。上面这段XML很明显不满足这个基本的要求,因为表示一个Profile对象的三个XML元素(<Profile>…</Profile>)是“同质”的,对于由它们表示的三个Profile对象来说,分别表示性别、年龄、电子邮箱地址和电话号码的四个叶子节点的路径是完全一样的,所以根本无法作为配置字典的Key。通过前面针对配置绑定的介绍我们知道,如果需要通过配置字典来表示一个Profile对象的集合,我们需要按照如下的方式为每个集合元素加上相应的索引(“foo”、“bar”和“baz”)。

  1. foo:Gender
  2. foo:Age
  3. foo:ContactInfo:EmailAddress
  4. foo:ContactInfo:PhoneNo
  5.  
  6. bar:Gender
  7. bar:Age
  8. bar:ContactInfo:EmailAddress
  9. bar:ContactInfo:PhoneNo
  10.  
  11. baz:Gender
  12. baz:Age
  13. baz:ContactInfo:EmailAddress
  14. baz:ContactInfo:PhoneNo

二、通过自定义IConfigurationSource解决问题

之所以XML不能像JSON格式那样可以以一种很自然的形式表示集合或者数组,是因为后者对这两种数据类型提供了明确的定义方式(采用中括号定义),但是XML只有子元素的概念,我们不能确定它的子元素是否是一个集合。如果做这样一个假设:如果同一个XML元素下的所有子元素都具有相同的名称,那么我们可以将其视为集合。根据这么一个假设,我们对XmlConfigurationSource略加改造就可以解决XML难以表示集合数据结构的问题。

我们通过派生XmlConfigurationSource创建一个新的IConfigurationSource实现类型,姑且将其命名为ExtendedXmlConfigurationSource。XmlConfigurationSource提供的ConfigurationProvdier类型为ExtendedXmlConfigurationProvider,它派生于XmlConfigurationProvider。在重写的Load方法中,ExtendedXmlConfigurationProvider通过对原始的XML结构进行相应的改动,从而让原本不合法的XML(XML元素具有相同的名称)可以转换成一个针对集合的配置字典 。下图展示了XML结构转换采用的规则和步骤。

6-18

如上图所示,针对集合对原始XML所作的结构转换由两个步骤组成。第一步为表示集合元素的XML元素添加一个名为“append_index”的属性(Attribute),我们采用零基索引作为该属性的值。第二步会根据第一步转换的结果创建一个新的XML,同名的集合元素(比如<profile>)将会根据添加的索引值重新命名(比如<profile_index_0>)。毫无疑问,转换后的这个XML可以很好地表示一个集合对象。如下所示的是ExtendedXmlConfigurationProvider对象的定义,上述的这个转换逻辑体现在重写的Load方法中。

  1. public class ExtendedXmlConfigurationProvider : XmlConfigurationProvider
  2. {
  3. public ExtendedXmlConfigurationProvider(XmlConfigurationSource source) : base(source)
  4. {}
  5.  
  6. public override void Load(Stream stream)
  7. {
  8. //加载源文件并创建一个XmlDocument
  9. var sourceDoc = new XmlDocument();
  10. sourceDoc.Load(stream);
  11.  
  12. //添加索引
  13. AddIndexes(sourceDoc.DocumentElement);
  14.  
  15. //根据添加的索引创建一个新的XmlDocument
  16. var newDoc = new XmlDocument();
  17. var documentElement = newDoc.CreateElement(sourceDoc.DocumentElement.Name);
  18. newDoc.AppendChild(documentElement);
  19.  
  20. foreach (XmlElement element in sourceDoc.DocumentElement.ChildNodes)
  21. {
  22. Rebuild(element, documentElement, name => newDoc.CreateElement(name));
  23. }
  24.  
  25. //根据新的XmlDocument初始化配置字典
  26. using (Stream newStream = new MemoryStream())
  27. {
  28. using (XmlWriter writer = XmlWriter.Create(newStream))
  29. {
  30. newDoc.WriteTo(writer);
  31. }
  32. newStream.Position = 0;
  33. base.Load(newStream);
  34. }
  35. }
  36.  
  37. private void AddIndexes(XmlElement element)
  38. {
  39. if (element.ChildNodes.OfType<XmlElement>().Count() > 1)
  40. {
  41. if (element.ChildNodes.OfType<XmlElement>().GroupBy(it => it.Name).Count() == 1)
  42. {
  43. var index = 0;
  44. foreach (XmlElement subElement in element.ChildNodes)
  45. {
  46. subElement.SetAttribute("append_index", (index++).ToString());
  47. AddIndexes(subElement);
  48. }
  49. }
  50. }
  51. }
  52.  
  53. private void Rebuild(XmlElement source, XmlElement destParent, Func<string, XmlElement> creator)
  54. {
  55. var index = source.GetAttribute("append_index");
  56. var elementName = string.IsNullOrEmpty(index) ? source.Name : $"{source.Name}_index_{index}";
  57. var element = creator(elementName);
  58. destParent.AppendChild(element);
  59. foreach (XmlAttribute attribute in source.Attributes)
  60. {
  61. if (attribute.Name != "append_index")
  62. {
  63. element.SetAttribute(attribute.Name, attribute.Value);
  64. }
  65. }
  66.  
  67. foreach (XmlElement subElement in source.ChildNodes)
  68. {
  69. Rebuild(subElement, element, creator);
  70. }
  71. }
  72. }

为了能够将上面这个XmlConfigurationProvider对象应用到我们的程序中,我们需要为它定义相应的IConfigurationSource对象,为此我们定义了如下这个ExtendedXmlConfigurationSource类型。它直接继承自XmlConfigurationSource对象,并在重写的Build方法中提供上面这个ExtendedXmlConfigurationProvider对象。为了方便将这个ExtendedXmlConfigurationSource对象注册到IConfigurationBuilder对象上,我们也可以进一步定义如下这些扩展方法。

  1. public class ExtendedXmlConfigurationSource : XmlConfigurationSource
  2. {
  3. public override IConfigurationProvider Build(IConfigurationBuilder builder)
  4. {
  5. EnsureDefaults(builder);
  6. return new ExtendedXmlConfigurationProvider(this);
  7. }
  8. }
  9.  
  10. public static class ExtendedXmlConfigurationExtensions
  11. {
  12. public static IConfigurationBuilder AddExtendedXmlFile( this IConfigurationBuilder builder, string path)=> builder.AddExtendedXmlFile(path, false, false);
  13. public static IConfigurationBuilder AddExtendedXmlFile( this IConfigurationBuilder builder, string path, bool optional) => builder.AddExtendedXmlFile(path, optional, false);
  14. public static IConfigurationBuilder AddExtendedXmlFile(tthis IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange)
  15. {
  16. builder.Add(new ExtendedXmlConfigurationSource { Path = path, Optional = optional, ReloadOnChange = reloadOnChange });
  17. return builder;
  18. }
  19. }

配置[8]:多样化的配置源[下篇] - 图3

作者:蒋金楠微信公众账号:大内老A微博:www.weibo.com/artech如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号)。本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文:https://www.cnblogs.com/artech/p/inside-asp-net-core-05-08.html