前陣子在寫LINQ2Douban的時候碰到關於XML序列化的場景。通過Douban api添加和更新數據的時候都需要Post一個xml entry,如:
添加活動(xml中用%%括起來的部分是需要填寫的部分,為了精簡刪除了xmlns)
01 <?xml version="1.0" encoding="UTF-8"?>
02 <entry>
03 <title>%title%</title>
04 <category scheme="http://www.douban.com/2007#kind" term="http://www.douban.com/2007#%Category%"/>
05 <content>%Content%</content>
06
07 <db:attribute name="invite_only">%IsInviteOnly%</db:attribute>
08 <db:attribute name="can_invite">%CanInvite%</db:attribute>
09 <gd:when endTime="%Duration.End%" startTime="%Duration.Start%"/>
10 <gd:where valueString="%Where%"/>
11 </entry>
一下子能想到的方法有兩個——通過XmlSerializer或在entity class上實現IXmlSerializable接口來實現,但很快發現有幾個問題沒法解決:
1、XmlSerializer不支持泛型集合的序列化,而我在定義entity class時用了不少IList和IDictionary,如db:attribute我就定義成IDictionary
2、XmlSerializer只能生成完整的element和attribute,像上面那段xml裡<category>節點term屬性裡變化的只有後面%Category%部分,這就沒法生成了
3、存在添加和更新的post內容不一樣的情況,這就意味著同一個entity class,存在多種序列化方案
4、douban的xml entry格式可能會更改,而我不希望因此而更改代碼
想來想去,最好的方法是通過XML模板+反射(簡稱XMl模板替換)來生成了,就像上面的xml etnry裡面%%括起來的部分,替換掉就可以了,這樣可以解決上述的四個問題。除了提供和XMLSerializer功能相同的序列化之外,XML模板替換還要滿足下面這些要求:
1、可以序列化實現IEnumerable的集合,這是最常用的集合,當然大多數的泛型集合也是應用了IEnumerable的
2、提供更靈活的替換。XmlSerializer實現的序列化順序是”A(B(c)B)A”,對於子對象的序列化只能是嵌套的模式,而XML模板替換可以實現任何層次的替換。
3、為每種類型的對象提供通用的序列化方式,不需要任何Attribute定義,不需要修改對象的定義。對於給定的object和XML模板,通過發射獲取屬性值進行XML替換後生成XML內容;對於相同的object,提供不同的XML模板就能生成不同的XML。
4、通過修改XML模板即可修改序列化結果
下面給出一個修改過的RSS的XML模板,這次Code4Fun的目的是在最後實現這個模板的替換,並且完成一個能夠實現上述功能的Helper class。
特別的地方:
1、<category>節點:通過”.”可訪問子對象的屬性,如果你希望獲取Domain的長度可以寫成”%Category.Domain.Length%”
2、<noReplacement>節點:該節點不包含任何替換信息,當進行替換處理時應當忽略
3、<skipHours>節點:SkipHours是一個List<int>集合,我們希望能夠根據SkipHours的值,展開多個<hour>節點
4、<as:scope>節點:<scope>是模板定義,聲明<scope>節點內包含的子節點在 Channel.Items對象的作用域中,所有%%(不包括%./Category.Name%)的屬性都是對Items對象的屬性訪問。由於此處 Items對象是List<RssItem>集合,所以將循環生成多個<item>。Scope的含義類似於程序域,支持多個 scope的嵌套,Scope定義不會出現在最後生成的xml中。
5、<channelCategory>節點:<channelCategory>節點在Items的作用域中,但我們可以通過”./”訪問外部scope的屬性,類似dos文件路徑,如果要訪問上上級scope,則是”././”。%./Category.Name%表示訪問Channel對象的Category屬性的Name屬性。
01 <channel>
02 <title>%Title%</title>
03 <link>%Link%</link>
04 <category domain="%Category.Domain%">%Category.Name%</category>
05 <noRelacement>不需要替換</noRelacement>
06 <skipHours>
07 <hour>%SkinHours%</hour>
08 </skipHours>
09 <as:scope xmlns:as="http://xml.allsharing.com/" name="Items" type="AllSharing.Xml.Rss.RssItem">
10 <item>
11 <title>%Title%</title>
12 <link>%Link%</link>
13 <description>%Description%</description>
14 <channelCategory>%./Category.Name%</channelCategory>
15 </item>
16 </as:scope>
17 </channel>
相關class定義:
01 public class RssChannel
02 {
03 public string Title { get; set; }
04 public string Link { get; set; }
05 public RssCategory Category { get; set; }
06 public IList<int> SkinHours { get; set; }
07 public IList<RssItem> Items { get; set; }
08 }
09
10 public class RssItem
11 {
12 public string Title { get; set; }
13 public string Link { get; set; }
14 }
15
16 public class RssCategory
17 {
18 public string Domain { get; set; }
19 public string Name { get; set; }
20 }
下面將一步步討論如何實現XML模板的替換,會給出部分代碼或偽代碼,完整的代碼在文章最後會給出下載。
一、分析XML模板
XML模板替換最終要是要回歸到用正則表達式替換掉所有的%%,但由於我們要處理的模板包括域、子屬性訪問、循環的信息,所以這不是僅僅的 Regex.Replace就可以搞定的。分析XML模板,就是要遍歷XML模板生成一個Scope樹,上面的XML模板可以生成下面的Scope樹:
XML模板中包含了的三種我們需要處理的元素
1、包含%%的XML Attribute
2、包含%%的XML Element以及Element的子節點
3、Scope節點
從上面的Scope樹可以看出,像<noReplace>這樣不需要替換的XML element或XML attribute被當作常量,沒有包括在Scope樹中。
在Helper Class中分析Scope樹的方法:
1 var scope = XmlTemplateScope.Compile(xmlPath, entityType)
xmlPath是模板文件的路徑,entityType是Scope樹用於分析的對象類型
二、對給定的object,生成XML
這裡用了LINQ2XML,首先用XDocument.Load(xmlPath)的到XML模板文件的XDocument,然後根據Scope樹對XDocument上的節點進行屬性值替換、節點Value替換、增加節點(Repeat的節點)。幸運的是XDocument比 XmlDocument方便太多了,實現起來非常快。
在Helper Class中生成XML的方法:
1 var template = new XmlTemplate(xmlPath, scope);
2 Console.WriteLine(template.Translate(entityObj));
template是線程安全的,根據需要你可以Cache起來,不用每次都生成Scope樹,這樣或許會減少部分性能消耗(未測試)
完整的代碼如下(包含在Demo中):
01 var channel = new RssChannel();
02 channel.Title = "this is channel title";
03 channel.Link = "http://chwkai.cnblogs.com/";
04 channel.Description = "this is channel description";
05 channel.Category = new RssCategory();
06 channel.Category.Domain = "http://chwkai.cnblogs.com/";
07 channel.Category.Name = "this is channel category";
08 channel.SkipHours.Add(1);
09 channel.SkipHours.Add(2);
10 channel.SkipHours.Add(3);
11 channel.Items.Add(new RssItem { Title="Item1", Link="Link1", Description="Des1" });
12 channel.Items.Add(new RssItem { Title = "Item2", Link = "Link2", Description = "Des2" });
13 channel.Items.Add(new RssItem { Title = "Item3", Link = "Link3", Description = "Des3" });
14
15 var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "rss.xml");
16 var template = new XmlTemplate(
17 path, XmlTemplateScope.Compile(path, typeof(RssChannel)));
18 template.Translate(channel).Dump();
生成XML如下:
01 <channel>
02 <title>this is channel title</title>
03 <link>http://chwkai.cnblogs.com/</link>
04 <category domain="http://chwkai.cnblogs.com/">this is channel category</cate
05 gory>
06 <noRelacement>不需要替換</noRelacement>
07 <skipHours>
08 <hour>1</hour>
09 <hour>2</hour>
10 <hour>3</hour>
11 </skipHours>
12 <item>
13 <title>Item1</title>
14 <link>Link1</link>
15 <description>Des1</description>
16 <channelCategory>this is channel category</channelCategory>
17 </item>
18 <item>
19 <title>Item2</title>
20 <link>Link2</link>
21 <description>Des2</description>
22 <channelCategory>this is channel category</channelCategory>
23 </item>
24 <item>
25 <title>Item3</title>
26 <link>Link3</link>
27 <description>Des3</description>
28 <channelCategory>this is channel category</channelCategory>
29 </item>
30 </channel>
總結:本文給出了另外一種XML序列化的方法,通過XmlTemplate,你可以更簡單更靈活的生成XML文檔,這在生成RSS, Atom等文檔,以及開發WebAPI的client的時候都是非常方便的。另外,本文給出的方法同樣適用於基於XML模板生成Html文件。