前阵子在写LINQDouban的时候碰到关于XML序列化的场景通过Douban api添加和更新数据的时候都需要Post一个xml entry如
添加活动(xml中用%%括起来的部分是需要填写的部分为了精简删除了xmlns)
view sourceprint? <?xml version= encoding=UTF?>
<entry>
<title>%title%</title>
<category scheme= term=;
<content>%Content%</content>
<db:attribute name=invite_only>%IsInviteOnly%</db:attribute>
<db:attribute name=can_invite>%CanInvite%</db:attribute>
<gd:when endTime=%DurationEnd% startTime=%DurationStart%/>
<gd:where valueString=%Where%/>
</entry>
一下子能想到的方法有两个——通过XmlSerializer或在entity class上实现IXmlSerializable接口来实现但很快发现有几个问题没法解决
XmlSerializer不支持泛型集合的序列化而我在定义entity class时用了不少IList和IDictionary如db:attribute我就定义成IDictionary
XmlSerializer只能生成完整的element和attribute像上面那段xml里<category>节点term属性里变化的只有后面%Category%部分这就没法生成了
存在添加和更新的post内容不一样的情况这就意味着同一个entity class存在多种序列化方案
douban的xml entry格式可能会更改而我不希望因此而更改代码
想来想去最好的方法是通过XML模板+反射(简称XMl模板替换)来生成了就像上面的xml etnry里面%%括起来的部分替换掉就可以了这样可以解决上述的四个问题除了提供和XMLSerializer功能相同的序列化之外XML模板替换还要满足下面这些要求
可以序列化实现IEnumerable的集合这是最常用的集合当然大多数的泛型集合也是应用了IEnumerable的
提供更灵活的替换XmlSerializer实现的序列化顺序是A(B(c)B)A对于子对象的序列化只能是嵌套的模式而XML模板替换可以实现任何层次的替换
为每种类型的对象提供通用的序列化方式不需要任何Attribute定义不需要修改对象的定义对于给定的object和XML模板通过发射获取属性值进行XML替换后生成XML内容对于相同的object提供不同的XML模板就能生成不同的XML
通过修改XML模板即可修改序列化结果
下面给出一个修改过的RSS的XML模板这次CodeFun的目的是在最后实现这个模板的替换并且完成一个能够实现上述功能的Helper class
特别的地方
<category>节点通过可访问子对象的属性如果你希望获取Domain的长度可以写成%CategoryDomainLength%
<noReplacement>节点该节点不包含任何替换信息当进行替换处理时应当忽略
<skipHours>节点SkipHours是一个List<int>集合我们希望能够根据SkipHours的值展开多个<hour>节点
<as:scope>节点<scope>是模板定义声明<scope>节点内包含的子节点在ChannelItems对象的作用域中所有%%(不包括%/CategoryName%)的属性都是对Items对象的属性访问由于此处Items对象是List<RssItem>集合所以将循环生成多个<item>Scope的含义类似于程序域支持多个scope的嵌套Scope定义不会出现在最后生成的xml中
<channelCategory>节点<channelCategory>节点在Items的作用域中但我们可以通过/访问外部scope的属性类似dos文件路径如果要访问上上级scope则是//%/CategoryName%表示访问Channel对象的Category属性的Name属性
view sourceprint? <channel>
<title>%Title%</title>
<link>%Link%</link>
<category domain=%CategoryDomain%>%CategoryName%</category>
<noRelacement>不需要替换</noRelacement>
<skipHours>
<hour>%SkinHours%</hour>
</skipHours>
<as:scope xmlns:as= name=Items type=AllSharingXmlRssRssItem>
<item>
<title>%Title%</title>
<link>%Link%</link>
<description>%Description%</description>
<channelCategory>%/CategoryName%</channelCategory>
</item>
</as:scope>
</channel>
view sourceprint?
相关class定义
view sourceprint? public class RssChannel
{
public string Title { get; set; }
public string Link { get; set; }
public RssCategory Category { get; set; }
public IList<int> SkinHours { get; set; }
public IList<RssItem> Items { get; set; }
}
public class RssItem
{
public string Title { get; set; }
public string Link { get; set; }
}
public class RssCategory
{
public string Domain { get; set; }
public string Name { get; set; }
}
下面将一步步讨论如何实现XML模板的替换会给出部分代码或伪代码完整的代码在文章最后会给出下载
一分析XML模板
XML模板替换最终要是要回归到用正则表达式替换掉所有的%%但由于我们要处理的模板包括域子属性访问循环的信息所以这不是仅仅的RegexReplace就可以搞定的分析XML模板就是要遍历XML模板生成一个Scope树上面的XML模板可以生成下面的Scope树
XML模板中包含了的三种我们需要处理的元素
包含%%的XML Attribute
包含%%的XML Element以及Element的子节点
Scope节点
从上面的Scope树可以看出像<noReplace>这样不需要替换的XML element或XML attribute被当作常量没有包括在Scope树中
在Helper Class中分析Scope树的方法
view sourceprint? var scope = XmlTemplateScopeCompile(xmlPath entityType)
xmlPath是模板文件的路径entityType是Scope树用于分析的对象类型
二对给定的object生成XML
这里用了LINQXML首先用XDocumentLoad(xmlPath)的到XML模板文件的XDocument然后根据Scope树对XDocument上的节点进行属性值替换节点Value替换增加节点(Repeat的节点)幸运的是XDocument比XmlDocument方便太多了实现起来非常快
在Helper Class中生成XML的方法
view sourceprint? var template = new XmlTemplate(xmlPath scope);
ConsoleWriteLine(templateTranslate(entityObj));
template是线程安全的根据需要你可以Cache起来不用每次都生成Scope树这样或许会减少部分性能消耗(未测试)
完整的代码如下(包含在Demo中)
view sourceprint? var channel = new RssChannel();
channelTitle = this is channel title;
channelLink = ;
channelDescription = this is channel description;
channelCategory = new RssCategory();
channelCategoryDomain = ;
channelCategoryName = this is channel category;
channelSkipHoursAdd();
channelSkipHoursAdd();
channelSkipHoursAdd();
channelItemsAdd(new RssItem { Title=Item Link=Link Description=Des });
channelItemsAdd(new RssItem { Title = Item Link = Link Description = Des });
channelItemsAdd(new RssItem { Title = Item Link = Link Description = Des });
var path = PathCombine(AppDomainCurrentDomainBaseDirectory rssxml);
var template = new XmlTemplate(
path XmlTemplateScopeCompile(path typeof(RssChannel)));
templateTranslate(channel)Dump();
生成XML如下
view sourceprint? <channel>
<title>this is channel title</title>
<link>;/link>
<category domain=this>>this is channel category</cate
gory>
<noRelacement>不需要替换</noRelacement>
<skipHours>
<hour></hour>
<hour></hour>
<hour></hour>
</skipHours>
<item>
<title>Item</title>
<link>Link</link>
<description>Des</description>
<channelCategory>this is channel category</channelCategory>
</item>
<item>
<title>Item</title>
<link>Link</link>
<description>Des</description>
<channelCategory>this is channel category</channelCategory>
</item>
<item>
<title>Item</title>
<link>Link</link>
<description>Des</description>
<channelCategory>this is channel category</channelCategory>
</item>
</channel>
总结本文给出了另外一种XML序列化的方法通过XmlTemplate你可以更简单更灵活的生成XML文档这在生成RSS Atom等文档以及开发WebAPI的client的时候都是非常方便的另外本文给出的方法同样适用于基于XML模板生成Html文件