Overview of a Functional Transform
[Blog Map] This blog is inactive. New blog: EricWhite.com/blog
There are lots of ways to transform XML. This example is a functional approach, somewhat similar in approach to XSLT. In this example, we first create a dictionary that defines our transform. The key in this dictionary is a path to the root. The value in the dictionary is a delegate that takes an XElement object, and returns a string. The function that gets called by the delegate takes the XElement object and assembles a string that contains whatever it wants to print for the node. Then, we assemble a number of entries for the dictionary, each containing a path and the function to render the report for any node that matches the path.
This isn't intended to be an exhaustive approach to using LINQ to XML to transform docs (although the exhaustive approach could be a lot of fun to code). Instead, it is just an example that shows that once you drink the FP cool-aid, you can find lots of cool ways to apply FP.
This example could be significantly extended. For example, you could use the Visit extension method so that you could compose output both going down and up the tree.
The code operates on the Purchase Orders xml document.
Here is the code to declare and populate the dictionary:
var txDef =
new Dictionary<string, Func<XElement, string>>();
txDef.Add("PurchaseOrders/",
n =>
"Purchase Order Report ["
+ String.Format("{0}", DateTime.Now)
+ "]"
+ Environment.NewLine
+ Environment.NewLine
);
txDef.Add("PurchaseOrders/PurchaseOrder/",
new Func<XElement, string>(FormatPurchaseOrderHeader)
);
txDef.Add("PurchaseOrders/PurchaseOrder/Address/",
delegate(XElement n)
{
StringBuilder sb = new StringBuilder();
return
sb
.Append("Address Type: ")
.Append((string)n.Attribute("Type") + Environment.NewLine)
.Append(" " + (string)n.Element("Name") + Environment.NewLine)
.Append(" " + (string)n.Element("Street") + Environment.NewLine)
.Append(" " + (string)n.Element("City"))
.Append(", " + (string)n.Element("State") + " ")
.Append((string)n.Element("Zip") + Environment.NewLine)
.Append(Environment.NewLine)
.ToString();
}
);
This code uses three types of delegates to populate the dictionary.
The first dictionary entry uses a lambda expression to format a string.
The second uses a delegate created in a traditional way, using a static method:
static string FormatPurchaseOrderHeader(XElement n)
{
return
"Purchase Order Number: "
+ (string)n.Attribute("PurchaseOrderNumber")
+ " Purchase Order Date: "
+ (string)n.Attribute("OrderDate")
+ Environment.NewLine;
}
The third uses an inline anonymous method, using the delegate keyword.
Once we have the dictionary declared, we can use it in a single statement, as follows:
Console.WriteLine(
pos
.SelfAndDescendants()
.Select(
n =>
(string)
(
txDef.ContainsKey(n.GetPath()) ?
txDef[n.GetPath()](n) :
""
)
)
.StringConcatenate()
);
Note that this example uses the GetPath extension method that I wrote for the ParseWordML example. It also uses the StringConcatenate extension method.
The call to the delegate is interesting - we retrieve the delegate from the dictionary and call it in a single expression:
txDef[n.GetPath()](n)
The entire program, including the extension methods follows:
using System;
using System.Collections.Generic;
using System.Text;
using System.Query;
using System.Xml.XLinq;
namespace LinqToXmlTransform
{
public delegate void VoidFunc<T0>(T0 a0);
public static class MySequence {
public static void ForEach<T>(
this IEnumerable<T> source,
VoidFunc<T> func)
{
foreach (var i in source)
func(i);
}
public static string GetPath(this XElement el)
{
return
el
.SelfAndAncestors()
.Aggregate("",
(seed, i) =>
i.Name.LocalName + "/" + seed
);
}
public static string StringConcatenate(
this IEnumerable<string> source)
{
StringBuilder sb = new StringBuilder();
foreach (var s in source)
sb.Append(s);
return sb.ToString();
}
public static string StringConcatenate<T>(
this IEnumerable<T> source,
Func<T, string> projectionFunc
)
{
StringBuilder sb = new StringBuilder();
foreach (var s in source)
sb.Append(projectionFunc(s));
return sb.ToString();
}
}
class Program
{
static string FormatPurchaseOrderHeader(XElement n)
{
return
"Purchase Order Number: "
+ (string)n.Attribute("PurchaseOrderNumber")
+ " Purchase Order Date: "
+ (string)n.Attribute("OrderDate")
+ Environment.NewLine;
}
static void Main(string[] args)
{
XElement pos =
XElement.Load("PurchaseOrders.xml");
var txDef =
new Dictionary<string, Func<XElement, string>>();
txDef.Add("PurchaseOrders/",
n =>
"Purchase Order Report ["
+ String.Format("{0}", DateTime.Now)
+ "]"
+ Environment.NewLine
+ Environment.NewLine
);
txDef.Add("PurchaseOrders/PurchaseOrder/",
new Func<XElement, string>(FormatPurchaseOrderHeader)
);
txDef.Add("PurchaseOrders/PurchaseOrder/Address/",
delegate(XElement n)
{
StringBuilder sb = new StringBuilder();
return
sb
.Append("Address Type: ")
.Append((string)n.Attribute("Type") + Environment.NewLine)
.Append(" " + (string)n.Element("Name") + Environment.NewLine)
.Append(" " + (string)n.Element("Street") + Environment.NewLine)
.Append(" " + (string)n.Element("City"))
.Append(", " + (string)n.Element("State") + " ")
.Append((string)n.Element("Zip") + Environment.NewLine)
.Append(Environment.NewLine)
.ToString();
}
);
Console.WriteLine(
pos
.SelfAndDescendants()
.Select(
n =>
(string)
(
txDef.ContainsKey(n.GetPath()) ?
txDef[n.GetPath()](n) :
""
)
)
.StringConcatenate()
);
}
}
}
When run on PurchaseOrders.xml, it outputs:
Purchase Order Report [10/3/2006 3:52:44 AM]
Purchase Order Number: 99503 Purchase Order Date: 1999-10-20
Address Type: Shipping
Ellen Adams
123 Maple Street
Mill Valley, CA 10999
Address Type: Billing
Tai Yee
8 Oak Avenue
Old Town, PA 95819
Purchase Order Number: 99505 Purchase Order Date: 1999-10-22
Address Type: Shipping
Cristian Osorio
456 Main Street
Buffalo, NY 98112
Address Type: Billing
Cristian Osorio
456 Main Street
Buffalo, NY 98112
Purchase Order Number: 99504 Purchase Order Date: 1999-10-22
Address Type: Shipping
Jessica Arnold
4055 Madison Ave
Seattle, WA 98112
Address Type: Billing
Jessica Arnold
4055 Madison Ave
Buffalo, NY 98112
Next: PurchaseOrders.xml
Comments
- Anonymous
May 05, 2009
public static string GetPath(this XElement el) { return el .AncestorsAndSelf() .Aggregate("", (seed, i) => i.Name.LocalName + "/" + seed ); }