Don’t you often run into situations when writing custom XSLT when you wonder how easy it would be if you could code that XSLT in straight c#? I do, and I found a way below to take advantage of C#’s power to solve some complex problem in XSLT.
Take the input XML below, I want to generate an output XML that groups sales by state. This is not a particularly complicates XSLT to write but we’ll use this example to illustrate just how easy and readable the XSLT code becomes with liberal use of C# functions.
Input
<Orders><Order><OrderID>0001</OrderID><Amount>100.00</Amount><State>TX</State></Order><Order><OrderID>0002</OrderID><Amount>75.00</Amount><State>CA</State></Order><Order><OrderID>0003</OrderID><Amount>50.00</Amount><State>TX</State></Order><Order><OrderID>0004</OrderID><Amount>25.00</Amount><State>CA</State></Order></Orders>
Expected Output
<SalesReportByState><Sales><State>TX</State><Amount>150.00</Amount></Sales><Sales><State>CA</State><Amount>100.00</Amount></Sales></SalesReportByState>
So my approach is to loop through all the Order nodes in the input XML and build a Dictionary that has the state as the key and the total sales amount per state as the value.
I have to first include the userCSharp namespace in the namespace declaration as highlighted below.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"xmlns:msxsl="urn:schemas-microsoft-com:xslt"xmlns:var="http://schemas.microsoft.com/BizTalk/2003/var"exclude-result-prefixes="msxsl var s0"version="1.0"xmlns:userCSharp="http://schemas.microsoft.com/BizTalk/2003/userCSharp">
The msxsl:script section will contain the variable definitions and the functions that I will use.
<msxsl:script language="C#" implements-prefix="userCSharp"><![CDATA[System.Collections.Generic.Dictionary<string, decimal> totalsByState = new System.Collections.Generic.Dictionary<string, decimal>();System.Collections.Generic.Dictionary<string, string> alreadyAddedState = new System.Collections.Generic.Dictionary<string, string>();public string ShouldStateBeRendered(string state){string result = string.Empty;if (alreadyAddedState.ContainsKey(state)){result = "false";}else{alreadyAddedState.Add(state, string.Empty);result = "true";}return result;}public string AggregateValues(string state, string amount){amount = (amount == null ? "0" : amount);if (totalsByState.ContainsKey(state))totalsByState [state] = totalsByState [state] + decimal.Parse(amount);elsetotalsByState.Add(state, decimal.Parse(amount));return string.Empty;}public string GetTotalForState(string state){if(totalsByState.ContainsKey(state))return totalsByState [state].ToString();elsereturn "0";}]]></msxsl:script>
In my XSLT script, I will first loop through all the Order nodes and build up the totalsByState Dictionary. I use a dummy variable in calling the aggregate function.
<xsl:for-each select="/Orders/Order "><xsl:variable name="state" select="State"/><xsl:variable name="amount" select="Amount" /><xsl:variable name="dummyForAggregation" select="userCSharp:AggregateValues($state,$amount)"/></xsl:for-each>
I then loop through the Order nodes and render the output Sales node only if it hasn’t already been rendered for that state.
<xsl:for-each select="/Orders/Order"><xsl:variable name="state" select="State"/><xsl:variable name="shouldStateBeRendered" select="userCSharp:ShouldStateBeRendered($state)"/><xsl:if test="$shouldStateBeRendered = 'true'"><xsl:element name="Sales"><xsl:element name="State">0</xsl:element><xsl:element name="Amount"><xsl:value-of select=" userCSharp: GetTotalForState($state)" /></xsl:element></xsl:element></xsl:if><xsl:for-each>