Monday, September 16, 2013

Using C# functions for heavy lifting custom XSLT

 

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); 
  else 
    totalsByState.Add(state, decimal.Parse(amount)); 
  return string.Empty; 
} 
public string GetTotalForState(string state) 
{ 
  if(totalsByState.ContainsKey(state)) 
    return totalsByState [state].ToString(); 
  else 
    return "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>

No comments:

Post a Comment