Recently I had chance to work with a couple of customers that make extensive use the ESB Toolkit (specifically, V 2.0 in this case) in their BizTalk solutions, and to exchange ideas with some of you regarding the alleged performance problems affecting Dynamic Send Ports. In the last few weeks I received comments like the following:
Therefore, I decided to take the plunge, put on my (Lego) Indiana Jones hat, put my favorite gadgets (Reflector Pro, Visual Studio Profiler, SQL Server Profiler, etc.) in my backpack and enter the forest of the ESB Toolkit to make my own investigations. This article is quite long, so I decided to break it down in 3 parts in the following way:
The use of Dynamic Send Ports confers great flexibility to any BizTalk applications, not only to those using the ESB Toolkit, because they provide the ability to dynamically choose within a Receive Location or an Orchestration, the proper Adapter to use, set its context properties, and finally specify the target URL. However, they also present the following challenges:
MessageOut=MessageIn; MessageOut(WCF.BindingType)="customBinding"; MessageOut(WCF.Action)="http://tempuri.org/IReceiveMessage/ReceiveMessage"; MessageOut(WCF.BindingConfiguration)=@"<binding name=""customBinding""><binaryMessageEncoding /><tcpTransport /></binding>"; DynamicSendPort(Microsoft.XLANGs.BaseTypes.Address)="net.tcp://localhost:8001/customNetTcp"; DynamicSendPort(Microsoft.XLANGs.BaseTypes.TransportType)="WCF-Custom";
Each individual Dynamic Send Port has unique Activation Subscriptions for each Adapter installed in the BizTalk environment, as shown in the following figure. To a certain extent, this can increase the cost of subscription matching at runtime.
The ESB Toolkit encourages and promotes the use of Dynamic Send Ports. When you create an Itinerary and in particular when you configure an Off-Ramp Service, the designer allows you to select one of the Dynamic Send Ports available within the selected BizTalk application, while Static Send Ports are not supported. In other words, by default, you cannot use the Microsoft.Practices.ESB.Services.Routing service provided out of the box by the ESB Toolkit with Static Send Ports. However, there’s an easy and obvious workaround for this problem. To use a Static Send Port in place of a Dynamic Send Port you can proceed as follows:
For more information on this topic, you can also review the following blog posts:
A drawback of this technique is that it requires the creation of a placeholder Dynamic Send Port as well as a corresponding Static Send Port for each remote endpoint. So I asked myself the following question: is there a better way to use Static Ports with the ESB Toolkit? Can I avoid creating several Static Ports, one for each target endpoint or system? The answer is yes and the effort to accomplish this goal is minimal.
I started noting that in the majority of BizTalk applications, Dynamic Send Ports are exploited just to change the target URL, that is the BTS.OutboundTransportLocation context property, within a pipeline component in a Receive Location or inside an Orchestration, while the Adapter does not vary. This task can be easily accomplished using the Routing Service (Messaging or Orchestration), and one of the Resolvers provided out-of-the-box by the ESB Toolkit (UDDI, BRE, STATIC, XPATH, etc.). So I asked myself the following question: can I define a Static Send Port and use it to exchange data with multiple, distinct target systems just by changing the context properties, like the aforementioned BTS.OutboundTransportLocation, before transmitting outbound messages?
When you create a Static Send Port you have to specify, in a declarative way at configuration time, the target URL and Adapter-specific properties (for example, the Action property, when defining a WCF Send Port). At runtime, if you try to dynamically change, within a Receive Location or an Orchestration, the value of the corresponding context properties like the BTS.OutboundTransportLocation or the WCF.Action, the Static Send Port will ignore these values and continue to use the data statically specified as part of the Port definition. However, this rule applies only if you try to change general and Adapter-specific context properties before publishing the message to the MessageBox. In fact, if you try to change the value of context properties like BTS.OutboundTransportLocation and WCF.Action within a pipeline component in the transmit pipeline used by the Static Send Port, the Adapter Send Handler will ignore the values configured on the Static Port and will use those specified by the pipeline component.
To confirm my theory, I made a sample in which a custom pipeline component dynamically changes the target URL (from the URL defined during configuration) on a FILE Send Port by assigning a new value to the BTS.OutboundTransportLocation context property within the send pipeline used by the Port.
If you look at the samples shipped with the ESB Toolkit or published by bloggers on the Internet, that make use of the Routing Messaging Service, you can easily note that the value of the Container property of this latter service is always equal to OnRamp.receiveInbound, as shown in the picture below.
This means that the service in question will be executed within a Receive Location configured to use one of the receive pipelines provided by the ESB Toolkit or a custom receive pipeline that contains the ESB Dispatcher component. When using the Routing or Transform Orchestration service provided out-of-the-box by the ESB Toolkit or a custom Orchestration Service, you can retrieve routing data using a Resolver defined on the service itself or on the Off-Ramp Extender, as shown in the following figure.
In any case when you advance the Itinerary to the next Itinerary Step, the Resolver will be executed and the corresponding data used to initialize routing-related context properties (BTS.OutboundTransportLocation and BTS.OutboundTransportType). In both cases, routing-related context properties will be set before posting the message to the MessageBox. Now, as we noted before, this pattern permits routing the message to a Dynamic Send Port and initializes the target URL, but it does not allow overriding the URL statically defined on a Static Send Port. Therefore, I tried to use a different approach and I moved both the Transform Service and the Routing Service on the Off-Ramp Send Port by selecting OffRamp.sendTransmit as the value for their Container property (see the picture below):
As described in the first part of this article, I used a placeholder Dynamic Send Port to define the Off-Ramp Service within my Itinerary, but then I replaced this with the real one-way FILE Static Send Port that uses the same filter expression; the STATIC Resolver defined on the Routing Service (see the figure below) has been configured to write the incoming message to a folder other than that used as a target by the above FILE Send Port:
To test my Itinerary I built the use case represented by the following picture:
Message Flow:
Everything was in place, I was ready to go, I gave it a try, but it didn’t work as expected : in fact, the message was copied in the folder referenced by the Static Send Port rather than in the folder indicated by the STATIC Router used by the Routing Service. Therefore, I decided to start using my favorite tool (.NET Reflector PRO) to investigate. Analyzing and debugging through the code, I finally found the culprit line of code: the Execute method exposed by the Microsoft.Practices.ESB.Itinerary.Services.RoutingService class contains the following code:
public IBaseMessage Execute(IPipelineContext context, IBaseMessage msg, string resolverString, IItineraryStep step) { // only end point resolution if this is a Receive Inbound if (step.MessageDirection == ItineraryMessageDirection.ReceiveInbound || step.MessageDirection == ItineraryMessageDirection.SendInbound) { return ExecuteRoute(context, msg, resolverString); } return msg; }
In practice, when running within a send pipeline, the Microsoft.Practices.ESB.Itinerary.Services.RoutingService provided out-of-the-box by the ESB Toolkit just returns the incoming message without performing any . Therefore I decided to create my own version of this component: using .NET Reflector PRO I disassembled and copied the code of the original class and I created a new custom class called Microsoft.BizTalk.CAT.ESB.Itinerary.Services.RoutingService where I removed the if condition from the Execute method as shown in the picture below.
public IBaseMessage Execute(IPipelineContext context, IBaseMessage message, string resolverString, IItineraryStep step) { return ExecuteRoute(context, message, resolverString); }
Then I installed my component in the GAC and registered it as a service in the ESB Toolkit configuration file (esb.config). Finally I replaced the original RoutingService in my Itinerary with the new custom service. At this point I crossed my fingers and ran a new test: this time the use case behaved as expected and the FILE Adapter wrote the message in the path specified by the STATIC Resolver within the Itinerary.
Note This technique, consisting in changing the value of the OutboundTransportLocation property within a Static Send Port, is not guaranteed to work with all the BizTalk Adapters: for example, I tried to use my custom Routing Service with an MQSeries Static Send Port, but it didn't work as expected. In other words, the MQSeries Adapter ignores the value set by the Routing Service within the ItinerarySendPassthrough pipeline and continues to use the URL statically defined in the Queue Definition property inside the Adapter configuration.
The next step was to review the code of the default TransformService class to see if it could be optimized for best performance. This brings us to the next point.
Once again, I used .NET Reflector to disassemble and inspect the code of the TransformService class contained in the Microsoft.Practices.ESB.Itinerary.Services assembly. For you convenience, I reported below just the code of the method TransformStream that is responsible for applying a transformation map to the stream containing the message.
private static Stream TransformStream(Stream stream, string mapName, bool validate, ref string messageType) { Type mapType = Type.GetType(mapName); if (null == mapType) throw new TransformationServiceException(Properties.Resources.InvalidMapType, mapName); TransformMetaData metaData = TransformMetaData.For(mapType); SchemaMetadata schema = metaData.SourceSchemas[0]; String sourceMap = schema.SchemaName; SchemaMetadata targetSchema = metaData.TargetSchemas[0]; if (validate) { if (string.Compare(messageType, sourceMap, false, CultureInfo.CurrentCulture) != 0) throw new TransformationServiceException(Properties.Resources.SourceDocumentMismatch, messageType, sourceMap); } messageType = targetSchema.SchemaName; XPathDocument doc = new XPathDocument(stream); XslTransform transform = metaData.Transform; Stream output = new MemoryStream(); transform.Transform(doc, metaData.ArgumentList, output); output.Flush(); output.Seek(0, SeekOrigin.Begin); return output; }
As you can easily note, the method starts using the fully qualified name of the map contained in the mapName parameter to retrieve the type of the map, then it uses this object to retrieve the map metadata and finally applies the map to the message contained in the stream parameter using a XslTransform object. Reviewing the code above I identified 2 potential optimizations:
As you probably know, the BizTalk Runtime still makes an extensive use of the System.Xml.Xsl.XslTransform: for instance, 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.
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 the 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:
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 BizTalk Development Team to see how best to take advantage of this class in the next version of BizTalk, some months ago I decided to create a helper class that, exploiting the capabilities provided by the XslCompiledTransform class, can significantly boost the performance of transformations in BizTalk application. For more information, you can read my previous posts on this subject:
Taking into account all these considerations, I decided to create a new, faster version of the Transform Service. Once again I started from the code of the original TransformService class, but this time I radically changed its code to cache map metadata and exploit the capabilities provided by the XslCompiledTransform class. For your convenience, I reported below the code of the custom TransformService class and a customized version of my XslCompiledTransformHelper class. In particular, the following 2 optimizations have been implemented in the TransformStream method:
TransformService Class
#region Copyright //------------------------------------------------- // Author: Paolo Salvatori // Email: paolos@microsoft.com // History: 2010-06-15 Created //------------------------------------------------- #endregion #region Using Directives using System; using System.IO; using System.Reflection; using System.Xml.XPath; using System.Xml.Xsl; using System.Configuration; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using Microsoft.BizTalk.Component.Interop; using Microsoft.BizTalk.Message.Interop; using Microsoft.BizTalk.Streaming; using Microsoft.Practices.ESB.Adapter; using Microsoft.Practices.ESB.Itinerary; using Microsoft.Practices.ESB.Exception.Management; using Microsoft.Practices.ESB.GlobalPropertyContext; using Microsoft.Practices.ESB.Resolver; using Microsoft.Practices.ESB.Utilities; using Microsoft.XLANGs.RuntimeTypes; using Microsoft.BizTalk.CAT.ESB.Itinerary.Services.Properties; #endregion namespace Microsoft.BizTalk.CAT.ESB.Itinerary.Services { /// <summary> /// Service responsible for setting up the message for transformation and executing the transformation. /// </summary> public class TransformationService : IMessagingService { #region Private Constants private const string ValidateSourceKey = "ValidateSource"; private const string BufferSize = "bufferSize"; private const string ThresholdSize = "thresholdSize"; #endregion #region Private Static Fields private static Dictionary<string, TypeInfo> typeInfoCollection = new Dictionary<string, TypeInfo>(); private static int bufferSize = XslCompiledTransformHelper.DefaultBufferSize; private static int thresholdSize = XslCompiledTransformHelper.DefaultThresholdSize; #endregion #region Static Constructor static TransformationService() { try { string setting = null; setting = ConfigurationManager.AppSettings[BufferSize]; if (!string.IsNullOrEmpty(setting)) { if (!int.TryParse(setting, out bufferSize)) { bufferSize = XslCompiledTransformHelper.DefaultBufferSize; } } setting = ConfigurationManager.AppSettings[ThresholdSize]; if (!string.IsNullOrEmpty(setting)) { if (!int.TryParse(setting, out thresholdSize)) { thresholdSize = XslCompiledTransformHelper.DefaultThresholdSize; } } } catch (Exception) { } } #endregion #region IMessagingService Members /// <summary> /// Name of the service. /// </summary> /// <remarks> /// This name must match the configured name in Itinerary services configuration. /// </remarks> [Browsable(true)] [Description("Name of the service. This name must match the configured name in Itinerary services configuration.")] [ReadOnly(true)] public string Name { get { return Resources.TransformServiceName; } } /// <summary> /// The routing service does not support disassemble and execution of multiple resolvers. /// </summary> public bool SupportsDisassemble { get { return false; } } /// <summary> /// Determines if the step should be advanced. /// </summary> /// <param name="step">Current step information from the itinerary.</param> /// <param name="msg">Current message in pipeline.</param> /// <returns>True</returns> public bool ShouldAdvanceStep(IItineraryStep step, IBaseMessage msg) { return true; } /// <summary> /// Execute a transformation of a message within the lifecyle of an itinerary. /// </summary> /// <param name="step">Current step in the itinerary</param> /// <param name="context">Pipeline context information</param> /// <param name="message">Message coming through the pipeline</param> /// <param name=Resources.ResolverString>Resolver string that needs to be resolved by the appropriate resolver.</param> /// <returns>The transformed message.</returns> public IBaseMessage Execute(IPipelineContext context, IBaseMessage message, string resolverString, IItineraryStep step) { if (context == null) { throw new ArgumentNullException(Resources.ContextParameter, Resources.ContextCannotBeNull); } if (message == null) { throw new ArgumentNullException(Resources.MessageParameter, Resources.MessageCannotBeNull); } if (string.IsNullOrEmpty(resolverString)) { throw new ArgumentException(Resources.ArgumentStringRequired, Resources.ResolverString); } try { bool validateSource = false; if (step != null && step.PropertyBag.ContainsKey(ValidateSourceKey)) { validateSource = string.Compare(step.PropertyBag[ValidateSourceKey], "true", true, CultureInfo.CurrentCulture) == 0; } return ExecuteTransform(context, message, resolverString, validateSource); } catch (Exception ex) { EventLogger.Write(MethodInfo.GetCurrentMethod(), ex); throw; } } /// <summary> /// Execute a transformation of a message. This operation is generally used outside of an itinerary. /// </summary> /// <param name="context">Pipeline context information</param> /// <param name="message">Message coming through the pipeline</param> /// <param name="mapNameOrResolverString">Map name including assembly details or resolver string to get the map name.</param> /// <param name="validateSource">Determine if source message type is validated against map source message type.</param> /// <returns>The transformed message</returns> public IBaseMessage ExecuteTransform(IPipelineContext context, IBaseMessage message, string mapNameOrResolverString, bool validateSource) { if (context == null) { throw new ArgumentNullException(Resources.ContextParameter, Resources.ContextCannotBeNull); } if (message == null) { throw new ArgumentNullException(Resources.MessageParameter, Resources.MessageCannotBeNull); } if (string.IsNullOrEmpty(mapNameOrResolverString)) { throw new TransformationServiceException(Resources.MapNameRequired); } try { string mapName = string.Empty; ResolverInfo info = ResolverMgr.GetResolverInfo(ResolutionType.Transform, mapNameOrResolverString); if (info.Success) { Dictionary<string, string> resolverDictionary = ResolverMgr.Resolve(info, message, context); mapName = resolverDictionary["Resolver.TransformType"]; } else { mapName = mapNameOrResolverString; } if (string.IsNullOrEmpty(mapName)) { throw new TransformationServiceException(Resources.MapNameRequired); } Stream stream = message.BodyPart.GetOriginalDataStream(); // If not seekable, make it so if (!stream.CanSeek) { // Create a virtual and seekable stream Stream virtualStream = new VirtualStream(bufferSize, thresholdSize); Stream seekableStream = new ReadOnlySeekableStream(stream, virtualStream, bufferSize); stream = seekableStream; } stream.Position = 0; string messageType = message.Context.Read(BtsProperties.MessageType.Name, BtsProperties.MessageType.Namespace) as string; if (string.IsNullOrEmpty(messageType)) { messageType = MessageHelper.GetMessageType(message, context); } IBaseMessage outMsg = MessageHelper.CreateNewMessage(context, message, TransformStream(stream, mapName, validateSource, (int)bufferSize, (int)thresholdSize, ref messageType)); outMsg.Context.Write(BtsProperties.SchemaStrongName.Name, BtsProperties.SchemaStrongName.Namespace, null); outMsg.Context.Write(BtsProperties.MessageType.Name, BtsProperties.MessageType.Namespace, messageType); MessageHelper.SetDocProperties(context, outMsg); return outMsg; } catch (Exception ex) { EventLogger.Write(MethodInfo.GetCurrentMethod(), ex); throw; } } #endregion #region Private Static Methods private static Stream TransformStream(Stream stream, string mapName, bool validate, int bufferSize, int thresholdSize, ref string messageType) { TypeInfo typeInfo = null; lock (typeInfoCollection) { if (typeInfoCollection.ContainsKey(mapName)) { typeInfo = typeInfoCollection[mapName]; } else { typeInfo = new TypeInfo(); Type mapType = Type.GetType(mapName); if (null == mapType) { throw new TransformationServiceException(Resources.InvalidMapType, mapName); } TransformMetaData transformMetaData = TransformMetaData.For(mapType); if (transformMetaData.SourceSchemas.Length > 0 && transformMetaData.SourceSchemas[0] != null && !string.IsNullOrEmpty(transformMetaData.SourceSchemas[0].SchemaName)) { typeInfo.SourceSchemaName = transformMetaData.SourceSchemas[0].SchemaName; } if (transformMetaData.TargetSchemas.Length > 0 && transformMetaData.TargetSchemas[0] != null && !string.IsNullOrEmpty(transformMetaData.TargetSchemas[0].SchemaName)) { typeInfo.TargetSchemaName = transformMetaData.TargetSchemas[0].SchemaName; } typeInfoCollection.Add(mapName, typeInfo); } } if (validate) { if (string.Compare(messageType, typeInfo.SourceSchemaName, false, CultureInfo.CurrentCulture) != 0) { throw new TransformationServiceException(Resources.SourceDocumentMismatch, messageType, typeInfo.SourceSchemaName); } } messageType = typeInfo.TargetSchemaName; return XslCompiledTransformHelper.Transform(stream, mapName, true, bufferSize, thresholdSize); } #endregion } public class TypeInfo { #region Private Fields private string sourceSchemaName = null; private string targetSchemaName; #endregion #region Public Constructors public TypeInfo() { } public TypeInfo(string sourceSchemaName, string targetSchemaName) { this.sourceSchemaName = sourceSchemaName; this.targetSchemaName = targetSchemaName; } #endregion #region Public Properties public string SourceSchemaName { get { return this.sourceSchemaName; } set { this.sourceSchemaName = value; } } public string TargetSchemaName { get { return this.targetSchemaName; } set { this.targetSchemaName = value; } } #endregion } }
#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 System.Reflection; using Microsoft.XLANGs.BaseTypes; using Microsoft.XLANGs.Core; using Microsoft.BizTalk.Streaming; using Microsoft.Practices.ESB.Exception.Management; using Microsoft.BizTalk.CAT.ESB.Itinerary.Services.Properties; #endregion namespace Microsoft.BizTalk.CAT.ESB.Itinerary.Services { public class XslCompiledTransformHelper { #region Internal Constants internal const int DefaultBufferSize = 10240; //10 KB internal const int DefaultThresholdSize = 1048576; //1 MB internal 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, true, DefaultBufferSize, DefaultThresholdSize); } public static XLANGMessage Transform(XLANGMessage message, string mapFullyQualifiedName, string messageName, bool seekOutboundStream) { return Transform(message, 0, mapFullyQualifiedName, messageName, DefaultPartName, seekOutboundStream, DefaultBufferSize, DefaultThresholdSize); } public static XLANGMessage Transform(XLANGMessage message, int partIndex, string mapFullyQualifiedName, string messageName, string partName, bool seekOutboundStream, int bufferSize, int thresholdSize) { try { using (Stream stream = message[partIndex].RetrieveAs(typeof(Stream)) as Stream) { Stream response = Transform(stream, mapFullyQualifiedName, seekOutboundStream, 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) { EventLogger.Write(MethodInfo.GetCurrentMethod(), ex); throw; } finally { if (message != null) { message.Dispose(); } } } public static XLANGMessage Transform(XLANGMessage[] messageArray, int[] partIndexArray, string mapFullyQualifiedName, string messageName, string partName, bool seekOutboundStream, 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, seekOutboundStream, 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) { EventLogger.Write(MethodInfo.GetCurrentMethod(), ex); 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, true, DefaultBufferSize, DefaultThresholdSize); } public static Stream Transform(Stream stream, string mapFullyQualifiedName, bool seekOutboundStream) { return Transform(stream, mapFullyQualifiedName, seekOutboundStream, DefaultBufferSize, DefaultThresholdSize); } public static Stream Transform(Stream stream, string mapFullyQualifiedName, bool seekOutboundStream, int bufferSize, int thresholdSize) { return Transform(stream, new VirtualStream(bufferSize, thresholdSize), mapFullyQualifiedName, seekOutboundStream, bufferSize, thresholdSize); } public static Stream Transform(Stream inboundStream, Stream outboundStream, string mapFullyQualifiedName, bool seekOutboundStream, int bufferSize, int thresholdSize) { try { MapInfo mapInfo = GetMapInfo(mapFullyQualifiedName); if (mapInfo != null) { XmlTextReader xmlTextReader = null; try { xmlTextReader = new XmlTextReader(inboundStream); mapInfo.Xsl.Transform(xmlTextReader, mapInfo.Arguments, outboundStream); if (seekOutboundStream) { outboundStream.Seek(0, SeekOrigin.Begin); } return outboundStream; } finally { if (xmlTextReader != null) { xmlTextReader.Close(); } } } } catch (Exception ex) { EventLogger.Write(MethodInfo.GetCurrentMethod(), ex); throw; } return null; } public static Stream Transform(Stream[] streamArray, string mapFullyQualifiedName) { return Transform(streamArray, mapFullyQualifiedName, true, DefaultBufferSize, DefaultThresholdSize); } public static Stream Transform(Stream[] streamArray, string mapFullyQualifiedName, bool seekOutboundStream) { return Transform(streamArray, mapFullyQualifiedName, seekOutboundStream, DefaultBufferSize, DefaultThresholdSize); } public static Stream Transform(Stream[] streamArray, string mapFullyQualifiedName, bool seekOutboundStream, int bufferSize, int thresholdSize) { try { MapInfo mapInfo = GetMapInfo(mapFullyQualifiedName); 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); if (seekOutboundStream) { virtualStream.Seek(0, SeekOrigin.Begin); } return virtualStream; } finally { if (compositeStream != null) { compositeStream.Close(); } } } } catch (Exception ex) { EventLogger.Write(MethodInfo.GetCurrentMethod(), ex); throw; } return null; } #endregion #region Private Static Methods private static MapInfo GetMapInfo(string mapFullyQualifiedName) { 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(); 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 } }
In the next episode of the series we will walk through a variety of test scenarios that I used to ensure everything is working as desired, and that also help characterize the performance of the implementation. Stay tuned for Part 2!
You can immediately download the code of the custom ESB services and test cases here. As always, you are kindly invited to provide feedbacks and comments.
gr8 artical ...
i am new to BizTalk ESB toolkit, Can you please provide me basic demo using ESB Toolkit 2.0
it will be very helpful to start with ESB toolkit 2.0
Hi Chetan,
you can review the following links:
- BizTalk ESB Toolkit Sample Applications at msdn.microsoft.com/.../ee236708(BTS.10).aspx.
- The Videos section on What Is an Enterprise Service Bus? at msdn.microsoft.com/.../dd876606.aspx
- ESB Toolkit Extensions at esbextlibrary.codeplex.com
- Sample ESB Itinerary Services at esbservices.codeplex.com
Let me know if I answered your question!
Ciao,
Paolo