How To Boost Message Transformations Using the XslCompiledTransform class
Introduction
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
|
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.
-
XslCompiledTransform Performance: Beating MSXML 4.0
-
XslCompiledTransform Slower than XslTransform?
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](https://msdn.microsoft.com/en-us/library/system.xml.xsl.xsltransform.aspx) 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.
BizTalk Application
In order to compare the performance of the XslTransform and XslCompiledTransform classes I created an easy BizTalk application composed of the following projects:
Helpers
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:
-
Open the BizTalk Administration Console and navigate to the Maps folder within your BizTalk application.
-
Double click the map in question.
-
Copy the content of the Name label (see the picture below) and paste it in a text editor.
-
Append a comma followed by a space (“, “).
-
Copy the content of the Assembly label (see the picture below) and paste it in a text editor.
Pretty easy, don’t you think?
XslTransformHelper class
|
XslCompiledTransformHelper class
|
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.
Schemas
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
|
A CalculatorResponse message contains a Result element for each Operation element within the corresponding CalculatorRequest message, as shown in the following picture:
CalculatorResponse message
|
Maps
This project contains the CalculatorRequestToCalculatorResponse map (see the picture below) that transforms an inbound request message into the corresponding response message.
Orchestrations
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:
1.
A One-Way FILE Receive Location receives a new CalculatorRequest xml document from the IN folder.
2.
The XML disassembler component within the XMLTransmit pipeline promotes the Method element inside the CalculatorRequest xml document. The Message Agent submits the incoming message to the MessageBox (BizTalkMsgBoxDb).
3.
The inbound request starts a new instance of the SingleDynamicTransform. This latter uses a Direct Bound Port and a Filter to receive only the CalculatorRequest messages with the Method promoted property = “SingleDynamicTransform”.
4.
The SingleDynamicTransform invokes the Transform static method exposed by the XslCompiledTransformHelper class to apply the CalculatorRequestToCalculatorResponse map to the inbound CalculatorRequest message and generate the corresponding CalculatorResponse document.
5.
The CalculatorRequestToCalculatorResponse publishes the CalculatorResponse message to the MessageBox (BizTalkMsgBoxDb).
6.
The response message is retrieved by a One-Way FILE Send Port.
7.
The response message is written to an OUT folder by the One-Way FILE Send Port.
DefaultStaticLoop Test Case
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.
Message Flow:
1.
A One-Way FILE Receive Location receives a new CalculatorRequest xml document from the IN folder.
2.
The XML disassembler component within the XMLTransmit pipeline promotes the Method element inside the CalculatorRequest xml document. The Message Agent submits the incoming message to the MessageBox (BizTalkMsgBoxDb).
3.
The inbound request starts a new instance of the DefaultStaticLoop . This latter uses a Direct Bound Port and a Filter to receive only the CalculatorRequest messages with the Method promoted property = “DefaultStaticLoop”.
4.
The DefaultStaticLoop executes a loop (1000 iterations) in which it uses a Transform Shape to apply the CalculatorRequestToCalculatorResponse map to the inbound CalculatorRequest message (80KB).
DefaultDynamicLoop 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.
|
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.
Message Flow:
1.
A One-Way FILE Receive Location receives a new CalculatorRequest xml document from the IN folder.
2.
The XML disassembler component within the XMLTransmit pipeline promotes the Method element inside the CalculatorRequest xml document. The Message Agent submits the incoming message to the MessageBox (BizTalkMsgBoxDb).
3.
The inbound request starts a new instance of the DefaultDynamicLoop . This latter uses a Direct Bound Port and a Filter to receive only the CalculatorRequest messages with the Method promoted property = “DefaultDynamicLoop”.
4.
The DefaultDynamicLoop executes a loop (1000 iterations) in which it uses a Message Assignment Shape to execute the CalculatorRequestToCalculatorResponse map against the inbound CalculatorRequest message (80KB).
CustomDynamicLoop 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.
|
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.
Message Flow:
1.
A One-Way FILE Receive Location receives a new CalculatorRequest xml document from the IN folder.
2.
The XML disassembler component within the XMLTransmit pipeline promotes the Method element inside the CalculatorRequest xml document. The Message Agent submits the incoming message to the MessageBox (BizTalkMsgBoxDb).
3.
The inbound request starts a new instance of the CustomDynamicLoop. This latter uses a Direct Bound Port and a Filter to receive only the CalculatorRequest messages with the Method promoted property = “CustomDynamicLoop”.
4.
The CustomDynamicLoop executes a loop (1000 iterations) in which it uses a XslCompiledTransformHelper class to execute the CalculatorRequestToCalculatorResponse map against the inbound CalculatorRequest message (80KB).
Pipeline Components
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
|
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.
Execute method
|
Pipelines
This project contains 2 custom pipelines:
-
TransformReceivePipeline:this pipeline contains only an instance of the TransformPipelineComponent.
-
LoopbackReceivePipeline: this pipeline contains only an instance of the LoopbackPipelineComponent.
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.
Message Flow:
1.
The DT.TransformStaticallyDefined.WCF-NetTcp.RL WCF-NetTcp Request-Response Receive Location receives a CalculatorRequest xml document submitted running the InvokeStaticMap Unit Test within Visual Studio.
2.
The LoopbackReceivePipeline promotes the RouteDirectToTp property to true and the MessageType property. I could have used the Xml Disassembler component within the Receive Pipeline to find and promote the MessageType, but I preferred to specify the MessageType of the inbound message as part of the configuration of the Receive Location (see the picture below). This way I can avoid the overhead introduced by the Xml Disassembler component and measure just the time spent by the Messaging Engine to apply the CalculatorRequestToCalculatorResponse map statically defined on the Receive Port. Once transformed the CalculatorRequest message into a CalculatorResponse document, the Message Agent posts this latter to the MessageBox.
3.
The transformed message is immediately returned to the Receive Location.
4.
The response message is returned to the InvokeStaticMap Unit Test.
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.
TransformReceivePipeline Test Case
The following picture depicts the architecture of the TransformReceivePipeline test case.
Message Flow:
1.
The DT.TransformReceivePipeline.WCF-NetTcp.RL WCF-NetTcp Request-Response Receive Location receives a CalculatorRequest xml document submitted running the InvokeDynamicMap Unit Test within Visual Studio.
2.
The TransformPipelineComponent (the following picture shows its configuration) promotes the RouteDirectToTp property to true and transforms the inbound message using the XslCompiledTransformHelper class and the CalculatorRequestToCalculatorResponse map. Then the Message Agent posts the transformed message to the MessageBox.
3.
The transformed message is immediately returned to the Receive Location.
4.
The response message is returned to the InvokeDynamicMap Unit Test.
DT.TransformReceivePipeline.WCF-NetTcp.RL Configuration
The following picture shows the configuration of the TransformReceivePipeline on the DT.TransformReceivePipeline.WCF-NetTcp.RL Receive Location.
UnitAndLoadTests
Finally, I created a Test Project called UnitAndLoadTests that contains a small set of unit and load tests described below:
- TestXslTransformHelper: this unit test can be used to measure the time spent to execute loops transformations using the XslTransformHelper class, where loops is defined in the configuration file. The following picture reports the code of the TestXslTransformHelper unit test.
TestXslTransformHelper method
|
- TestXslCompiledTransformHelper: this unit test can be used to measure the time spent to execute loops transformations using the XslCompiledTransformHelper class, where loops is defined in the configuration file. For the sake of brevity, I omitted to include the code of the TestXslCompiledTransformHelper unit test as this latter is very similar to one of the previous unit test.
- InvokeStaticMap: this unit test can be used to send a single CalculatorRequest xml document to the DT.TransformStaticallyDefined.WCF-NetTcp.RL Receive Location used by the TransformStaticallyDefined Test case.
- InvokeDynamicMap: this unit test can be used to send a single CalculatorRequest xml document to the DT.TransformReceivePipeline.WCF-NetTcp.RL Receive Location used by the TransformReceivePipeline Test case.
- StaticMapLoadTest: this load test is based on the InvokeStaticMap unit test and can be used to generate traffic against the TransformStaticallyDefined Use Case.
- DynamicMapLoadTest: this load test is based on the InvokeDynamicMap unit test and can be used to generate traffic against the TransformReceivePipeline Use Case.
All these tests share the same configuration contained in the App.config configuration file. In particular this latter contains the following information:
-
The WCF Endpoint used to invoke the DT.TransformStaticallyDefined.WCF-NetTcp.RL and DT.TransformReceivePipeline.WCF-NetTcp.RL Receive Locations.
-
The appSettings section defines multiple keys that allows to control the runtime behavior of unit and load tests:
-
mapFullyQualifiedName: contains the name of the map used by TestXslTransformHelper and TestXslCompiledTransformHelper unit tests.
-
inputFile: defines the path of the inbound document used by all unit tests (TestXslTransformHelper , TestXslCompiledTransformHelper, InvokeStaticMap, InvokeDynamicMap).
-
outputFolder: indicates the path where to save response messages.
-
traceResponses: indicates whether to save response messages.
-
loops: allows to control the number of loop iterations performed by the TestXslTransformHelper and TestXslCompiledTransformHelper unit tests.
For the sake of completeness, I include below the App.config I used for my tests.
App.config file
|
Results
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.
TestXslTransformHelper vs TestXslCompiledTransformHelper
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:
- TestXslTransformHelper Unit Test: Total Elapsed Time = ~144 seconds, Average Elapsed Time/Transformation = ~144 milliseconds
- TestXslCompiledTransformHelper Unit Test: Total Elapsed Time = ~3.5 seconds, Average Elapsed Time/Transformation = ~3.5 milliseconds
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.
DefaultStaticLoop Test Case vs DefaultDynamicLoop Test Case vs CustomDynamicLoop Test Case
All the orchestrations used in the 3 test cases share the same structure and implement the same behavior using a different technique:
- DefaultStaticLoop orchestration: uses a Transform shape to execute the CalculatorRequestToCalculatorResponse against the inbound document.
- DefaultDynamicLoop orchestration: uses the transform method within a Message Assignment Shape to accomplish the same task.
- CustomDynamicLoop orchestration: uses the XslCompiledTransformHelper.Transform method to invoke the map against against the request message.
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:
-
https://microsoft.biztalk.cat/10/dynamictransforms/propertyschema.Method == <OrchestrationName>
Therefore, the following files are identical:
-
DefaultStaticLoop.xml
-
DefaultDynamicLoop.xml
-
CustomDynamicLoop.xml
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:
- DefaultStaticLoop Test Case: Total Elapsed Time = ~57.3 seconds, Average Elapsed Time/Transformation = ~57 milliseconds
- DefaultDynamicLoop Test Case: Total Elapsed Time = ~56.2 seconds, Average Elapsed Time/Transformation = ~56 milliseconds
- CustomDynamicLoop Test Case: Total Elapsed Time = ~3.6 seconds, Average Elapsed Time/Transformation = ~3.6 milliseconds
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.
TransformStaticallyDefined vs TransformReceivePipeline
The objective of this test is to compare the performance of the following test cases:
-
TransformStaticallyDefined Test Case: as explained in the first part of the article, the inbound CalculatorRequest message is transformed using the CalculatorRequestToCalculatorResponse map declaratively configured on the DT.TransformStaticallyDefined.RP Request Port. Once posted to the MessageBox, the CalculatorResponse transformed message is immediately returned to the DT.TransformStaticallyDefined.WCF-NetTcp.RL Receive Location (Loopback pattern).
-
TransformReceivePipeline Test Case: the inbound CalculatorRequest message is transformed by the TransformReceivePipeline hosted by the DT.TransformStaticallyDefined.WCF-NetTcp.RL Receive Location. In particular, the TransformPipelineComponent invokes the XslCompiledTransformHelper.Transform static method to apply the CalculatorRequestToCalculatorResponse map to the inbound xml document. The FQDN of the map is declaratively specified in the pipeline configuration. Once posted to the MessageBox, the CalculatorResponse transformed message is immediately returned to the DT.TransformStaticallyDefined.WCF-NetTcp.RL Receive Location (Loopback pattern).
To generate traffic against the 2 test cases and measure performance I used the following Load Tests defined in the UnitAndLoadTests Test Project:
-
StaticMapLoadTest: this load test is based on the InvokeStaticMap unit test and can be used to generate traffic against the TransformStaticallyDefined Use Case. The test is configured to send 1000 CalculatorRequest messages to the DT.TransformStaticallyDefined.WCF-NetTcp.RL Receive Location using 25 different worker threads.
-
DynamicMapLoadTest: this load test is based on the InvokeDynamicMap unit test and can be used to generate traffic against the TransformReceivePipeline Use Case. The test is configured to send 1000 CalculatorRequest messages to the DT.TransformStaticallyDefined.WCF-NetTcp.RL Receive Location using 25 different worker threads.
In particular, as shown in the picture below, I created a custom Counter Set called BizTalk composed of the following performance counters:
-
Inbound Latency (sec): measures the average latency in milliseconds from when the Messaging Engine receives a document from the adapter until the time it is published to Message Box.
-
Request-Response Latency (sec): measures the average latency in milliseconds from when the Messaging Engine receives a request document from the adapter until the time a response document is given back to the adapter.
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:
StaticMapLoadTest Graphs & Summary
DynamicMapLoadTest Graphs & Summary
The following table reports for convenience the results highlighted in the screens above:
Test Case | Inbound Latency (sec) | Request Response Latency (sec) | Avg Test Time (sec) | Tests/sec (Throughput) | Duration (sec) | % CPU Time |
StaticMapLoadTest | 0.41 | 0.90 | 2.55 | 8.77 | 114 | 65.2 |
DynamicMapLoadTest | 0.11 | 0.49 | 1.29 | 17.6 | 56 | 43 |
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.
Conclusions
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.
Follow-Up
I wrote another article on this subject and extended my code to support multi-source-document-maps. You can find my post here.
Code
Here you can download the code. Any feedback is highly appreciated. ;-)
Comments
Anonymous
January 29, 2010
Thanks Paolo, for all the work you put into this post, -it will come to good use!Anonymous
January 29, 2010
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.Anonymous
January 29, 2010
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 TMAnonymous
January 31, 2010
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. Ciao, PaoloAnonymous
February 02, 2010
Hi Paolo Very good, long and thourough post. Excellent! :-) -- eliasenAnonymous
March 10, 2010
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?Anonymous
March 11, 2010
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. ;-)Anonymous
March 16, 2010
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.Anonymous
March 17, 2010
Hi Greg, I understand your point! You can just copy the Microsoft.BizTalk.Streaming assembly from the installation dvd (MSIProgram Files folder) to the C:Program Files (x86)Microsoft BizTalk Server 2009 and then reference this copy. :-)Anonymous
March 30, 2010
Excellent post. I was just discussing this very issue with my team leader last week. Will be very useful indeed.Anonymous
March 31, 2010
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.. THanksAnonymous
April 07, 2010
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. Ciao, PaoloAnonymous
April 12, 2010
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! -DanAnonymous
April 13, 2010
The comment has been removedAnonymous
June 14, 2010
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; #endregion #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)); else outputDCSerializer = new DataContractSerializer(typeof(OutputDataContractType)); } #endregion #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) { try { MapInfo mapInfo = GetMapInfo(typeof(TransformType).FullName, 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("XslCompiledTransformHelper", ex); TraceHelper.WriteLineIf(debug, 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)) { TransformType transformBase = Activator.CreateInstance<TransformType>(); 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; } private static Stream Serialize(InputDataContractType input) { VirtualStream stream = new VirtualStream(); if (inputDCSerializer != null) { inputDCSerializer.WriteObject(stream, input); } else { 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); } else 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 ((customAttributes != null) && (customAttributes.Length > 0)) { return true; } if (type.IsArray) { type = type.GetElementType(); } else { type = null; } } while (type != null); return false; } #endregion } For usage, see holsson.spaces.live.com/.../cns!91D78390ECE48C0D!913.entry.Anonymous
June 14, 2010
The comment has been removedAnonymous
July 21, 2010
Well done Paolo - great demo today!Anonymous
February 22, 2011
Your solution is also quite useful for dynamic message mapping. But I’m wondering whether this solution will affect the performance of large message transforming. As we know we can set the message size threshold at this location "HKLMSoftwareMicrosoftBizTalk Server3.0AdministrationTransformThreshold". It will improve the throughput for large message processing when buffering large messages to the file system during mapping. will the large message be buffered to file system if we use your solution? Thanks.Anonymous
February 22, 2011
The comment has been removedAnonymous
June 20, 2011
Does BizTalk 2010 use the XslCompiledTransform class internally for transformations or does it still use the old XslTransform? thanks!Anonymous
June 20, 2011
Hi Heidi, as far as I know, unfortunately BizTalk Server 2010 still uses the old XslTransform. :-( Ciao, Paolo