Code4Fun: 通过XML模板系统实现对象的灵活序列化
通过替换事先定义的模板来生成XML文档的方法叫做“XML模板系统”,这在PHP的开发中有广泛的应用,如PHP Smarty,在.Net上有NVelocity。作为完整的模板系统,他们的功能更加强大和全面。但是上述模板系统因为插入占位标记,破坏了XML文档的结构,模板变成了普通文本,并且在模板中引入了几乎与编程语言相等循环控制,相对复杂。本文给出的XmlTemplate引入了规范XML模板、变量域、集合属性自循环和访问嵌套属性的方法,可以实现模板系统大多数的功能,但是却非常简单,根本无需记忆,你还可以通过扩展XmlTemplate来控制生成文本的格式。通过XmlTemplate,你可以更简单更灵活的生成XML文档,这在生成RSS, Atom等文档,以及开发WebAPI的client的时候都是非常方便的。
前阵子在写LINQ2Douban的时候碰到关于XML序列化的场景。通过Douban api添加和更新数据的时候都需要Post一个xml entry,如:
添加活动(xml中用%%括起来的部分是需要填写的部分,为了精简删除了xmlns)
<?xml version="1.0" encoding="UTF-8"?> <entry> <title>%title%</title> <category scheme="http://www.douban.com/2007#kind" term="http://www.douban.com/2007#%Category%"/> <content>%Content%</content> <db:attribute name="invite_only">%IsInviteOnly%</db:attribute> <db:attribute name="can_invite">%CanInvite%</db:attribute> <gd:when endTime="%Duration.End%" startTime="%Duration.Start%"/> <gd:where valueString="%Where%"/> </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属性。
<channel> <title>%Title%</title> <link>%Link%</link> <category domain="%Category.Domain%">%Category.Name%</category> <noRelacement>不需要替换</noRelacement> <skipHours> <hour>%SkinHours%</hour> </skipHours> <as:scope xmlns:as="http://xml.allsharing.com/" name="Items" type="AllSharing.Xml.Rss.RssItem"> <item> <title>%Title%</title> <link>%Link%</link> <description>%Description%</description> <channelCategory>%./Category.Name%</channelCategory> </item> </as:scope> </channel>
相关class定义:
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模板替换最终要是要回归到用文本替换,替换掉所有的%%,但由于我们要处理的模板包括变量域、子属性访问、循环的信息,所以这不是仅仅的Regex.Replace或String.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树的方法:
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的方法:
var template = new XmlTemplate(xmlPath, scope); Console.WriteLine(template.Translate(entityObj));
template是线程安全的,根据需要你可以Cache起来,不用每次都生成Scope树,这样或许会减少部分性能消耗(未测试)
完整的代码如下(包含在Demo中):
var channel = new RssChannel(); channel.Title = "this is channel title"; channel.Link = "http://chwkai.cnblogs.com/"; channel.Description = "this is channel description"; channel.Category = new RssCategory(); channel.Category.Domain = "http://chwkai.cnblogs.com/"; channel.Category.Name = "this is channel category"; channel.SkipHours.Add(1); channel.SkipHours.Add(2); channel.SkipHours.Add(3); channel.Items.Add(new RssItem { Title="Item1", Link="Link1", Description="Des1" }); channel.Items.Add(new RssItem { Title = "Item2", Link = "Link2", Description = "Des2" }); channel.Items.Add(new RssItem { Title = "Item3", Link = "Link3", Description = "Des3" }); var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "rss.xml"); var template = new XmlTemplate( path, XmlTemplateScope.Compile(path, typeof(RssChannel))); template.Translate(channel).Dump();
生成XML如下:
<channel> <title>this is channel title</title> <link>http://chwkai.cnblogs.com/</link> <category domain="http://chwkai.cnblogs.com/">this is channel category</cate gory> <noRelacement>不需要替换</noRelacement> <skipHours> <hour>1</hour> <hour>2</hour> <hour>3</hour> </skipHours> <item> <title>Item1</title> <link>Link1</link> <description>Des1</description> <channelCategory>this is channel category</channelCategory> </item> <item> <title>Item2</title> <link>Link2</link> <description>Des2</description> <channelCategory>this is channel category</channelCategory> </item> <item> <title>Item3</title> <link>Link3</link> <description>Des3</description> <channelCategory>this is channel category</channelCategory> </item> </channel>
链接
Source Code for XmlTemplate: https://files.cnblogs.com/chwkai/Template.rar
All the posts in this blog are provided "AS IS" with no warranties, and confer no rights. Except where otherwise noted, content on this site is licensed under a Creative Commons Attribution 2.5 China Mainland License.