1. 程式人生 > >.NET Core採用的全新配置系統[9]: 為什麼針對XML的支援不夠好?如何改進?

.NET Core採用的全新配置系統[9]: 為什麼針對XML的支援不夠好?如何改進?

物理檔案是我們最常用到的原始配置的載體,最佳的配置檔案格式主要由三種,它們分別是JSON、XML和INI,對應的配置源型別分別是JsonConfigurationSource、XmlConfigurationSource和IniConfigurationSource。但是對於.NET Core的配置系統來說,我們習以為常的XML反倒不是理想的配置源,至少和JSON比較起來,它具有一個先天不足的劣勢,那就是針對集合資料結構的支援不如人意。[ 本文已經同步到《ASP.NET Core框架揭祕》之中]

一、為什麼針對集合的配置難以通過優雅的XML來表示

在《配置模型設計詳解》一文中我們對配置模型的設計和實現進行了詳細介紹。在此文中我們說應用中的配置體現為一種樹形化的層次結構,所我將它稱為“配置樹”,具體的配置資料通過配置樹的“葉子節點”承載。當配置資料從不同的來源載入之後都會轉換成一個字典,我將其稱為“配置字典”。為了讓“配置字典”能夠儲存“配置樹”的所有資料和自身結構,我們需要在配置字典中儲存所有葉子節點,葉子節點的路徑和值將直接作為字典元素的Key和Value。由於字典的Key是唯一的,這就要求配置樹中的每一個節點必須具有唯一的路徑。XmlConfigurationSource/XmlConfigurationProvider不能很好地支援集合資料結構的問題就出現在這裡。

   1: public class Profile
   2: {
   3:     public Gender         Gender { get; set; }
   4:     public int            Age { get; set; }
   5:     public ContactInfo    ContactInfo { get; set; }
   6: }
   7:  
   8: public class ContactInfo
   9: {
  10:     public string EmailAddress { get; set; }
  11:     public
string PhoneNo { get; set; }
  12: }
  13:  
  14: public enum Gender
  15: {
  16:     Male,
  17:     Female
  18: }

舉個簡單的例子,假設需要採用XML來表示一個Profile物件的集合(Profile的型別具有如上所示的定義),那麼我們很自然地會採用如下的結構。

   1: <Profiles>
   2:   <Profile Gender="Male" Age="18">
   3:     <ContactInfo EmailAddress ="
[email protected]
"
PhoneNo="123"/>
   4:   </Profile>
   5:   <Profile Gender="Male" Age="25">
   6:     <ContactInfo EmailAddress ="[email protected]" PhoneNo="456"/>
   7:   </Profile>
   8:   <Profile Gender="Male" Age="40">
   9:     <ContactInfo EmailAddress ="[email protected]" PhoneNo="789"/>
  10: </Profile>

對於這段XML結構,XmlConfigurationProvider會採用“簡單粗暴”的方式將它對映為如下所示的“配置樹”。由於這棵樹直接將XML元素的名稱作為配置節點名稱,所以三個Profile物件在這棵樹中的根節點都以“Profile”命名,毫無疑問,這顆樹將不能使用字典來表示,因為它不能保證所有的節點都具有不同的路徑

image

二、按照配置樹的要求對XML結構稍作轉換

之所以XML不能像JSON格式那樣可以以一種很自然的形式表示集合或者陣列,是因為後者對這兩種資料型別提供了明確的定義方式(採用中括號定義),但是XML只有子元素的概念,我們不能確定它的子元素是否是一個集合。如果做這樣一個假設:如果同一個XML元素下的所有子元素都具有相同的名稱,那麼我們可以將其視為集合。根據這麼一個假設,我們對XmlConfigurationSource略加改造就可以解決XML難以表示集合資料結構的問題。

我們通過派生XmlConfigurationSource建立一個新的ConfigurationSource型別,姑且將其命名為ExtendedXmlConfigurationSource。XmlConfigurationSource提供的ConfigurationProvdier型別為ExtendedXmlConfigurationProvider,它派生於XmlConfigurationProvider。在重寫的Load方法中,ExtendedXmlConfigurationProvider通過對原始的XML結構進行相應的改動,從而讓原本不合法的XML(XML元素具有相同的名稱)可以轉換成一個針對集合的配置字典 。下圖展示了XML結構轉換採用的規則和步驟。

12

如上圖所示,針對集合對原始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:         XmlDocument sourceDoc = new XmlDocument();
  10:         sourceDoc.Load(stream);
  11:  
  12:         //新增索引
  13:         this.AddIndexes(sourceDoc.DocumentElement);
  14:  
  15:         //根據新增的索引建立一個新的XmlDocument
  16:         XmlDocument newDoc = new XmlDocument();
  17:         XmlElement documentElement = newDoc.CreateElement(sourceDoc.DocumentElement.Name);
  18:         newDoc.AppendChild(documentElement);
  19:  
  20:         foreach (XmlElement element in sourceDoc.DocumentElement.ChildNodes)
  21:         {
  22:             this.Rebuild(element, documentElement, 
  23:                 name => newDoc.CreateElement(name));
  24:         }
  25:  
  26:         //根據新的XmlDocument初始化配置字典
  27:         using (Stream newStream = new MemoryStream())
  28:         {
  29:             using (XmlWriter writer = XmlWriter.Create(newStream))
  30:             {
  31:                 newDoc.WriteTo(writer);
  32:             }
  33:             newStream.Position = 0;
  34:             base.Load(newStream);
  35:         }
  36:     }
  37:  
  38:     private void AddIndexes(XmlElement element)
  39:     {
  40:         if (element.ChildNodes.OfType<XmlElement>().Count() > 1)
  41:         {
  42:             if (element.ChildNodes.OfType<XmlElement>().GroupBy(it => it.Name).Count() == 1)
  43:             {
  44:                 int index = 0;
  45:                 foreach (XmlElement subElement in element.ChildNodes)
  46:                 {
  47:                     subElement.SetAttribute("append_index", (index++).ToString());
  48:                     AddIndexes(subElement);
  49:                 }
  50:             }
  51:         }
  52:     }
  53:  
  54:     private void Rebuild(XmlElement source, XmlElement destParent, Func<string, XmlElement> creator)
  55:     {
  56:         string index = source.GetAttribute("append_index");
  57:         string elementName = string.IsNullOrEmpty(index) ? source.Name : $"{source.Name}_index_{index}";
  58:         XmlElement element = creator(elementName);
  59:         destParent.AppendChild(element);
  60:         foreach (XmlAttribute attribute in source.Attributes)
  61:         {
  62:             if (attribute.Name != "append_index")
  63:             {
  64:                 element.SetAttribute(attribute.Name, attribute.Value);
  65:             }
  66:         }
  67:  
  68:         foreach (XmlElement subElement in source.ChildNodes)
  69:         {
  70:             Rebuild(subElement, element, creator);
  71:         }
  72:     }
  73: }