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>

Tuesday, April 23, 2013

Tracking strangeness and tracking on pipeline

I hadn't noticed this earlier but there are tracking options that you can set at the pipeline level. I was running into an issue in BizTalk for a HTTP send port that I wanted to track message bodies in. Even though I had checked all the check boxes in the tracking tab of the HTTP send port, it wasn't tracking at all. After some digging around, I discovered that the PassThruTransmit and PassThruReceive pipelines in my BizTalk installation had Tracking disabled. I enabled Tracking events on this pipeline and voila, my send ports started working again. 

Now the question is, does setting tracking in the pipeline mean that all send ports/receive locations that use that pipeline will automatically be tracked or does this mean that I can still disable tracking at the individual send port/receive port level.

Thursday, April 21, 2011

Reserved words as field names in schema

I learnt my lesson on using reserved words in schemas today. I had a canonical schema that had two root nodes – request and response. The namespace for my schema was something like http://Narayan.Blog.ProcessingInstruction so naturally, I called the request, “request” and the response “response”.

SchemaWithReservedWords

The compiler didn’t like that. I got a bunch of errors none of which made it very clear that I was using a reserved word as seen below.ReservedWordErrors

 

 

 

 

 

 

As soon as I changed the field names to “processrequest” and “processresponse” the errors went away. Lesson learnt. I’ll be looking around to see if there’s a list of reserved words published anywhere. Lesson learnt.

My very first blog post...

Hello all and welcome to my blog. This is my very first blog post. I will be using this blog to document my thoughts, experiences and ideas on BizTalk Server. I have been a BizTalk developer for the past 4 years and love the technology. This blog is a way for me to start giving back to the BizTalk community. More posts to follow. Stay tuned.