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树:

Type.RssChannel

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

posted @ 2010-01-02 23:54  海南K.K  阅读(1650)  评论(5编辑  收藏  举报