Welcome to MSDN Blogs Sign in | Join | Help
Overview of a Functional Transform

[Back to the Table of Contents] 

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

Posted: Wednesday, October 04, 2006 5:33 AM by EricWhite

Comments

Krzysztof Radzimski said:

       public static string GetPath(this XElement el)

       {

           return

               el

               .AncestorsAndSelf()

               .Aggregate("",

                 (seed, i) =>

                   i.Name.LocalName + "/" + seed

               );

       }

# May 5, 2009 12:00 PM
Leave a Comment

(required) 

(required) 

(optional)

(required) 

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Page view tracker