(Update: June 23, 2008: I've updated and improved on this technique in this blog post)
Introduction
[xml]
<b><i>
T:System.Xml.Linq.XElement
http://www.microsoft.com/LinqToXmlTransformApplyTransformsn the source tree
T:System.Xml.Linq.XElementApplyTransformsxsl:apply-templates
Transforming a Tree
This first example renames all Paragraph nodes to para.
[c#]
XElement root = XElement.Parse(@"
<Root>
<Paragraph>This is a sentence with <b>bold</b> and <i>italic</i> text.</Paragraph>
<Paragraph>More text.</Paragraph>
</Root>");
// replace Paragraph with p
foreach (var el in root.Descendants("Paragraph"))
el.AddAnnotation(
new XElement("para",
// same idea as xsl:apply-templates
new XElement(xf + "ApplyTransforms")
)
);
XElement newRoot = XForm(root);
Console.WriteLine(newRoot);
This example produces the following output:
[xml]
<Root>
<para>This is a sentence with <b>bold</b> and <i>italic</i> text.</para>
<para>More text.</para>
</Root>
A More Complicated Transform
The following example queries the tree and calculates the average and sum of the Data elements, and adds them as new elements to the tree.
[c#]
XElement data = new XElement("Root",
new XElement("Data", 20),
new XElement("Data", 10),
new XElement("Data", 3)
);
// while adding annotations, you can query the source tree all you want,
// as the tree is not mutated while annotating.
data.AddAnnotation(
new XElement("Root",
new XElement(xf + "ApplyTransforms"),
new XElement("Average",
String.Format("{0:F4}",
data
.Elements("Data")
.Select(z => (Decimal)z)
.Average()
)
),
new XElement("Sum",
data
.Elements("Data")
.Select(z => (int)z)
.Sum()
)
)
);
Console.WriteLine("Before Transform");
Console.WriteLine("----------------");
Console.WriteLine(data);
Console.WriteLine();
Console.WriteLine();
XElement newData = XForm(data);
Console.WriteLine("After Transform");
Console.WriteLine("----------------");
Console.WriteLine(newData);
This example produces the following output:
Before Transform
----------------
<Root>
<Data>20</Data>
<Data>10</Data>
<Data>3</Data>
</Root>
After Transform
----------------
<Root>
<Data>20</Data>
<Data>10</Data>
<Data>3</Data>
<Average>11.0000</Average>
<Sum>33</Sum>
</Root>
Effecting the Transform
A small function, XForm, creates a new transformed tree from the original, annotated tree.
The implementation of this function in this example is only about 50 lines long.
XForm Function
The following code is a complete example that includes the XForm function. It includes most of the typical uses of this type of transform:
[c#]
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
class Program
{
static XNamespace xf = "http://www.microsoft.com/LinqToXmlTransform";
static XName at = xf + "ApplyTransforms";
// Build a transformed XML tree per the annotations
static XElement XForm(XElement source)
{
if (source.Annotation<XElement>() != null)
{
XElement anno = source.Annotation<XElement>();
return new XElement(anno.Name,
anno.Attributes(),
anno
.Nodes()
.Select(
(XNode n) =>
{
XElement annoEl = n as XElement;
if (annoEl != null)
{
if (annoEl.Name == at)
return (object)(
source.Nodes()
.Select(
(XNode n2) =>
{
XElement e2 = n2 as XElement;
if (e2 == null)
return n2;
else
return XForm(e2);
}
)
);
else
return n;
}
else
return n;
}
)
);
}
else
{
return new XElement(source.Name,
source.Attributes(),
source
.Nodes()
.Select(n =>
{
XElement el = n as XElement;
if (el == null)
return n;
else
return XForm(el);
}
)
);
}
}
static void Main(string[] args)
{
XElement root = new XElement("Root",
new XComment("A comment"),
new XAttribute("Att1", 123),
new XElement("Child", 1),
new XElement("Child", 2),
new XElement("Other",
new XElement("GC", 3),
new XElement("GC", 4)
),
XElement.Parse("<SomeMixedContent>This is <i>an</i> element that <b>has</b> some mixed content</SomeMixedContent>"),
new XElement("AnUnchangedElement", 42)
);
// each of the following serves the same semantic purpose as
// XSLT templates and sequence constructors
// replace Child with NewChild
foreach (var el in root.Elements("Child"))
el.AddAnnotation(new XElement("NewChild", (string)el));
// replace first GC with GrandChild, add an attribute
foreach (var el in root.Descendants("GC").Take(1))
el.AddAnnotation(
new XElement("GrandChild",
new XAttribute("ANewAttribute", 999),
(string)el
)
);
// replace Other with NewOther, add new child elements around original content
foreach (var el in root.Elements("Other"))
el.AddAnnotation(
new XElement("NewOther",
new XElement("MyNewChild", 1),
// same idea as xsl:apply-templates
new XElement(xf + "ApplyTransforms"),
new XElement("ChildThatComesAfter")
)
);
// change name of element that has mixed content
root.Descendants("SomeMixedContent").First().AddAnnotation(
new XElement("MixedContent",
new XElement(xf + "ApplyTransforms")
)
);
// replace <b> with <Bold>
foreach (var el in root.Descendants("b"))
el.AddAnnotation(
new XElement("Bold",
new XElement(xf + "ApplyTransforms")
)
);
// replace <i> with <Italic>
foreach (var el in root.Descendants("i"))
el.AddAnnotation(
new XElement("Italic",
new XElement(xf + "ApplyTransforms")
)
);
Console.WriteLine("Before Transform");
Console.WriteLine("----------------");
Console.WriteLine(root);
Console.WriteLine();
Console.WriteLine();
XElement newRoot = XForm(root);
Console.WriteLine("After Transform");
Console.WriteLine("----------------");
Console.WriteLine(newRoot);
}
}
This example produces the following output:
Before Transform
----------------
<Root Att1="123">
<!--A comment-->
<Child>1</Child>
<Child>2</Child>
<Other>
<GC>3</GC>
<GC>4</GC>
</Other>
<SomeMixedContent>This is <i>an</i> element that <b>has</b> some mixed content</SomeMixedContent>
<AnUnchangedElement>42</AnUnchangedElement>
</Root>
After Transform
----------------
<Root Att1="123">
<!--A comment-->
<NewChild>1</NewChild>
<NewChild>2</NewChild>
<NewOther>
<MyNewChild>1</MyNewChild>
<GrandChild ANewAttribute="999">3</GrandChild>
<GC>4</GC>
<ChildThatComesAfter />
</NewOther>
<MixedContent>This is <Italic>an</Italic> element that <Bold>has</Bold> some mixed content</MixedContent>
<AnUnchangedElement>42</AnUnchangedElement>
</Root>