The BizTalk Runtime still makes an extensive use of the System.Xml.Xsl.XslTransform. When you create and build a BizTalk project, a separate .NET class is generated. for each transformation map. Each of these classes inherits from the Microsoft.XLANGs.BaseTypes.TransformBase class. For convenience, I used Reflector to retrieve and report its code in the table below. As you can easily note, the get accessor of the Transform property returns a XslTransform object.
TransformBase class
[Serializable] public abstract class TransformBase { // Methods protected TransformBase() { } // Properties public virtual string[] SourceSchemas { get { return null; } } public BTSXslTransform StreamingTransform { get { StringReader input = new StringReader(this.XmlContent); XmlTextReader stylesheet = new XmlTextReader(input); BTSXslTransform transform = new BTSXslTransform(); transform.Load(stylesheet, null, base.GetType().Assembly.Evidence); return transform; } } public virtual string[] TargetSchemas { get { return null; } } public XslTransform Transform { get { StringReader input = new StringReader(this.XmlContent); XmlTextReader stylesheet = new XmlTextReader(input); XslTransform transform = new XslTransform(); transform.Load(stylesheet, null, base.GetType().Assembly.Evidence); return transform; } } public XsltArgumentList TransformArgs { get { XmlDocument document = new XmlDocument(); document.PreserveWhitespace = true; document.LoadXml(this.XsltArgumentListContent); XsltArgumentList list = new XsltArgumentList(); foreach (XmlNode node in document.SelectNodes("//ExtensionObjects/ExtensionObject")) { XmlAttributeCollection attributes = node.Attributes; XmlNode namedItem = attributes.GetNamedItem("Namespace"); XmlNode node3 = attributes.GetNamedItem("AssemblyName"); XmlNode node4 = attributes.GetNamedItem("ClassName"); object extension = Assembly.Load(node3.Value).CreateInstance(node4.Value); list.AddExtensionObject(namedItem.Value, extension); } return list; } } public abstract string XmlContent { get; } public abstract string XsltArgumentListContent { get; } }
When BizTalk Server 2004 was built, the XslTransform was the only class provided by the Microsoft .NET Framework 1.1 to apply an XSLT to an inbound XML document. When the Microsoft .NET Framework version 2.0. was released, the XslTransform was declared obsolete and thus deprecated. As clearly stated on MSDN, the System.Xml.Xsl.XslCompiledTransform should be used instead. This class is used to compile and execute XSLT transformations. In most cases, the XslCompiledTransform class significantly outperforms the XslTransform class in terms of time need to execute the same XSLT against the same inbound XML document. The article Migrating From the XslTransform Class on MSDN reports as follows:
“The XslCompiledTransform class includes many performance improvements. The new XSLT processor compiles the XSLT style sheet down to a common intermediate format, similar to what the common language runtime (CLR) does for other programming languages. Once the style sheet is compiled, it can be cached and reused.”
The caveat is that because the XSLT is compiled to MSIL, the first time the transform is run there is a performance hit, but subsequent executions are much faster. To avoid paying the extra cost of initial compilation every time a map is executed, this latter could be cached in a static structure (e.g. Dictionary). I’ll show you how to implement this pattern in the second part of the article. For a detailed look at the performance differences between the XslTransform and XslCompiledTransform classes (plus comparisons with other XSLT processors) have a look at following posts.
Although the overall performance of the XslCompiledTransform class is better than the XslTransform class, the Load method of the XslCompiledTransform class might perform more slowly than the Load method of the XslTransform class the first time it is called on a transformation. This is because the XSLT file must be compiled before it is loaded. However, if you cache an XslCompiledTransform object for subsequent calls, its Transform method is incredibly faster than the equivalent Transform method of the XslTransform class. Therefore, from a performance perspective:
The XslTransform class is the best choice in a "Load once, Transform once" scenario as it doesn't require the initial map-compilation.
The XslCompiledTransform class is the best choice in a "Load once, Cache and Transform many times" scenario as it implies the initial cost for the map-compilation, but then this overhead is highly compensated by the fact that subsequent calls are much faster.
As BizTalk is a server application (or, if you prefer an application server), the second scenario is more likely than the first. The only way to take advantage of this class (given that BizTalk does not currently make use of the XslCompiledTransform class) is to write custom components. If this seems a little strange to you, remember that all BizTalk versions since BizTalk Server 2004 have inherited that core engine, based on .NET Framework 1.1. Since the XslCompiledTransform class wasn’t added until .NET Framework 2.0, it wasn’t leveraged in that version of BizTalk. While I’m currently working with the product group to see how best to take advantage of this class in the next version of BizTalk, let’s go ahead and walk through creating a helper class to boost the performance of message transformations in your current BizTalk implementation using the XslCompiledTransform class and let’s compare its performance with another helper component that makes use the old XslTransform class.
In order to compare the performance of the XslTransform and XslCompiledTransform classes I created an easy BizTalk application composed of the following projects:
This library contains 2 helpers classes called, respectively, XslTransformHelper and XslCompiledTransformHelper. These components share most of the code and expose the same static methods. I minimized the differences between the 2 classes as the final scope was to compare the performance of the XslTransform and XslCompiledTransform classes. As their name suggests, the first helper class uses the XslTransform class, while the second makes use of the XslCompiledTransform class. The Transform static method of both helper classes provides multiple overloads/variants/signatures. This allows the components to be invoked by any orchestration, pipeline component or .NET class in general. Either classes use a static Dictionary to cache maps in-process for later calls. The fully qualified name (FQDN) of a BizTalk map is used as key to retrieve the value of the corresponding instance within the Dictionary. The fully qualified name (FQDN) of a BizTalk map can be easily determined as follows:
Pretty easy, don’t you think?
XslTransformHelper class
#region Copyright //------------------------------------------------- // Author: Paolo Salvatori // Email: paolos@microsoft.com // History: 2010-01-26 Created //------------------------------------------------- #endregion #region Using References using System; using System.IO; using System.Text; using System.Collections.Generic; using System.Configuration; using System.Xml; using System.Xml.XPath; using System.Xml.Xsl; using System.Diagnostics; using Microsoft.XLANGs.BaseTypes; using Microsoft.XLANGs.Core; using Microsoft.BizTalk.Streaming; using Microsoft.BizTalk.CAT.Samples.DynamicTransforms.Helpers.Properties; #endregion namespace Microsoft.BizTalk.CAT.Samples.DynamicTransforms.Helpers { public class XslTransformHelper { #region Private Constants private const int DefaultBufferSize = 10240; //10 KB private const int DefaultThresholdSize = 1048576; //1 MB private const string DefaultPartName = "Body"; #endregion #region Private Static Fields private static Dictionary<string, TransformBase> mapDictionary; #endregion #region Static Constructor static XslTransformHelper() { mapDictionary = new Dictionary<string, TransformBase>(); } #endregion #region Public Static Methods public static XLANGMessage Transform(XLANGMessage message, string mapFullyQualifiedName, string messageName) { return Transform(message, 0, mapFullyQualifiedName, messageName, DefaultPartName, false, DefaultBufferSize, DefaultThresholdSize); } public static XLANGMessage Transform(XLANGMessage message, string mapFullyQualifiedName, string messageName, bool debug) { return Transform(message, 0, mapFullyQualifiedName, messageName, DefaultPartName, debug, DefaultBufferSize, DefaultThresholdSize); } public static XLANGMessage Transform(XLANGMessage message, int partIndex, string mapFullyQualifiedName, string messageName, string partName, bool debug, int bufferSize, int thresholdSize) { try { using (Stream stream = message[partIndex].RetrieveAs(typeof(Stream)) as Stream) { Stream response = Transform(stream, mapFullyQualifiedName, debug, bufferSize, thresholdSize); CustomBTXMessage customBTXMessage = null; customBTXMessage = new CustomBTXMessage(messageName, Service.RootService.XlangStore.OwningContext); customBTXMessage.AddPart(string.Empty, partName); customBTXMessage[0].LoadFrom(response); return customBTXMessage.GetMessageWrapperForUserCode(); } } catch (Exception ex) { ExceptionHelper.HandleException(Resources.XslCompiledTransformHelper, ex); TraceHelper.WriteLineIf(debug, null, ex.Message, EventLogEntryType.Error); throw; } finally { if (message != null) { message.Dispose(); } } } public static XLANGMessage Transform(XLANGMessage[] messageArray, int[] partIndexArray, string mapFullyQualifiedName, string messageName, string partName, bool debug, int bufferSize, int thresholdSize) { try { if (messageArray != null && messageArray.Length > 0) { Stream[] streamArray = new Stream[messageArray.Length]; for (int i = 0; i < messageArray.Length; i++) { streamArray[i] = messageArray[i][partIndexArray[i]].RetrieveAs(typeof(Stream)) as Stream; } Stream response = Transform(streamArray, mapFullyQualifiedName, debug, bufferSize, thresholdSize); CustomBTXMessage customBTXMessage = null; customBTXMessage = new CustomBTXMessage(messageName, Service.RootService.XlangStore.OwningContext); customBTXMessage.AddPart(string.Empty, partName); customBTXMessage[0].LoadFrom(response); return customBTXMessage.GetMessageWrapperForUserCode(); } } catch (Exception ex) { ExceptionHelper.HandleException(Resources.XslCompiledTransformHelper, ex); TraceHelper.WriteLineIf(debug, null, ex.Message, EventLogEntryType.Error); throw; } finally { if (messageArray != null && messageArray.Length > 0) { for (int i = 0; i < messageArray.Length; i++) { if (messageArray[i] != null) { messageArray[i].Dispose(); } } } } return null; } public static Stream Transform(Stream stream, string mapFullyQualifiedName) { return Transform(stream, mapFullyQualifiedName, false, DefaultBufferSize, DefaultThresholdSize); } public static Stream Transform(Stream stream, string mapFullyQualifiedName, bool debug) { return Transform(stream, mapFullyQualifiedName, debug, DefaultBufferSize, DefaultThresholdSize); } public static Stream Transform(Stream stream, string mapFullyQualifiedName, bool debug, int bufferSize, int thresholdSize) { try { TransformBase transformBase = GetTransformBase(mapFullyQualifiedName); if (transformBase != null) { VirtualStream virtualStream = new VirtualStream(bufferSize, thresholdSize); XPathDocument xpathDocument = new XPathDocument(stream); transformBase.Transform.Transform(xpathDocument, transformBase.TransformArgs, virtualStream); virtualStream.Seek(0, SeekOrigin.Begin); return virtualStream; } } catch (Exception ex) { ExceptionHelper.HandleException(Resources.DynamicTransformsHelper, ex); TraceHelper.WriteLineIf(debug, null, ex.Message, EventLogEntryType.Error); throw; } return null; } public static Stream Transform(Stream[] streamArray, string mapFullyQualifiedName) { return Transform(streamArray, mapFullyQualifiedName, false, DefaultBufferSize, DefaultThresholdSize); } public static Stream Transform(Stream[] streamArray, string mapFullyQualifiedName, bool debug) { return Transform(streamArray, mapFullyQualifiedName, debug, DefaultBufferSize, DefaultThresholdSize); } public static Stream Transform(Stream[] streamArray, string mapFullyQualifiedName, bool debug, int bufferSize, int thresholdSize) { try { TransformBase transformBase = GetTransformBase(mapFullyQualifiedName); if (transformBase != null) { CompositeStream compositeStream = null; try { VirtualStream virtualStream = new VirtualStream(bufferSize, thresholdSize); compositeStream = new CompositeStream(streamArray); XPathDocument xpathDocument = new XPathDocument(compositeStream); transformBase.Transform.Transform(xpathDocument, transformBase.TransformArgs, virtualStream); virtualStream.Seek(0, SeekOrigin.Begin); return virtualStream; } finally { if (compositeStream != null) { compositeStream.Close(); } } } } catch (Exception ex) { ExceptionHelper.HandleException(Resources.DynamicTransformsHelper, ex); TraceHelper.WriteLineIf(debug, null, ex.Message, EventLogEntryType.Error); throw; } return null; } #endregion #region Private Static Methods private static TransformBase GetTransformBase(string mapFullyQualifiedName) { TransformBase transformBase = null; lock (mapDictionary) { if (!mapDictionary.ContainsKey(mapFullyQualifiedName)) { Type type = Type.GetType(mapFullyQualifiedName); transformBase = Activator.CreateInstance(type) as TransformBase; if (transformBase != null) { mapDictionary[mapFullyQualifiedName] = transformBase; } } else { transformBase = mapDictionary[mapFullyQualifiedName]; } } return transformBase; } #endregion } }
XslCompiledTransformHelper class
#region Copyright //------------------------------------------------- // Author: Paolo Salvatori // Email: paolos@microsoft.com // History: 2010-01-26 Created //------------------------------------------------- #endregion #region Using References using System; using System.IO; using System.Text; using System.Collections.Generic; using System.Configuration; using System.Xml; using System.Xml.Xsl; using System.Xml.XPath; using System.Diagnostics; using Microsoft.XLANGs.BaseTypes; using Microsoft.XLANGs.Core; using Microsoft.BizTalk.Streaming; using Microsoft.BizTalk.CAT.Samples.DynamicTransforms.Helpers.Properties; #endregion namespace Microsoft.BizTalk.CAT.Samples.DynamicTransforms.Helpers { public class XslCompiledTransformHelper { #region Private Constants private const int DefaultBufferSize = 10240; //10 KB private const int DefaultThresholdSize = 1048576; //1 MB private const string DefaultPartName = "Body"; #endregion #region Private Static Fields private static Dictionary<string, MapInfo> mapDictionary; #endregion #region Static Constructor static XslCompiledTransformHelper() { mapDictionary = new Dictionary<string, MapInfo>(); } #endregion #region Public Static Methods public static XLANGMessage Transform(XLANGMessage message, string mapFullyQualifiedName, string messageName) { return Transform(message, 0, mapFullyQualifiedName, messageName, DefaultPartName, false, DefaultBufferSize, DefaultThresholdSize); } public static XLANGMessage Transform(XLANGMessage message, string mapFullyQualifiedName, string messageName, bool debug) { return Transform(message, 0, mapFullyQualifiedName, messageName, DefaultPartName, debug, DefaultBufferSize, DefaultThresholdSize); } public static XLANGMessage Transform(XLANGMessage message, int partIndex, string mapFullyQualifiedName, string messageName, string partName, bool debug, int bufferSize, int thresholdSize) { try { using (Stream stream = message[partIndex].RetrieveAs(typeof(Stream)) as Stream) { Stream response = Transform(stream, mapFullyQualifiedName, debug, bufferSize, thresholdSize); CustomBTXMessage customBTXMessage = null; customBTXMessage = new CustomBTXMessage(messageName, Service.RootService.XlangStore.OwningContext); customBTXMessage.AddPart(string.Empty, partName); customBTXMessage[0].LoadFrom(response); return customBTXMessage.GetMessageWrapperForUserCode(); } } catch (Exception ex) { ExceptionHelper.HandleException(Resources.XslCompiledTransformHelper, ex); TraceHelper.WriteLineIf(debug, null, ex.Message, EventLogEntryType.Error); throw; } finally { if (message != null) { message.Dispose(); } } } public static XLANGMessage Transform(XLANGMessage[] messageArray, int[] partIndexArray, string mapFullyQualifiedName, string messageName, string partName, bool debug, int bufferSize, int thresholdSize) { try { if (messageArray != null && messageArray.Length > 0) { Stream[] streamArray = new Stream[messageArray.Length]; for (int i = 0; i < messageArray.Length; i++) { streamArray[i] = messageArray[i][partIndexArray[i]].RetrieveAs(typeof(Stream)) as Stream; } Stream response = Transform(streamArray, mapFullyQualifiedName, debug, bufferSize, thresholdSize); CustomBTXMessage customBTXMessage = null; customBTXMessage = new CustomBTXMessage(messageName, Service.RootService.XlangStore.OwningContext); customBTXMessage.AddPart(string.Empty, partName); customBTXMessage[0].LoadFrom(response); return customBTXMessage.GetMessageWrapperForUserCode(); } } catch (Exception ex) { ExceptionHelper.HandleException(Resources.XslCompiledTransformHelper, ex); TraceHelper.WriteLineIf(debug, null, ex.Message, EventLogEntryType.Error); throw; } finally { if (messageArray != null && messageArray.Length > 0) { for (int i = 0; i < messageArray.Length; i++) { if (messageArray[i] != null) { messageArray[i].Dispose(); } } } } return null; } public static Stream Transform(Stream stream, string mapFullyQualifiedName) { return Transform(stream, mapFullyQualifiedName, false, DefaultBufferSize, DefaultThresholdSize); } public static Stream Transform(Stream stream, string mapFullyQualifiedName, bool debug) { return Transform(stream, mapFullyQualifiedName, debug, DefaultBufferSize, DefaultThresholdSize); } public static Stream Transform(Stream stream, string mapFullyQualifiedName, bool debug, int bufferSize, int thresholdSize) { try { MapInfo mapInfo = GetMapInfo(mapFullyQualifiedName, debug); if (mapInfo != null) { XmlTextReader xmlTextReader = null; try { VirtualStream virtualStream = new VirtualStream(bufferSize, thresholdSize); xmlTextReader = new XmlTextReader(stream); mapInfo.Xsl.Transform(xmlTextReader, mapInfo.Arguments, virtualStream); virtualStream.Seek(0, SeekOrigin.Begin); return virtualStream; } finally { if (xmlTextReader != null) { xmlTextReader.Close(); } } } } catch (Exception ex) { ExceptionHelper.HandleException(Resources.XslCompiledTransformHelper, ex); TraceHelper.WriteLineIf(debug, null, ex.Message, EventLogEntryType.Error); throw; } return null; } public static Stream Transform(Stream[] streamArray, string mapFullyQualifiedName) { return Transform(streamArray, mapFullyQualifiedName, false, DefaultBufferSize, DefaultThresholdSize); } public static Stream Transform(Stream[] streamArray, string mapFullyQualifiedName, bool debug) { return Transform(streamArray, mapFullyQualifiedName, debug, DefaultBufferSize, DefaultThresholdSize); } public static Stream Transform(Stream[] streamArray, string mapFullyQualifiedName, bool debug, int bufferSize, int thresholdSize) { try { MapInfo mapInfo = GetMapInfo(mapFullyQualifiedName, debug); if (mapInfo != null) { CompositeStream compositeStream = null; try { VirtualStream virtualStream = new VirtualStream(bufferSize, thresholdSize); compositeStream = new CompositeStream(streamArray); XmlTextReader reader = new XmlTextReader(compositeStream); mapInfo.Xsl.Transform(reader, mapInfo.Arguments, virtualStream); virtualStream.Seek(0, SeekOrigin.Begin); return virtualStream; } finally { if (compositeStream != null) { compositeStream.Close(); } } } } catch (Exception ex) { ExceptionHelper.HandleException(Resources.XslCompiledTransformHelper, ex); TraceHelper.WriteLineIf(debug, null, ex.Message, EventLogEntryType.Error); throw; } return null; } #endregion #region Private Static Methods private static MapInfo GetMapInfo(string mapFullyQualifiedName, bool debug) { MapInfo mapInfo = null; lock (mapDictionary) { if (!mapDictionary.ContainsKey(mapFullyQualifiedName)) { Type type = Type.GetType(mapFullyQualifiedName); TransformBase transformBase = Activator.CreateInstance(type) as TransformBase; if (transformBase != null) { XslCompiledTransform map = new XslCompiledTransform(debug); using (StringReader stringReader = new StringReader(transformBase.XmlContent)) { XmlTextReader xmlTextReader = null; try { xmlTextReader = new XmlTextReader(stringReader); XsltSettings settings = new XsltSettings(true, true); map.Load(xmlTextReader, settings, new XmlUrlResolver()); mapInfo = new MapInfo(map, transformBase.TransformArgs); mapDictionary[mapFullyQualifiedName] = mapInfo; } finally { if (xmlTextReader != null) { xmlTextReader.Close(); } } } } } else { mapInfo = mapDictionary[mapFullyQualifiedName]; } } return mapInfo; } #endregion } public class MapInfo { #region Private Fields private XslCompiledTransform xsl; private XsltArgumentList arguments; #endregion #region Public Constructors public MapInfo() { this.xsl = null; this.arguments = null; } public MapInfo(XslCompiledTransform xsl, XsltArgumentList arguments) { this.xsl = xsl; this.arguments = arguments; } #endregion #region Public Properties public XslCompiledTransform Xsl { get { return this.xsl; } set { this.xsl = value; } } public XsltArgumentList Arguments { get { return this.arguments; } set { this.arguments = value; } } #endregion } }
Note: Support for embedded scripts is an optional XSLT setting on the XslCompiledTransform class. Script support is disabled by default. Therefore, to enable script support, it’s necessary to create an XsltSettings object with the EnableScript property set to true and pass the object to the Load method. That’s what I did in my code above.
This project contains 2 Xml Schemas, CalculatorRequest and CalculatorResponse, which define, respectively, the request and response message and a PropertySchema that defines the Method promoted property. A CalculatorRequest message can contain zero or multiple Operation elements, as shown in the following picture:
CalculatorRequest message
<CalculatorRequest xmlns="http://microsoft.biztalk.cat/10/dynamictransforms/calculatorrequest"> <Method>UnitTest</Method> <Operations> <Operation> <Operator>+</Operator> <Operand1>82</Operand1> <Operand2>18</Operand2> </Operation> <Operation> <Operator>-</Operator> <Operand1>30</Operand1> <Operand2>12</Operand2> </Operation> <Operation> <Operator>*</Operator> <Operand1>25</Operand1> <Operand2>8</Operand2> </Operation> <Operation> <Operator>\</Operator> <Operand1>100</Operand1> <Operand2>25</Operand2> </Operation> </Operations> </CalculatorRequest>
A CalculatorResponse message contains a Result element for each Operation element within the corresponding CalculatorRequest message, as shown in the following picture:
CalculatorResponse message
<CalculatorResponse xmlns="http://microsoft.biztalk.cat/10/dynamictransforms/calculatorresponse"> <Status>Ok</Status> <Results> <Result> <Value>100</Value> <Error>None</Error> </Result> <Result> <Value>18</Value> <Error>None</Error> </Result> <Result> <Value>200</Value> <Error>None</Error> </Result> <Result> <Value>4</Value> <Error>None</Error> </Result> </Results> </CalculatorResponse>
This project contains the CalculatorRequestToCalculatorResponse map (see the picture below) that transforms an inbound request message into the corresponding response message.
This project contains the 4 orchestrations.
SingleDynamicTransform Test Case
This flow had been created just to test the XslCompiledTransformHelper class within an orchestration.
The following picture depicts the architecture of the SingleDynamicTransform test case.
Message Flow:
As shown in the picture below, this orchestration receives a CalculatorRequest xml document (80KB) and executes a loop (1000 iterations) in which it uses a Transform Shape to apply the CalculatorRequestToCalculatorResponse map to the inbound message. The orchestration does not produce any response message. The code within the StartStepTrace and EndStepTrace Expression Shapes keeps track of the time spent to execute the map at each iteration, while the code contained in the final Trace Expression Shape writes the total elapsed time on the standard output. The objective of this test case is to measure the time spent by the orchestration to apply the map to the inbound document 1000 times using the Transform Shape.
The following picture depicts the architecture of the DefaultStaticLoop test case.
This component is a variation of the DefaultStaticLoop orchestration. As this latter, it receives a CalculatorRequest xml document (80KB) and executes a loop (1000 iterations), but it doesn’t use a Transform shape to execute the CalculatorRequestToCalculatorResponse map against the inbound message, it rather uses a Message Assignment Shape that contain the following code. See How to Use Expressions to Dynamic Transform Messages for more information on this topic. The objective of this test case is to measure the time spent by the orchestration to apply the map to the inbound document 1000 times using the transform statement provided by the XLANG Runtime.
startTime = System.DateTime.Now; type = System.Type.GetType("<Map FQDN>"); transform(CalculatorResponse) = type(CalculatorRequest); stopTime = System.DateTime.Now; elapsedTime = stopTime.Subtract(startTime); total = total + elapsedTime.TotalMilliseconds; i = i + 1;
As the DefaultStaticLoop, the orchestration does not produce any response. The code within the CreateResponse Shape keeps track of the time spent to execute the map at each iteration, while the code contained in the final Trace Expression Shape writes the total elapsed time on the standard output.
The following picture depicts the architecture of the DefaultDynamicLoop test case.
As the previous orchestrations, the CustomDynamicLoop receives a CalculatorRequest xml document (80KB) and executes a loop (1000 iterations). However, instead of using a Transform shape or the Dynamic Transformation mechanism provided by BizTalk to apply the map to the inbound document, it uses an Expression Shape (see the code below) to invoke the Transform method exposed by my XslCompiledTransformHelper component. The objective of this test case is to measure the time spent by the orchestration to apply the map to the inbound document 1000 times using the XslCompiledTransformHelper class.
startTime = System.DateTime.Now; CalculatorResponse = Microsoft.BizTalk.CAT.Samples.DynamicTransforms.Helpers.XslCompiledTransformHelper.Transform(CalculatorRequest, "<Map FQDN>"); stopTime = System.DateTime.Now; elapsedTime = stopTime.Subtract(startTime); total = total + elapsedTime.TotalMilliseconds; i = i + 1;
As the previous orchestrations, the CustomDynamicLoop does not produce any response. The code within the final Trace Expression Shape writes the total elapsed time on the standard output.
The following picture depicts the architecture of the CustomDynamicLoop test case.
This project contains 2 custom pipeline components called, respectively, TransformPipelineComponent and LoopbackPipelineComponent.
TransformPipelineComponent
This component can be used within a Receive or a Send custom pipeline to transform the inbound message using the XslCompiledTransformHelper class. For the sake of brevity, we just report the code of the Execute method of in the picture below. Note that the if the loopback property exposed by the component equals true, this latter promotes the RouteDirectToTp context property to true. This way, when the TransformPipelineComponent is used by a Receive Pipeline within a Request-Response Receive Location, when the Message Agent posts the transformed message to the MessageBox, this latter is immediately returned as a response to the Receive Location (Loopback pattern).
Execute method
public IBaseMessage Execute(IPipelineContext context, IBaseMessage message) { try { if (componentEnabled) { if (context == null) { throw new ArgumentException("The pipeline context parameter cannot be null."); } if (message != null) { IBaseMessagePart bodyPart = message.BodyPart; Stream inboundStream = bodyPart.GetOriginalDataStream(); Stream outboundStream = XslCompiledTransformHelper.Transform(inboundStream, mapFQDN, traceEnabled, bufferSize, thresholdSize); bodyPart.Data = outboundStream; context.ResourceTracker.AddResource(inboundStream); context.ResourceTracker.AddResource(outboundStream); if (loopback) { message.Context.Promote("RouteDirectToTP", "http://schemas.microsoft.com/BizTalk/2003/system-properties", true); } } } } catch (Exception ex) { ExceptionHelper.HandleException("TransformPipelineComponent", ex); TraceHelper.WriteLineIf(traceEnabled, context, ex.Message, EventLogEntryType.Error); } return message; }
LoopbackPipelineComponent
This component can be used to set the RouteDirectToTp context property to true to implement the Loopback pattern. When used within a Receive Pipeline, the component allows to promote the MessageType property without the need to use an Xml Disassembler. At runtime, the MessageType is mandatory to determine the map to apply to a given message on a Request or Send Port.
public IBaseMessage Execute(IPipelineContext context, IBaseMessage message) { try { if (loopback) { message.Context.Promote("RouteDirectToTP", "http://schemas.microsoft.com/BizTalk/2003/system-properties", true); if (messageType != null) { message.Context.Promote("MessageType", "http://schemas.microsoft.com/BizTalk/2003/system-properties", messageType); } } } catch (Exception ex) { ExceptionHelper.HandleException("LoopbackPipelineComponent", ex); TraceHelper.WriteLineIf(traceEnabled, context, ex.Message, EventLogEntryType.Error); } return message; }
This project contains 2 custom pipelines:
Then, I created 2 use cases to compare the performance of the default message transformation provided by BizTalk Messaging Engine and the message transformation accomplished using my XslCompiledTransformHelper class.
TransformStaticallyDefined Test Case
The following picture depicts the architecture of the TransformStaticallyDefined test case.
DT.TransformStaticallyDefined.RP Configuration
The screen below shows that the use of the CalculatorRequestToCalculatorResponse map has been statically configured on the DT.TransformStaticallyDefined.RP Receive Port.
DT.TransformStaticallyDefined.WCF-NetTcp.RL Configuration
The following picture shows the configuration of the LoopbackReceivePipeline on the DT.TransformStaticallyDefined.WCF-NetTcp.RL Receive Location.
The following picture depicts the architecture of the TransformReceivePipeline test case.
DT.TransformReceivePipeline.WCF-NetTcp.RL Configuration
The following picture shows the configuration of the TransformReceivePipeline on the DT.TransformReceivePipeline.WCF-NetTcp.RL Receive Location.
Finally, I created a Test Project called UnitAndLoadTests that contains a small set of unit and load tests described below:
TestXslTransformHelper method
[TestMethod] public void TestXslTransformHelper() { Assert.AreNotEqual<string>(null, inputFile, "The inpuFile key in the configuration file cannot be null."); Assert.AreNotEqual<string>(String.Empty, inputFile, "The inpuFile key in the configuration file cannot be empty."); Assert.AreEqual<bool>(true, File.Exists(inputFile), string.Format(CultureInfo.CurrentCulture, "The {0} file does not exist.", inputFile)); Assert.AreNotEqual<string>(null, mapFullyQualifiedName, "The mapFullyQualifiedName key in the configuration file cannot be null."); Assert.AreNotEqual<string>(String.Empty, mapFullyQualifiedName, "The mapFullyQualifiedName key in the configuration file cannot be empty."); if (traceResponses) { Assert.AreEqual<bool>(true, Directory.Exists(outputFolder), string.Format(CultureInfo.CurrentCulture, "The {0} folder does not exist.", outputFolder)); } Type type = null; try { type = Type.GetType(mapFullyQualifiedName); } catch (Exception ex) { Assert.Fail(ex.Message); } MemoryStream stream = null; string message; using (StreamReader reader = new StreamReader(File.Open(inputFile, FileMode.Open, FileAccess.Read, FileShare.Read))) { message = reader.ReadToEnd(); } byte[] buffer = Encoding.UTF8.GetBytes(message); Stopwatch stopwatch = new Stopwatch(); Stream output = null; TestContext.BeginTimer("TestXslTransformHelper"); for (int i = 0; i < loops; i++) { stream = new MemoryStream(buffer); stopwatch.Start(); output = XslTransformHelper.Transform(stream, mapFullyQualifiedName); stopwatch.Stop(); if (output != null && traceResponses) { using (StreamReader reader = new StreamReader(output)) { message = reader.ReadToEnd(); } using (StreamWriter writer = new StreamWriter(File.OpenWrite( Path.Combine(outputFolder, string.Format(CultureInfo.CurrentCulture, "{{{0}}}.xml", Guid.NewGuid().ToString()))))) { writer.Write(message); writer.Flush(); } } } TestContext.EndTimer("TestXslTransformHelper"); Trace.WriteLine(String.Format(CultureInfo.CurrentCulture, "[TestXslTransformHelper] Loops: {0} Elapsed Time (milliseconds): {1}", loops, stopwatch.ElapsedMilliseconds)); }
All these tests share the same configuration contained in the App.config configuration file. In particular this latter contains the following information:
For the sake of completeness, I include below the App.config I used for my tests.
App.config file
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.serviceModel> <!-- Bindings used by client endpoints --> <bindings> <netTcpBinding> <binding name="netTcpBinding" closeTimeout="01:10:00" openTimeout="01:10:00" receiveTimeout="01:10:00" sendTimeout="01:10:00" transactionFlow="false" transferMode="Buffered" transactionProtocol="OleTransactions" hostNameComparisonMode="StrongWildcard" listenBacklog="100" maxBufferPoolSize="1048576" maxBufferSize="10485760" maxConnections="200" maxReceivedMessageSize="10485760"> <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384" maxBytesPerRead="4096" maxNameTableCharCount="16384" /> <reliableSession ordered="true" inactivityTimeout="00:10:00" enabled="false" /> <security mode="None"> <transport clientCredentialType="Windows" protectionLevel="EncryptAndSign" /> <message clientCredentialType="Windows" /> </security> </binding> </netTcpBinding> </bindings> <client> <!-- Client endpoints used by client excahnge messages with the WCF Receive Locations --> <endpoint address="net.tcp://localhost:3816/dynamictransforms" binding="netTcpBinding" bindingConfiguration="netTcpBinding" contract="System.ServiceModel.Channels.IRequestChannel" name="StaticMapEndpoint" /> <endpoint address="net.tcp://localhost:3817/dynamictransforms" binding="netTcpBinding" bindingConfiguration="netTcpBinding" contract="System.ServiceModel.Channels.IRequestChannel" name="DynamicMapEndpoint" /> </client> </system.serviceModel> <appSettings> <add key="mapFullyQualifiedName" value="Microsoft.BizTalk.CAT.Samples.DynamicTransforms.Maps.CalculatorRequestToCalculatorResponse, Microsoft.BizTalk.CAT.Samples.DynamicTransforms.Maps, Version=1.0.0.0, Culture=neutral, PublicKeyToken=8c83cae5bc47edb0"/> <add key="inputFile" value="C:\Projects\DynamicTransforms\Test\UnitTest.xml"/> <add key="outputFolder" value="C:\Projects\DynamicTransforms\Test\Out"/> <add key="traceResponses" value="false"/> <add key="loops" value="1000"/> </appSettings> </configuration>
Let’s start running some of the test cases and unit tests I created. Take into account that the unit tests and that you can find in the code attached to the article are parametric and they can be executed using any xml message and map. Therefore, I strongly encourage you to repeat my tests using your own messages and maps.
I configured both the unit tests to execute the CalculatorRequestToCalculatorResponse map against the the UnitTest.xml file (80KB) 1000 times. Each test method uses an instance of the Stopwatch class to measure the time spent to executing all calls and finally traces a message containing the total elapsed time. The screens below were taken within Visual Studio at the end of the 2 tests.
TestXslTransformHelper
TestXslCompiledTransformHelper
The difference in terms of performance between the 2 unit tests is simply astonishing:
Obviously, I conducted several test runs and they all confirmed that the XslCompiledTransformHelper is class incredibly faster than the XslTransformHelper class and this clearly demonstrates that the XslCompiledTransform class is absolutely much better than the XslTransform class in a Load once, Cache and Transform many times” scenario.
All the orchestrations used in the 3 test cases share the same structure and implement the same behavior using a different technique:
Each orchestration contains a loop that executes the message transformation exactly 1000 times and finally reports the total elapsed time. For the test I created 3 separate xml files (they can be found in the Test folder), one for each orchestration. As I explained in the first part of the article, each orchestration receives the request message through a Direct Bound Port. In particular, the following Filter Expression has been defined on the Activate Receive Shape of each orchestration:
Therefore, the following files are identical:
with the exception of the Method element that contains the name of the related orchestration. To execute each test case is sufficient to copy the corresponding file to the Test\IN folder: the DT.FILE.RL FILE Receive Location will than receive the message and activate the intended test case. I used DebugView to keep track of the elapsed time reported by each of the test cases:
Results are quite eloquent and don’t give room to doubts:
Once again, I conducted several test runs to confirm the results obtained and reported above. These latter clearly demonstrated that the XslCompiledTransformHelper class is an order of magnitude faster than the default mechanisms provided by BizTalk for transforming messages.
The objective of this test is to compare the performance of the following test cases:
To generate traffic against the 2 test cases and measure performance I used the following Load Tests defined in the UnitAndLoadTests Test Project:
In particular, as shown in the picture below, I created a custom Counter Set called BizTalk composed of the following performance counters:
Specifically, the average latency measured by the Inbound Latency (sec) counter includes the time spent for transforming the message in both use cases. Obviously it counts also the time spent running other activities like posting the message to the MessageBox, but still it represents a good mechanism to measure to compare the time spent by the 2 test cases for transforming the inbound message.
I conducted several test runs to confirm results obtained. The screens below were taken, respectively, at the end of StaticMapLoadTest and DynamicMapLoadTest:
The following table reports for convenience the results highlighted in the screens above:
The difference in terms of latency and throughput between the 2 test cases is quite dramatic and this clearly confirms once again that the XslCompiledTransform class is much faster than the XslTransform class natively used by BizTalk. In our case, the adoption of the custom XslCompiledTransformHelper class allowed to double the throughput and halve the latency. Obviously, the performance gain can vary from case to case as it depends on many factors (inbound message size, map complexity, etc.), but it’s quite evident that the overall performance of a BizTalk application that makes an extensive use of message transformations can greatly be improved using a helper component like the XslCompiledTransformHelper class that exploits the XslCompiledTransform class to compile, invoke and cache maps for later calls.
As I said in the first part of the article, I started to work with the product group to see how best to take advantage of the XslCompiledTransform class in the next version of BizTalk. Nevertheless, you can immediately exploits this class in your custom components to boost the execution of you message transformations. Therefore, I encourage you to download my code here and repeat the tests described in this article using your own messages and maps.
I wrote another article on this subject and extended my code to support multi-source-document-maps. You can find my post here.
Here you can download the code. Any feedback is highly appreciated. ;-)
Thanks Paolo, for all the work you put into this post, -it will come to good use!
Thanks Mikael, my only wish is to share my findings with the community of BizTalk programmers or should I say the "big family" of BizTalk developers. ;-)
Ciao,
Paolo
P.S. I added your blog to my list of suggested BizTalk blogs.
Paolo,
another absolutely amazing blog post with lots of real world applicability.
It's a shame BizTalk didn't move over to .NET 2.0 at 2006.
A huge thank you for the effort on this one, I for one will be putting your code to use almost immediately :)
Many thanks
TM
Thanks TM, your are right when you say that BizTalk should have moved over to .NET 2.0 at 2006. I'll do my best to see this and other improvements implemented in the next versions of the product.
Hi Paolo
Very good, long and thourough post.
Excellent! :-)
--
eliasen
Wow! An excellent document - well worth reading. Do you know if BizTalk 2009 still uses the old XslTransform class or has it moved on to the compiled version?
Not yet! According to the product group folks, it might be implemented in a future release after BizTalk Server 2009 R2. This feature requires regression tests to verify that is compatible with all the maps generated with the BizTalk Mapper. My suggestion has been to add a checkbox/option on the Send/Receive Ports and a property to the Transformation shape of an orchestration to give developers the possibility to declare that the map should be cached and applied using the XslCompiledTransform. I'll insist to see this feature implemented in a future release, but at the moment this is just a promise. ;-)
How did you add your reference to Microsoft.BizTalk.Streaming? Did you do it by hand? Much as I love this code example, I'm a bit worried about referencing assemblies in the GAC.
Hi Greg,
I understand your point! You can just copy the Microsoft.BizTalk.Streaming assembly from the installation dvd (MSI\Program Files folder) to the C:\Program Files (x86)\Microsoft BizTalk Server 2009 and then reference this copy. :-)
Excellent post. I was just discussing this very issue with my team leader last week. Will be very useful indeed.
Very nice post. I just have a question. How would you handle mupltiple input messages in a map? I have a map that has 2 input source msgs that i would like to unit test. The BTS expression shape has transform(outmsg) = map(msg1, msg2)
But i have not yet found a way to do it in a C# class..
THanks
Hi mate,
I wrote an entire new post to answer your question. Please, donwload the new version of my code and follow up if you encounter any problem. I had just one day at my disposal to write and test the new code.
This is absolutely amazing stuff! My suggested architectures are always map heavy and this totally resolves my performance concerns with that approach.
I cannot wait to start using these. I just wish I had brought this up at the MVP Summit.
Thank you!
-Dan
Thanks Dan,
also review the second post I wrote to extend my component to support maps that have multiple input documents:
http://blogs.msdn.com/paolos/archive/2010/04/08/how-to-boost-message-transformations-using-the-xslcompiledtransform-class-extended.aspx
Please, let me know if my helper component provided any benefit to your application in terms of performance, but also if it had problems with a given map or under certain conditions.
Now that BizTalk 2010 beta is out, you can use BizTalk maps in Workflow Foundation through the Mapper activity. (Installation of the WCF LOB Adapter SDK is required.) I was curious to see if there is a simple way to do the same, i.e. map from data contract to data contract, from pure .Net code (without the workflow context). It turned out to be relatively simple. I modified your XslCompiledTransform class removing BizTalk message parameters as follows:
public class XslCompiledTransformHelper<TransformType, InputDataContractType, OutputDataContractType> where TransformType : Microsoft.XLANGs.BaseTypes.TransformBase
{
#region Private Constants
private const int DefaultBufferSize = 10240; //10 KB
private const int DefaultThresholdSize = 1048576; //1 MB
#endregion
#region Private Static Fields
private static Dictionary<string, MapInfo> mapDictionary;
private static DataContractSerializer inputDCSerializer;
private static XmlSerializer inputXmlSerializer;
private static DataContractSerializer outputDCSerializer;
private static XmlSerializer outputXmlSerializer;
#region Static Constructor
static XslCompiledTransformHelper()
mapDictionary = new Dictionary<string, MapInfo>();
if (UseXmlSerializer(typeof(InputDataContractType)))
inputXmlSerializer = new XmlSerializer(typeof(InputDataContractType));
else
inputDCSerializer = new DataContractSerializer(typeof(InputDataContractType));
if (UseXmlSerializer(typeof(OutputDataContractType)))
outputXmlSerializer = new XmlSerializer(typeof(OutputDataContractType));
outputDCSerializer = new DataContractSerializer(typeof(OutputDataContractType));
}
#region Public Static Methods
public static OutputDataContractType Transform(InputDataContractType input)
return Transform(input, false, DefaultBufferSize, DefaultThresholdSize);
public static OutputDataContractType Transform(InputDataContractType input, bool debug)
return Transform(input, debug, DefaultBufferSize, DefaultThresholdSize);
public static OutputDataContractType Transform(InputDataContractType input, bool debug, int bufferSize, int thresholdSize)
try
return Deserialize(Transform(Serialize(input), debug, bufferSize, thresholdSize));
catch (Exception ex)
ExceptionHelper.HandleException("XslCompiledTransformHelper", ex);
TraceHelper.WriteLineIf(debug, ex.Message, EventLogEntryType.Error);
throw;
public static Stream Transform(Stream stream)
return Transform(stream, false, DefaultBufferSize, DefaultThresholdSize);
public static Stream Transform(Stream stream, bool debug)
return Transform(stream, debug, DefaultBufferSize, DefaultThresholdSize);
public static Stream Transform(Stream stream, bool debug, int bufferSize, int thresholdSize)
MapInfo mapInfo = GetMapInfo(typeof(TransformType).FullName, debug);
if (mapInfo != null)
XmlTextReader xmlTextReader = null;
VirtualStream virtualStream = new VirtualStream(bufferSize, thresholdSize);
xmlTextReader = new XmlTextReader(stream);
mapInfo.Xsl.Transform(xmlTextReader, mapInfo.Arguments, virtualStream);
virtualStream.Seek(0, SeekOrigin.Begin);
return virtualStream;
finally
if (xmlTextReader != null)
xmlTextReader.Close();
return null;
#region Private Static Methods
private static MapInfo GetMapInfo(string mapFullyQualifiedName, bool debug)
MapInfo mapInfo = null;
lock (mapDictionary)
if (!mapDictionary.ContainsKey(mapFullyQualifiedName))
TransformType transformBase = Activator.CreateInstance<TransformType>();
if (transformBase != null)
XslCompiledTransform map = new XslCompiledTransform(debug);
using (StringReader stringReader = new StringReader(transformBase.XmlContent))
xmlTextReader = new XmlTextReader(stringReader);
XsltSettings settings = new XsltSettings(true, true);
map.Load(xmlTextReader, settings, new XmlUrlResolver());
mapInfo = new MapInfo(map, transformBase.TransformArgs);
mapDictionary[mapFullyQualifiedName] = mapInfo;
mapInfo = mapDictionary[mapFullyQualifiedName];
return mapInfo;
private static Stream Serialize(InputDataContractType input)
VirtualStream stream = new VirtualStream();
if (inputDCSerializer != null)
inputDCSerializer.WriteObject(stream, input);
inputXmlSerializer.Serialize(stream, input);
stream.Seek(0, SeekOrigin.Begin);
return stream;
private static OutputDataContractType Deserialize(Stream stream)
if (outputDCSerializer != null)
XmlReader reader = XmlReader.Create(stream);
return (OutputDataContractType)outputDCSerializer.ReadObject(reader);
return (OutputDataContractType)outputXmlSerializer.Deserialize(stream);
private static bool UseXmlSerializer(Type type)
do
object[] customAttributes = type.GetCustomAttributes(typeof(XmlTypeAttribute), true);
if ((customAttributes != null) && (customAttributes.Length > 0))
return true;
customAttributes = type.GetCustomAttributes(typeof(XmlRootAttribute), true);
if (type.IsArray)
type = type.GetElementType();
type = null;
while (type != null);
return false;
For usage, see holsson.spaces.live.com/.../cns!91D78390ECE48C0D!913.entry.