開發 LINQ2Douban 專案時,發現 XmlSerializer 在處理 Douban API 要求的 XML Entry 格式時遭遇四大痛點:
IList、IDictionary)<category term="...#%Category%"> 中僅尾部變動)解決方案:XML 模板 + 反射 = 完全分離結構與資料。模板中用 %% 標記替換點,執行時透過反射取值替換,實現極致彈性。
| 需求 | XmlSerializer | XML 模板替換 |
|---|---|---|
| 泛型集合序列化 | ❌ 不支援 | ✅ 支援 IEnumerable 所有實作 |
| 靈活層級替換 | ❌ 僅巢狀 | ✅ 任意層級、跨作用域 |
| 多格式支援 | ❌ 需修改類別/屬性 | ✅ 同物件套用不同模板 |
| 格式變更維護 | ❌ 改代碼重編譯 | ✅ 僅改模板檔案 |
<channel>
<title>%Title%</title>
<link>%Link%</link>
<category domain="%Category.Domain%">%Category.Name%</category> <!-- 子物件屬性存取 -->
<noReplacement>不需要替換</noReplacement> <!-- 忽略標記 -->
<skipHours>
<hour>%SkipHours%</hour> <!-- 集合展開 -->
</skipHours>
<as:scope xmlns:as="https://xml.allsharing.com/" name="Items" type="AllSharing.Xml.Rss.RssItem">
<item> <!-- Scope 定義:針對 Items 集合每個元素產生 -->
<title>%Title%</title>
<link>%Link%</link>
<description>%Description%</description>
<channelCategory>%./Category.Name%</channelCategory> <!-- ../ 存取父 Scope 屬性 -->
</item>
</as:scope>
</channel>
%Property%:當前作用域物件屬性%Parent.Child%:巢狀物件屬性鏈%Collection%:IEnumerable 集合自動展開重複節點<as:scope name="Prop" type="Type">:定義新作用域,針對集合屬性逐項產生子節點%./Prop%:存取父作用域屬性(類似相對路徑 ../)<noReplacement>:標記純靜態內容,引擎跳過處理public class RssChannel
{
public string Title { get; set; }
public string Link { get; set; }
public RssCategory Category { get; set; }
public IList<int> SkipHours { get; set; }
public IList<RssItem> Items { get; set; }
}
public class RssItem
{
public string Title { get; set; }
public string Link { get; set; }
public string Description { get; set; }
}
public class RssCategory
{
public string Domain { get; set; }
public string Name { get; set; }
}
// 解析 XML 模板,建構 Scope 樹結構
var scope = XmlTemplateScope.Compile(xmlPath, typeof(RssChannel));
遍歷 XML 節點,識別三類元素:含 %% 的屬性/元素、Scope 節點、忽略節點。建構樹狀結構對應模板巢狀關係。
// 載入模板 XDocument,根據 Scope 樹進行替換
var template = new XmlTemplate(xmlPath, scope);
string xml = template.Translate(channelObject); // 執行替換產出最終 XML
XDocument) 操作,比 XmlDocument 簡潔高效XmlTemplate 為執行緒安全,可快取重用避免重複編譯var channel = new RssChannel {
Title = "this is channel title",
Link = "https://chwkai.cnblogs.com/",
Category = new RssCategory { Domain = "https://chwkai.cnblogs.com/", Name = "this is channel category" },
SkipHours = new List<int> { 1, 2, 3 },
Items = new List<RssItem> {
new RssItem { Title = "Item1", Link = "Link1", Description = "Des1" },
new RssItem { Title = "Item2", Link = "Link2", Description = "Des2" },
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)));
Console.WriteLine(template.Translate(channel));
<channel>
<title>this is channel title</title>
<link>https://chwkai.cnblogs.com/</link>
<category domain="https://chwkai.cnblogs.com/">this is channel category</category>
<noReplacement>不需要替換</noReplacement>
<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>...</item>
<item>...</item>
</channel>
此方案將序列化邏輯外部化為聲明式模板,實現結構與資料完全解耦,是處理複雜、多變 XML 格式的終極利器。