前阵子在写LINQ2Douban的时候碰到关于XML序列化的场景。通过Douban api添加和更新数据的时候都需要Post一个xml entry,如:
添加活动(xml中用%%括起来的部分是需要填写的部分,为了精简删除了xmlns)
view sourceprint?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属性。