.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: publicstring 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”命名,毫無疑問,這顆樹將不能使用字典來表示,因為它不能保證所有的節點都具有不同的路徑。
二、按照配置樹的要求對XML結構稍作轉換
之所以XML不能像JSON格式那樣可以以一種很自然的形式表示集合或者陣列,是因為後者對這兩種資料型別提供了明確的定義方式(採用中括號定義),但是XML只有子元素的概念,我們不能確定它的子元素是否是一個集合。如果做這樣一個假設:如果同一個XML元素下的所有子元素都具有相同的名稱,那麼我們可以將其視為集合。根據這麼一個假設,我們對XmlConfigurationSource略加改造就可以解決XML難以表示集合資料結構的問題。
我們通過派生XmlConfigurationSource建立一個新的ConfigurationSource型別,姑且將其命名為ExtendedXmlConfigurationSource。XmlConfigurationSource提供的ConfigurationProvdier型別為ExtendedXmlConfigurationProvider,它派生於XmlConfigurationProvider。在重寫的Load方法中,ExtendedXmlConfigurationProvider通過對原始的XML結構進行相應的改動,從而讓原本不合法的XML(XML元素具有相同的名稱)可以轉換成一個針對集合的配置字典 。下圖展示了XML結構轉換採用的規則和步驟。
如上圖所示,針對集合對原始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: }