BizTalk presentations in the summer/fall, part two

I case anyone has missed it, BizTalk User Group Sweden, is as active as ever. We’ve got two great events planned after the summer.

Date: 26/8 2010 – Evening event.
Location: Stockholm
Topic: Managing your BizTalk environment using System Center Operations Manager 2007 R2
Speaker: Kent Weare
Sign-up: Here

This is a great event not only for BizTalk Developers and Operations but also for IT-professionals working with System Center, especially if they have BizTalk in their environment.

Date: 8-9/9 2010 – Yes, that’s right, this is a TWO DAY event.
Location: Stockholm
Topic: Applied Architecture Patterns on the Microsoft Platform aka BizTalk Server 2010 – Release party
Speaker: Richard Seroter, Ewan Fairweather, Stephen W. Thomas
Sign-up: Here

These are both events you do not wan’t to miss, and it seems people agree. Slots are filling up fast so be sure to be quick about signing up.

BizTalk presentations during the summer, part one

I’ll be presenting at Microsoft Swedens “Sommarkollo”. I’ll be talking about BizTalk. Introducing it, showing of enhancements in 2010 and going through some (what I hope are) inspirational scenarios. The target audience are developers or architects. Even though I will spend some time in the beginning at an introductory level I believe there are things that will entertain even more seasoned developers. Level is 200-300.

More info about this and other presentations can be found at http://www.microsoft.com/sverige/sommarkollo/default.html

Also, signups for my presentations can be reached through the below links:

Date: 30/6 kl. 13-16
Location: G%u00f6teborg
Sign-up: Here

Datum: 1/7 kl. 9-12
Location: Malm%u00f6.
Sign-up: Here

Datum: 7/7 kl. 9-12
Location: Stockholm
Sign-up: Here

Datum: 24/8 kl. 9-12
Location: Stockholm
Sign-up: Here

Enjoy the summer! I know I will 🙂

PS. Just to make readers (especially those from other countries with less generous terms) jealous I’ll be on vacation/parental leave (and have been for two weeks) until the very last days of august.

Large Message Transfer with WCF-Adapters Part 1

Introduction

This is the first in a series of articles that will introduce and explore a couple a patterns to transfer and process large messages using WCF-Adapters. Generally speaking, an end to end BizTalk solution adopts the FTP Adapter or one of its variations (SFTP/FTPS) when it needs to exchange large messages with external parties over the internet, while it uses the FILE adapter for moving large batch files over the intranet. Large input files can be debatched by a receive pipeline into smaller discrete messages. This technique removes the need to worry about a large message and enables processing to be parallelized. The main recommendation to bear in mind when dealing with large messages might seem obvious, but it is often overlooked by developers: you must avoid any operations that require the entire message to be loaded into memory and adopt a streaming approach, especially in those cases where the application can concurrently receive and process multiple large messages in the same time. BizTalk itself adopts a streaming approach whereby the message is read through the use of a stream, meaning that the entire message doesn’t have to be present in memory at any point in time. This streaming approach is used by all the built-in pipeline components such as the Flat File Disassembler, XML Disassembler, etc. Therefore, it’s extremely important that you adopt the same approach when designing and developing custom pipeline components, orchestrations or helper classes for processing large messages.

The problems associated with large messages can be divided into the following categories:

  • Out of memory errors: Certain types of message processing can require the entire message to be loaded into memory. BizTalk Server operates a streaming approach whereby the message is read through the use of a stream, meaning that the entire message doesn’t have to be present in memory at any point in time. For example, BizTalk Server 2006 introduced the VirtualStream class in the Messaging Runtime and if the message size is over a certain, configurable threshold (1MB is the default), the document is written to a temporary file, thus keeping the memory usage flat during mapping. Of course, this swapping incurs performance overhead, but this prevents the host process from running out of memory. Any custom pipeline components, orchestrations, or helper classes should follow the same streaming approach and avoid using an XmlDocument to load the entire message into memory.
  • Performance problems for messages that are not loaded into memory: Messages that are not required to be loaded into memory are streamed to the MessageBox database using an .NET XmlReader object. While they are not subject to any size limitations, there are some important factors that impact how BizTalk Server processes messages that are streamed to the MessageBox database.

The original message size, message format, and type of message processing are the main factors that affect how BizTalk Server processes large messages.

  • Original message size: The size of the message received by BizTalk Server is the most visible indication of how large the message will be when it is processed by BizTalk Server. The original size of a message has a much greater impact on performance if the entire message is loaded into memory than if the message is streamed to the MessageBox database.
  • Message format: Messages are received into BizTalk Server in one of two primary formats: XML files or flat files.
    • XML files: In order for BizTalk Server to perform any processing on a message other than pass through routing, the message must be in the XML file format. If the files to be processed are received in XML format then they will retain their original size for the most part.
    • Flat files: Flat files must be parsed into an XML format before BizTalk Server can perform any processing (for instance message transformation) other than pass through routing. Parsing a flat file into an XML file can significantly increase the size of the file. Flat files do not contain XML tags with descriptive information about their data. By contrast, XML files wrap all of their data in descriptive XML tags. In some scenarios parsing can increase the size of a flat file by a factor of 10 or more, depending on how much descriptive data is contained in the XML tags for the file.
  • Type of message processing: In BizTalk Server, there are two types of message processing: Routing only and Mapping. The performance variables tied to the type of message processing that is performed are message size and whether the message is loaded into memory.
    • Routing: If BizTalk Server is only used only for routing messages based upon promoted properties, then the Message Agent uses the stream opened by the receive adapter to publish the message to the BizTalkMsgBoxDb database using the .NET XmlReader interface, and message parts are not individually loaded into memory. In this scenario, out of memory errors are not an issue and the primary consideration is the amount of time that is required to write very large messages (over 100 MB) into the BizTalkMsgBoxDb database. In fact, this operation can have a severe impact on overall performance, especially when the same BizTalk installation is used to host also low latency applications. The BizTalk Server development team has successfully tested the processing of messages up to 1 GB in size when performing routing only.
    • Mapping: Transforming a document with a map is a memory-intensive operation. When a document is transformed by a map, BizTalk Server passes the message stream to the .Net XslTransform class, which then loads the document into a .NET XPathDocument object for processing. Loading the document into the .NET XPathDocument can potentially expand the original file size in memory by a factor of 10 or more. This expansion may be more pronounced when mapping flat files because flat files must be parsed into XML before they can be transformed. Almost 4 months ago I wrote an article on how boosting message transformation using the XslCompiledTransform class in place of the XslTransform.

For more information about large message transfer and processing with BizTalk Server, you can read the following topics:

Problem Statement

In this article we’ll try to provide an answer to the following questions:

  • How can I use BizTalk and WCF to transfer a large message in a reliable and performant way?
  • What is the best Message Encoder (Binary, MTOM, Text) and Transfer Mode (Buffered, Streamed) that I can use to transmit a large message to a WCF Receive Location?

Besides, in this series of articles I’ll discuss the following topics:

  • How to use the InboundHeaders context property exposed by WCF Adapters to access the SOAP headers of incoming WCF messages.
  • How to use the TransferMode = Streamed.
  • How to process in a streaming fashion within a pipeline component.
  • How to transfer a large message in a transactional way using a client-side initiated transaction.
  • How to stage the incoming document to a folder or SQL.
  • How to use a table with a column of type “varbinary(max) FILESTREAM” to stage a large file on SQL Server 2008.
  • How to use the API of the Transaction NTFS (TxF) to write a large file to a shared folder in reliable, transactional way.
  • How to hook the distributed transaction used by the Message Agent within a pipeline component.
  • How to use a custom stream on the send port to wrap the original stream and execute closing tasks after message transmission.
  • How to initiate delivery notifications within a pipeline component and handle ACK/NACK messages within an orchestration.

WCF Message Encoders and Transfer Modes

When I started to design this demo, my will was comparing the performance of the various message encoders and transfers modes provided out of the box by WCF and find the best combination to adopt when transferring large message over the network. WCF includes three types of encoding for SOAP messages:

  • Text Message Encoding: The text encoding represented by the TextMessageEncodingBindingElement is the most interoperable, but the least efficient encoder for XML messages. Web service or Web service client can generally understand textual XML. However, transmitting large blocks of binary data as text is not efficient.
  • Binary Message Encoding: The BinaryMessageEncodingBindingElement is the binding element that specified the .NET Binary Format for XML should be used for encoding messages, and has options to specify the character encoding and the SOAP and WS-Addressing version to be used. Binary encoding is most efficient but least interoperable of the encoding options.
  • MTOM Message Encoding: The MTOMMessageEncodingBindingElement represents the binding element that specifies the character encoding and message versioning and other settings used for messages using a Message Transmission Optimization Mechanism (MTOM) encoding. (MTOM) is an efficient technology for transmitting binary data in WCF messages. The MTOM encoder attempts to create a balance between efficiency and interoperability. The MTOM encoding transmits most XML in textual form, but optimizes large blocks of binary data by transmitting them as-is, without conversion to their base64 encoded format.

WCF transport channels support two modes for transferring messages in each direction:

  • Buffered: transmissions hold the entire message in a memory buffer until the transfer is complete. On the service side message processing cannot start until the entire message has been received.
  • Streamed: transmissions only buffer the message headers and expose the message body as a stream, from which smaller portions can be read at a time.

The TransferMode property exposed by transport protocol channel (e.g.HttpTransportBindingElement, TcpTransportBindingElement, etc.) and bindings (BasicHttpBinding, NetTcpBinding, etc.) allows to indicate whether messages are sent buffered or streamed. Streamed transfers can improve the scalability of a service by eliminating the need for large memory buffers. Whether changing the transfer mode actually improves scalability in practice depends on the size of the messages being transferred. Improvements in scalability should be most evident when large messages use streamed instead of buffered transfers. Therefore we’ll exploit the streamed transfer mode in for transferring large messages using BizTalk and WCF Adapters.
Note: By default, the HTTP, TCP/IP and Named Pipe transports use buffered message transfers. If you plan to exploit the streamed transfer mode when receiving a large message through a WCF Receive Location, you have to use the WCF-Custom/WCF-CustomIsolated Adapter. In fact, these latter allow developer to select  a certain binding (e.g. BasicHttpBinding, NetTcpBinding, NetNamedPipeBinding, etc.) and then change the value of its TransferMode property to Streamed. The other WCF Adapters (e.g. WCF-BasicHttp, WCF-NetTcp, etc.) do not provide this possibility.

For more information on the WCF Transfer Modes, you can read the following topic:

  • “TransferMode Enumeration” on MSDN.

Client Application

I created a WinForm application that is capable to upload multiple documents at the same time. The UI of the driver application allows to select a list of files to be sent in parallel to BizTalk and which service endpoint to use for this purpose. Six different WCF Receive Locations have been created, one for each possible combination of message encoders and transfer modes. Therefore, the configuration file of the client application contains a different service endpoint, one for each WCF Receive Location. When you push the Submit button on the main form, the click event handler method (btnSubmit_Click) creates a a list of calls and then using the ThreadPool it start an asynchronous thread for each file selected on the UI. At this point, if the selected service endpoint uses a transaction-enabled binding, the application will call the SubmitFileWithTxn method, otherwise the SubmitFileWithoutTxn method.

The code of the 2 functions is quite similar, the only difference between the two is that the SubmitFileWithTxn method initiates a transaction that will be flowed to the WCF Receive Location. Let’s analyze the code of this latter method. The SubmitFileWithTxn starts opening a FileStream to read the selected file from the disk and I wrap this latter with a custom stream called ProgressStream. As the name suggests, the main scope of this custom stream is to update a progress bar on the UI as the file is being transmitted. This feature is particularly interesting when we use a binding which TransferMode is equal to Streamed. The ProgressStream class exposes an event called ProgressChanged  that can be used to invoke a method that updates the progress bar. Since the progress bar control is owned by the main thread, while the application uses a ThreadPool worker thread to transmit the message to BizTalk, in order to change the value of the progress bar is necessary to call the Invoke method exposed by the ProgressForm object with a delegate. This technique enables the worker thread to ask the main thread to execute the delegate.
Then, the method creates an UploadMessage. If you look at the code of the class, you will note that I decorated the class with the MessageContractAttribute. Then I decorated all the properties representing transmission metadata like the name of the sender or the file size with the MessageHeaderAttribute attribute. This way I declare that this data will be transmitted as custom headers in the SOAP envelope.
Instead, the property called Data that will contain the content of the source file stream has been decorated with the MessageBodyMember attribute. Hence data will be transmitted within the body of the SOAP envelope.
Finally, the method creates a ChannelFactory using the IUploadMessageTxn interface as service contract. If you look at the code, you will note that this interface has been decorated with the TransactionFlowAttribute. In particular, the TransactionFlowOption.Mandatory value indicates that the service endpoint requires the client application to start and flow a distributed transaction.
For this reason, the SubmitFileWithTxn method invoke the underlying  WCF Receive Location within a transaction scope. The driver application allows to control the outcome of the transaction selecting one between the Commit or Abort option buttons on the UI. Selecting abort, you can simulate a transaction failure. This possibility is extremely useful when you configure the WCF receive location to use a transactional binding. In fact, in this case the Message Agent will use the transaction initiated by the client application to post a placeholder document to the BizTalkMsgBoxDb (as you will see in the remainder of the article, the original message will be staged in a folder or SQL database and replaced by a placeholder document which contains its location and metadata). Therefore, if the client application aborts the transaction, no message will be published to the BizTalkMsgBoxDb. Transferring a large message in the context of WS-AT or OleTransactions transaction greatly improves transmission reliability, but it has a severe impact on overall performance, so this technique is recommended only in those cases where resilience is the primary concern.
For sake of readability, I omitted most of the code and I reported below only the classes and methods that can be helpful to understand the application.

MainForm class

#region Copyright
//-------------------------------------------------
// Author:  Paolo Salvatori
// Email:   [email protected]
// History: 2009-01-14 Created
//-------------------------------------------------
#endregion

#region Using Directives
using System;
using System.IO;
using System.Collections.Generic;
using System.Configuration;
using System.ComponentModel;
using System.Transactions;
using System.Data;
using System.Drawing;
using System.Text;
using System.Xml;
using System.Xml.XPath;
using System.Threading;
using System.Windows.Forms;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
#endregion

namespace Microsoft.BizTalk.CAT.Samples.SSISIntegration.Client
{
    public partial class MainForm : Form
    {
...
private void btnSubmit_Click(object sender, EventArgs e) { try { if (shellView.SelectedItems != null && shellView.SelectedItems.Length > 0) { FileInfo fileInfo = null; List<CallParameters> callList = new List<CallParameters>(); for (int i = 0; i < shellView.SelectedItems.Length; i++) { if (File.Exists(shellView.SelectedItems[i].FileSystemPath)) { fileInfo = new FileInfo(shellView.SelectedItems[i].FileSystemPath); callList.Add(new CallParameters(i, (int)fileInfo.Length, shellView.SelectedItems[i].FileSystemPath, cboEndpoint.Text)); } else { WriteToLog(string.Format(FileDoesNotExist, shellView.SelectedItems[i].FileSystemPath)); } } count = callList.Count; completed = 0; progressForm = new ProgressForm(callList); progressForm.Show(); for (int i = 0; i < shellView.SelectedItems.Length; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback(SubmitFile), new StartState(callList[i], this)); } } else { WriteToLog(NoFilesSelected); } } catch (Exception ex) { WriteToLog(ex.Message); } } private void SubmitFile(Object stateInfo) { StartState state = stateInfo as StartState; if (TxnBinding.ContainsKey(state.Parameters.Endpoint) && TxnBinding[state.Parameters.Endpoint]) { SubmitFileWithTxn(state); } else { SubmitFileWithoutTxn(state); } } private void SubmitFileWithTxn(StartState startState) { ChannelFactory<IUploadMessageTxn> channelFactory = null; try { if (startState != null && startState.Parameters != null && !string.IsNullOrEmpty(startState.Parameters.Endpoint) && !string.IsNullOrEmpty(startState.Parameters.Filename) && File.Exists(startState.Parameters.Filename)) { FileInfo fileInfo = new FileInfo(startState.Parameters.Filename); using (FileStream fileStream = File.Open(startState.Parameters.Filename,
FileMode.Open,
FileAccess.Read,
FileShare.Read)) { ProgressStream progressStream = new ProgressStream(startState.Parameters.Index, fileStream); progressStream.ProgressChanged +=
new EventHandler<ProgressChangedEventArgs>(progressForm.ProgressStream_ProgressChanged); UploadMessage uploadMessage = new UploadMessage(Guid.NewGuid().ToString(), txtSender.Text, fileInfo.Name, DateTime.Now, fileInfo.Length, progressStream); channelFactory = new ChannelFactory<IUploadMessageTxn>(startState.Parameters.Endpoint); channelFactory.Endpoint.Contract.SessionMode = SessionMode.Allowed; IUploadMessageTxn channel = channelFactory.CreateChannel(); DateTime start = DateTime.Now; bool committed = true; string transactionId = string.Empty; using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required)) { channel.UploadMessageTxn(uploadMessage); transactionId = Transaction.Current.TransactionInformation.DistributedIdentifier.ToString(); WriteToLog(string.Format(TransactionIdFormat, transactionId)); if (commitRadioButton.Checked) { scope.Complete(); } else { committed = false; Thread.Sleep(200); } } DateTime end = DateTime.Now; TimeSpan time = end.Subtract(start); channelFactory.Close(); double seconds = time.TotalSeconds; WriteToLog(string.Format(MessageSuccessfullyUploaded, fileInfo.Name,
startState.Parameters.Endpoint, seconds)); if (committed) { WriteToLog(string.Format(TransactionCommittedFormat, transactionId)); } else { WriteToLog(string.Format(TransactionAbortedFormat, transactionId)); } } } else { WriteToLog(string.Format(FileDoesNotExist, startState.Parameters.Filename)); } } catch (FaultException ex) { WriteToLog(ex.Message); if (channelFactory != null) { channelFactory.Abort(); } } catch (CommunicationException ex) { WriteToLog(ex.Message); if (channelFactory != null) { channelFactory.Abort(); } } catch (TimeoutException ex) { WriteToLog(ex.Message); if (channelFactory != null) { channelFactory.Abort(); } } catch (Exception ex) { WriteToLog(ex.Message); if (channelFactory != null) { channelFactory.Abort(); } } finally { try { if (startState.Form != null) { startState.Form.ThreadTerminated(); } if (channelFactory != null) { channelFactory.Close(); } } catch (Exception) { } } }
... } }

ProgressStream class

#region Copyright
//-------------------------------------------------
// Author:  Paolo Salvatori
// Email:   [email protected]
// History: 2009-06-30 Created
//-------------------------------------------------
#endregion

#region Using Directives
using System;
using System.IO;
#endregion

namespace Microsoft.BizTalk.CAT.Samples.SSISIntegration.Client
{
    public class ProgressStream : Stream
    {
        #region Private Fields
        private Stream stream;
        private int index;
        private long bytesRead;
        private long totalLength; 
        #endregion

        #region Public Handler
        public event EventHandler<ProgressChangedEventArgs> ProgressChanged; 
        #endregion

        #region Public Constructor
        public ProgressStream(int index, Stream file)
        {
            this.index = index;
            this.stream = file;
            this.totalLength = file.Length;
            this.bytesRead = 0;
        } 
        #endregion

        #region Public Properties
        public override bool CanRead
        {
            get
            {
                return this.stream.CanRead;
            }
        }

        public override bool CanSeek
        {
            get
            {
                return this.stream.CanSeek;
            }
        }

        public override bool CanWrite
        {
            get
            {
                return this.stream.CanWrite;
            }
        }

        public override void Flush() 
        {
            this.stream.Flush();
        }

        public override long Length
        {
            get 
            { 
                return this.stream.Length; 
            }
        }

        public override long Position
        {
            get 
            { 
                return this.stream.Position; 
            }
            set 
            {
                this.stream.Position = value; 
            }
        } 
        #endregion

        #region Public Methods
        public override int Read(byte[] buffer, int offset, int count)
        {
            int result = stream.Read(buffer, offset, count);
            bytesRead += result;
            if (ProgressChanged != null)
            {
                try
                {
                    ProgressChanged(this, new ProgressChangedEventArgs(index, bytesRead, totalLength));
                }
                catch (Exception)
                {
                    ProgressChanged = null;
                }
            }
            return result;
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            return this.stream.Seek(offset, origin);
        }

        public override void SetLength(long value)
        {
            this.stream.SetLength(value);
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            this.stream.Write(buffer, offset, count);
        } 
        #endregion
    }

    public class ProgressChangedEventArgs : EventArgs
    {
        #region Private Fields
        private int index;
        private long bytesRead;
        private long totalLength; 
        #endregion

        #region Public Constructor
        public ProgressChangedEventArgs(int index, long bytesRead, long totalLength)
        {
            this.index = index;
            this.bytesRead = bytesRead;
            this.totalLength = totalLength;
        } 
        #endregion

        #region Public properties
        public int Index
        {
            get
            {
                return this.index;
            }
            set
            {
                this.index = value;
            }
        }

        public long BytesRead
        {
            get
            {
                return this.bytesRead;
            }
            set
            {
                this.bytesRead = value;
            }
        }

        public long TotalLength
        {
            get
            {
                return this.totalLength;
            }
            set
            {
                this.totalLength = value;
            }
        }
        #endregion
    }
}

IUploadMessageTxn interface

#region Copyright
//-------------------------------------------------
// Author:  Paolo Salvatori
// Email:   [email protected]
// History: 2009-01-14 Created
//-------------------------------------------------
#endregion

#region Using Directives
using System;
using System.IO;
using System.ServiceModel;
#endregion

namespace Microsoft.BizTalk.CAT.Samples.SSISIntegration.Client
{
    [ServiceContract(Namespace = "http://microsoft.biztalk.cat/10/samples/ssisintegration/uploadservice")]
    public interface IUploadMessageTxn
    {
        #region Contract Operations        
        [OperationContract(Action = "UploadMessageTxn", ReplyAction = "*")]
        [TransactionFlow(TransactionFlowOption.Mandatory)]
        [XmlSerializerFormat()]
        void UploadMessageTxn(UploadMessage message); 
        #endregion
    }
}

UploadMessage class

#region Copyright
//-------------------------------------------------
// Author:  Paolo Salvatori
// Email:   [email protected]
// History: 2009-01-14 Created
//-------------------------------------------------
#endregion

#region Using Directives
using System;
using System.IO;
using System.ServiceModel;
using System.Net.Security;
#endregion

namespace Microsoft.BizTalk.CAT.Samples.SSISIntegration.Client
{
    [MessageContract]
    public class UploadMessage
    {
        #region Private Fields
        private string id;
        private string sender;
        private string filename;
        private DateTime dateTime;
        private long size;
        private Stream data;
        #endregion

        #region Public Constructors
        public UploadMessage()
        {
            this.id = null;
            this.sender = null;
            this.filename = null;
            this.dateTime = DateTime.Now;
            this.size= 0;
            this.data = null;
        }

        public UploadMessage(string id,
                             string sender,
                             string filename,
                             DateTime dateTime,
                             long size,
                             Stream data)
        {
            this.id = id;
            this.sender = sender;
            this.filename = filename;
            this.dateTime = dateTime;
            this.size = size;
            this.data = data;
        }
        #endregion

        #region Public Properties
        [MessageHeader(Name = "id",
                       Namespace = "http://microsoft.biztalk.cat/10/samples/ssisintegration/uploadservice",
                       ProtectionLevel = ProtectionLevel.None)]
        public string Id
        {
            get
            {
                return this.id;
            }
            set
            {
                this.id = value;
            }
        }

        [MessageHeader(Name = "sender",
                       Namespace = "http://microsoft.biztalk.cat/10/samples/ssisintegration/uploadservice",
                       ProtectionLevel = ProtectionLevel.None)]
        public string Sender
        {
            get
            {
                return this.sender;
            }
            set
            {
                this.sender = value;
            }
        }

        [MessageHeader(Name = "filename",
                       Namespace = "http://microsoft.biztalk.cat/10/samples/ssisintegration/uploadservice",
                       ProtectionLevel = ProtectionLevel.None)]
        public string Filename
        {
            get
            {
                return this.filename;
            }
            set
            {
                this.filename = value;
            }
        }

        [MessageHeader(Name = "dateTime",
                       Namespace = "http://microsoft.biztalk.cat/10/samples/ssisintegration/uploadservice",
                       ProtectionLevel = ProtectionLevel.None)]
        public DateTime DateTime
        {
            get
            {
                return this.dateTime;
            }
            set
            {
                this.dateTime = value;
            }
        }

        [MessageHeader(Name = "size",
                       Namespace = "http://microsoft.biztalk.cat/10/samples/ssisintegration/uploadservice",
                       ProtectionLevel = ProtectionLevel.None)]
        public long Size
        {
            get
            {
                return this.size;
            }
            set
            {
                this.size = value;
            }
        }

        [MessageBodyMember(Name = "data",
                           Namespace = "http://microsoft.biztalk.cat/10/samples/ssisintegration/uploadservice",
                           ProtectionLevel = ProtectionLevel.None)]
        public Stream Data
        {
            get
            {
                return this.data;
            }
            set
            {
                this.data = value;
            }
        } 
        #endregion
    }
}

App.Config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.diagnostics>
        <sources>
            <source name="System.ServiceModel.MessageLogging" switchValue="Warning, ActivityTracing">
                <listeners>
                    <add type="System.Diagnostics.DefaultTraceListener" name="Default">
                        <filter type="" />
                    </add>
                    <add name="ServiceModelMessageLoggingListener">
                        <filter type="" />
                    </add>
                </listeners>
            </source>
        </sources>
        <sharedListeners>
            <add initializeData="C:\ssisintegration_client.svclog"
                 type="System.Diagnostics.XmlWriterTraceListener, System, Version=2.0.0.0, Culture=neutral, 
PublicKeyToken=b77a5c561934e089" name="ServiceModelMessageLoggingListener" traceOutputOptions="Timestamp"> <filter type="" /> </add> </sharedListeners> </system.diagnostics> <system.serviceModel> <diagnostics> <messageLogging logEntireMessage="false" logMalformedMessages="false" logMessagesAtServiceLevel="false" logMessagesAtTransportLevel="false" maxSizeOfMessageToLog="1000000" /> </diagnostics> <bindings> <customBinding> <binding name="netTcpMtomStreamedBinding" receiveTimeout="01:00:00" sendTimeout="01:00:00"> <mtomMessageEncoding maxBufferSize="1048576" maxReadPoolSize="1024" maxWritePoolSize="1024"> <readerQuotas maxDepth="1048576" maxStringContentLength="1048576" maxArrayLength="104857600" maxBytesPerRead="1048576" maxNameTableCharCount="1048576" /> </mtomMessageEncoding> <tcpTransport maxBufferPoolSize="1048576" maxReceivedMessageSize="104857600" connectionBufferSize="1048576" maxBufferSize="1048576" maxPendingConnections="200" maxPendingAccepts="200" transferMode="Streamed" listenBacklog="200"> <connectionPoolSettings maxOutboundConnectionsPerEndpoint="200" /> </tcpTransport> </binding> <binding name="netTcpBinaryStreamedBinding" receiveTimeout="01:00:00" sendTimeout="01:00:00"> <binaryMessageEncoding maxSessionSize="1048576"> <readerQuotas maxDepth="1048576" maxStringContentLength="1048576" maxArrayLength="104857600" maxBytesPerRead="1048576" maxNameTableCharCount="1048576" /> </binaryMessageEncoding> <tcpTransport maxBufferPoolSize="1048576" maxReceivedMessageSize="104857600" connectionBufferSize="1048576" maxBufferSize="1048576" maxPendingConnections="200" maxPendingAccepts="200" transferMode="Streamed" listenBacklog="200"> <connectionPoolSettings maxOutboundConnectionsPerEndpoint="200" /> </tcpTransport> </binding> <binding name="netTcpTextStreamedBinding" receiveTimeout="01:00:00" sendTimeout="01:00:00"> <textMessageEncoding maxWritePoolSize="64"> <readerQuotas maxDepth="1048576" maxStringContentLength="1048576" maxArrayLength="104857600" maxBytesPerRead="1048576" maxNameTableCharCount="1048576" /> </textMessageEncoding> <tcpTransport maxBufferPoolSize="1048576" maxReceivedMessageSize="104857600" connectionBufferSize="1048576" maxBufferSize="1048576" maxPendingConnections="200" maxPendingAccepts="200" transferMode="Streamed" listenBacklog="200"> <connectionPoolSettings maxOutboundConnectionsPerEndpoint="200" /> </tcpTransport> </binding> <binding name="netTcpMtomBufferedBinding" receiveTimeout="01:00:00" sendTimeout="01:00:00"> <mtomMessageEncoding maxBufferSize="1048576"> <readerQuotas maxDepth="1048576" maxStringContentLength="1048576" maxArrayLength="104857600" maxBytesPerRead="1048576" maxNameTableCharCount="1048576" /> </mtomMessageEncoding> <tcpTransport maxBufferPoolSize="1048576" maxReceivedMessageSize="1048576" connectionBufferSize="1048576" maxBufferSize="104857600" maxPendingConnections="200" maxPendingAccepts="200" transferMode="Buffered" listenBacklog="200"> <connectionPoolSettings maxOutboundConnectionsPerEndpoint="200" /> </tcpTransport> </binding> <binding name="netTcpBinaryBufferedBinding" receiveTimeout="01:00:00" sendTimeout="01:00:00"> <binaryMessageEncoding maxSessionSize="1048576"> <readerQuotas maxDepth="1048576" maxStringContentLength="1048576" maxArrayLength="104857600" maxBytesPerRead="1048576" maxNameTableCharCount="1048576" /> </binaryMessageEncoding> <tcpTransport maxBufferPoolSize="1048576" maxReceivedMessageSize="104857600" connectionBufferSize="1048576" maxBufferSize="104857600" maxPendingConnections="200" maxPendingAccepts="200" transferMode="Buffered" listenBacklog="200"> <connectionPoolSettings maxOutboundConnectionsPerEndpoint="200" /> </tcpTransport> </binding> <binding name="netTcpTextBufferedBinding" receiveTimeout="01:00:00" sendTimeout="01:00:00"> <textMessageEncoding maxWritePoolSize="64"> <readerQuotas maxDepth="1048576" maxStringContentLength="1048576" maxArrayLength="104857600" maxBytesPerRead="1048576" maxNameTableCharCount="1048576" /> </textMessageEncoding> <tcpTransport maxBufferPoolSize="1048576" maxReceivedMessageSize="104857600" connectionBufferSize="1048576" maxBufferSize="104857600" maxPendingConnections="200" maxPendingAccepts="200" transferMode="Buffered" listenBacklog="200"> <connectionPoolSettings maxOutboundConnectionsPerEndpoint="200" /> </tcpTransport> </binding> <binding name="netTcpMtomStreamedTxnBinding" receiveTimeout="01:00:00" sendTimeout="01:00:00"> <transactionFlow transactionProtocol="OleTransactions"/> <mtomMessageEncoding maxBufferSize="1048576" maxReadPoolSize="1024" maxWritePoolSize="1024"> <readerQuotas maxDepth="1048576" maxStringContentLength="1048576" maxArrayLength="104857600" maxBytesPerRead="1048576" maxNameTableCharCount="1048576" /> </mtomMessageEncoding> <tcpTransport maxBufferPoolSize="1048576" maxReceivedMessageSize="104857600" connectionBufferSize="1048576" maxBufferSize="1048576" maxPendingConnections="200" maxPendingAccepts="200" transferMode="Streamed" listenBacklog="200"> <connectionPoolSettings maxOutboundConnectionsPerEndpoint="200" /> </tcpTransport> </binding> </customBinding> <netNamedPipeBinding> <binding name="netNamedPipeBinding" transferMode="Streamed" maxBufferPoolSize="1048576" maxBufferSize="1048576" maxConnections="200" maxReceivedMessageSize="104857600"> <security mode="None" /> </binding> </netNamedPipeBinding> </bindings> <client> <endpoint address="net.tcp://localhost:11550/ssisintegration" binding="customBinding" bindingConfiguration="netTcpMtomStreamedBinding" contract="Microsoft.BizTalk.CAT.Samples.SSISIntegration.Client.IUploadMessage" name="netTcpMtomStreamedEndpoint" /> <endpoint address="net.tcp://localhost:11551/ssisintegration" binding="customBinding" bindingConfiguration="netTcpBinaryStreamedBinding" contract="Microsoft.BizTalk.CAT.Samples.SSISIntegration.Client.IUploadMessage" name="netTcpBinaryStreamedEndpoint" /> <endpoint address="net.tcp://localhost:11552/ssisintegration" binding="customBinding" bindingConfiguration="netTcpTextStreamedBinding" contract="Microsoft.BizTalk.CAT.Samples.SSISIntegration.Client.IUploadMessage" name="netTcpTextStreamedEndpoint" /> <endpoint address="net.tcp://localhost:11553/ssisintegration" binding="customBinding" bindingConfiguration="netTcpMtomBufferedBinding" contract="Microsoft.BizTalk.CAT.Samples.SSISIntegration.Client.IUploadMessage" name="netTcpMtomBufferedEndpoint" /> <endpoint address="net.tcp://localhost:11554/ssisintegration" binding="customBinding" bindingConfiguration="netTcpBinaryBufferedBinding" contract="Microsoft.BizTalk.CAT.Samples.SSISIntegration.Client.IUploadMessage" name="netTcpBinaryBufferedEndpoint" /> <endpoint address="net.tcp://localhost:11555/ssisintegration" binding="customBinding" bindingConfiguration="netTcpTextBufferedBinding" contract="Microsoft.BizTalk.CAT.Samples.SSISIntegration.Client.IUploadMessage" name="netTcpTextBufferedEndpoint" /> <endpoint address="net.tcp://localhost:11556/ssisintegration" binding="customBinding" bindingConfiguration="netTcpMtomStreamedTxnBinding" contract="Microsoft.BizTalk.CAT.Samples.SSISIntegration.Client.IUploadMessageTxn" name="netTcpMtomStreamedTxnEndpoint" /> <endpoint address="net.tcp://localhost:11557/ssisintegration" binding="customBinding" bindingConfiguration="netTcpBinaryStreamedBinding" contract="Microsoft.BizTalk.CAT.Samples.SSISIntegration.Client.IUploadMessage" name="netTcpSynchronousSSISPackageEndpoint" /> <endpoint address="net.tcp://localhost:11558/ssisintegration" binding="customBinding" bindingConfiguration="netTcpBinaryStreamedBinding" contract="Microsoft.BizTalk.CAT.Samples.SSISIntegration.Client.IUploadMessage" name="netTcpAsynchronousSSISPackageEndpoint" /> <endpoint address="net.pipe://localhost/ssiintegration" binding="netNamedPipeBinding" bindingConfiguration="netNamedPipeBinding" contract="Microsoft.BizTalk.CAT.Samples.SSISIntegration.Client.IUploadMessage" name="netNamedPipeStreamedEndpoint" /> <endpoint address="net.tcp://localhost:11557/ssisintegration" binding="customBinding" bindingConfiguration="netTcpMtomStreamedBinding" contract="Microsoft.BizTalk.CAT.Samples.SSISIntegration.Client.IUploadMessage" name="directServiceEndpoint" /> </client> </system.serviceModel> </configuration>

For more information on how using the streamed transfer mode along with WCF, you can read the following topic:

  • “How to: Enable Streaming” article on MSDN.

BizTalk Application

In order to compare the performance of the various message encoders and transfer modes when transferring large messages over the network, I created six different WCF-Custom Receive Locations, one for each possible combination of these properties:

  1. SSISIntegration.WCF-Custom.NetTcp.Mtom.Streamed.ReceiveLocation: TransferMode = Streamed, Message Encoder = MTOM.
  2. SSISIntegration.WCF-Custom.NetTcp.Binary.Streamed.ReceiveLocation: TransferMode = Streamed, Message Encoder = Binary.
  3. SSISIntegration.WCF-Custom.NetTcp.Text.Streamed.ReceiveLocation: TransferMode = Streamed, Message Encoder = Text.
  4. SSISIntegration.WCF-Custom.NetTcp.Mtom.Buffered.ReceiveLocation: TransferMode = Buffered, Message Encoder = MTOM.
  5. SSISIntegration.WCF-Custom.NetTcp.Binary.Buffered.ReceiveLocation: TransferMode = Buffered, Message Encoder = Binary.
  6. SSISIntegration.WCF-Custom.NetTcp.Text.Buffered.ReceiveLocation: TransferMode = Buffered, Message Encoder = Text.

Each of the above Receive Locations use a CustomBinding and the TcpTransportBindingElement. Let’s compare the Binding configuration of the Receive Locations #2 and #5. They both use the Binary message encoder but they are configured to use a different transfer mode, respectively streamed and buffered. If you look at the WCF-Custom Adapter configuration of the SSISIntegration.WCF-Custom.NetTcp.Binary.Streamed.ReceiveLocation receive location (see the picture below), you will note that:

  • The value of the MaxReceivedMessageSize property has been increased to almost 1 GB to handle large messages.
  • The value of the MaxBufferSize property has been set to 1 MB. You can eventually arrange a larger memory buffer.
  • The value of the TransferMode property has been set to Streamed.
  • I increased the value of several properties like MaxPendingAccepts (default value = 1), MaxPendingConnections (default value = 10), ListenBacklog (default value = 10), MaxOutboundConnectionsPerEndpoint (default value = 10). 200 is probably too high, but it’s important to note here that when using the WCF-Custom/WCF-CustomIsolated Adapter in conjunction with the CustomBinding, you have can fine tune the value of all the properties exposed by the individual binding elements which compose your binding.

By contrast, the SSISIntegration.WCF-Custom.NetTcp.Binary.Buffered.ReceiveLocation receive location has been  configured as follows:

  • The value of the MaxReceivedMessageSize property has been increased to almost 1 GB to handle large messages.
  • The value of the MaxBufferSize property is identical to the value of the MaxReceivedMessageSize. When using the Buffered transfer mode, the incoming message must be entirely received in a memory buffer before message processing can start. Therefore, the value of the MaxBufferSize property must necessarily be equal to the value of the MaxReceivedMessageSize property. If you try to specify a different value for the 2 properties, the WCF-Custom Receive Location will be immediately disabled and the following error message will be written in the Application Log:

    The Messaging Engine failed to add a receive location “<Receive Location Name>” with URL “<Receive Location URL>” to the adapter “WCF-Custom”. Reason: “System.ArgumentException: For TransferMode.Buffered, MaxReceivedMessageSize and MaxBufferSize must be the same value.

  • The value of the TransferMode property has been set to Streamed.
  • The value of other performance related has been tuned up.

All the above Receive Locations have been configured to use the ServiceThrottlingBehavior. The default value of the 3 properties exposed by the service behavior have been increased from the default value to 200. Indeed, as I explained in my article called Customizing and Extending the BizTalk WCF Adapters, all incoming messages to a certain WCF Receive Location are received and processed by a singleton instance of the BizTalkServiceInstance class. Thus, the value you specify for the MaxConcurrentInstances property is irrelevant as a single service instance will be used for each WCF Receive Location. Also the value you assign to the MaxConcurrentSessions  property is no relevant if you don’t use a session-aware binding. As a consequence, the only property that you should properly set is the MaxConcurrentCalls that specifies the maximum number of messages concurrently processed by the WCF Receive location.

 

If we enable WCF tracing on the client application and we try to send a file to BizTalk, we can note that the SOAP message sent by the client application to BizTalk has the following format (the namespace of custom headers have been removed for simplicity).

 
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
    <s:Header>
        <a:Action s:mustUnderstand="1">UploadMessage</a:Action>
        <h:dateTime>2010-05-17T17:14:47.0058023+02:00</h:dateTime>
        <h:filename>config.xml</h:filename>
        <h:id>93204315-aeee-48d0-8922-09ef521c27f2</h:id>
        <h:sender>Paolo Salvatori</h:sender>
        <h:size>3617</h:size>
        <a:MessageID>urn:uuid:d8f87456-cef5-4302-b760-f1765483e4f1</a:MessageID>
        <a:ReplyTo>
        <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
        </a:ReplyTo>
        <a:To s:mustUnderstand="1">net.tcp://localhost:11553/ssisintegration</a:To>
    </s:Header>
    <s:Body>
        <UploadMessage xmlns="http://microsoft.biztalk.cat/10/samples/ssisintegration/uploadservice">
            <data>...</data>
        </UploadMessage>
    </s:Body>
</s:Envelope>

As a consequence, in order to extract data from the Body element, it’s necessary to properly configure the Inbound BizTalk message body section of all the WCF Receive Locations:

  • The expression “/*[local-name()=’UploadMessage’ and namespace-uri()='<targetNamespace’’]/*[local-name()=’data’ and namespace-uri()=’targetNamespace’]”  must be assigned to the Body path expression field.
  • Base64 must be selected as Node encoding.

Scenario

To avoid publishing the incoming large message to the MessageBox, I created a custom receive pipeline and in particular a custom pipeline component called StageFilePipeComponent that receives and stage the incoming file a folder or to a custom SQL database using a streaming approach and finally replace the  original file with a placeholder document which contains the location of the staged file and its metadata. When published to the MessageBox, the placeholder document is consumed by a Send Port and processed by a custom send pipeline that reads the location of the original message and substitutes the placeholder document with this latter. Besides, the custom pipeline running within the Receive Location promotes the AckRequired context property. This will cause the send port to generate ACK and NACK messages when the adapter reports a message as successfully delivered or in the case of a NACK, when the adapter has failed and a message will be suspended. These messages are special and will only be published to the MessageBox, if there is someone subscribing to them. In my case, ACK and NACK messages are consumed by a special orchestration which role is to remove the original message from the staging repository (Folder, SQL Server) once the original message has been successfully transmitted by the send port or to eventually resubmit a suspended resumable message when the send port runs out of retries. In the next article of the series, I’ll show the code of the pipeline components and orchestration. For now I will only describe the data flow of the demo and discuss the results in terms of performance:

Non Transactional Case

 

  1. The user selects one or multiple files on the UI of the driver application, then selects a service endpoint that corresponds to a given WCF Receive Location and finally click the Submit button. For simplicity, let’s assume  that the user chose to send a single file. This latter is received by one of the six WCF Receive Locations.
  2. The custom StageFileReceivePipeline reads the message metadata from the custom headers using the InboundHeaders context property and then writes the original file to a staging repository that can be a folder or a table in custom SQL database which contains a varbinary(max) FILSTREAM column. Besides, the pipeline promotes the AckRequired context property to enable delivery notifications and substitutes the original file with a small placeholder document which contains the metadata and actual location of the original message.
  3. The placeholder document is published to the BizTalkMsgBoxDb by the Message Agent.
  4. The inbound request is consumed by a FILE Send Port.
  5. The RestoreFileSendPipeline reads the content of the placeholder document and replaces this latter with the original message. In particular, the RestoreFilePipelineComponent pipeline assign a different type of stream to the outbound IBaseMessage depending on the location of the original message:
    • Folder: the component assign a FileStream object to the Data property of the message body part.
    • SQL Server: the component opens a SqlFileStream to read the content of the original file from SQL Server and wraps this object within a WrapperStream object. The pipeline component opens a connection and initiate a SQL transaction to initiate the SqlFileStream. The content of this latter is not meant to be read by the pipeline component, but  by the send adapter, therefore the connection and transaction need to remain open. The scope of the WrapperStream object is committing the transaction and closing the connection when the adapter has successfully transmitted the message. In fact, when the adapter has finished processing, the BizTalk messaging runtime invokes the Close method of the WrapperStream object which contains the code for committing the transaction and closing the connection.
  6. The WCF-Custom Send Port  write the original message to the target folder (Out).
  7. The BizTalk Messaging Runtime generates an ACK/NACK message depending on whether the transmission was successful or not.
  8. The ACK/NACK message is consumed by a special orchestration called NotificationHandler. This latter executes a different action depending on the type of the inbound document:
    • ACK: the orchestration deletes the original file from the staging repository.
    • NACK: the orchestration invokes a BRE policy to decide how to proceed. Depending on the information contained in the policy, the orchestration can execute one of the following actions:
      • Terminate the suspended service instance.
      • Resume the suspended service instance.

Results

The two tables below report the performance data in terms of latency and memory consumption that I measured during my tests. The size of messages used during tests was around 50 MB. The following findings emerge from the analysis of the figures:

  1. Binary Encoder > Mtom Encoder > Text Encoder, where “>”  stands for faster.
  2. TransferMode= Streamed is faster than the TransferMode= Buffered (the default) when transmitting large messages.
  3. If the WCF Receive Location uses the TransferMode= Streamed the Working Set of the BizTalk process hosting the Receive Location remains pretty low when receiving multiple large messages at the same time.
  4. If the WCF Receive Location uses the TransferMode= Buffered the Working Set of the BizTalk process hosting the Receive Location grows almost linearly with the number of concurrent incoming messages.

Conclusions

This article introduced a powerful technique to transfer and handle large files and provided a comparison in terms of performance and memory consumption between the message encoders and transfer modes supplied by WCF. In the next article, I’ll provide more details on the solution implementation and I’ll explain how to configure a WCF-Custom Receive Location and the StageFileReceivePipeline to stage the incoming file to the file system (using the Transactional NTFS or TxF) or to SQL Server in a transactional way using the same transaction utilized by the Message Agent to post the placeholder document to the MessageBox. In the meantime, you can download the code from my site in 2 flavors:

  • LargeMessageTransmission: this is the original version with no SSIS integration.
  • SSISIntegration: this is a sort of experimental version where the StageFilePipelineComponent has been extended to allow the possibility to synchronously or asynchronously invoke a SSIS Package against the staged file. To this purpose, I extended the Helpers library with a class called PackageHelper that provides the possibility to call an SSIS Package from a custom component.

As always, please provide feedbacks, problems and suggestions.

Yet on BizTalk Impersonation With WCF Adapters

Scenario

Last year I wrote a post on how using BizTalk Server 2006 R2/2009 and Protocol Transition to impersonate the original caller when invoking a downstream service that uses the Windows Integrated Security. Recently, one customer posed the following question to my colleague, Tim Wieman:

Can I create a WCF Send Port that is able to impersonate a statically defined domain user, other than then the service account of the host instance process, when calling a downstream WCF service that exposes a BasicHttpBinding/WsHttpBinding endpoint configured to use the Transport security mode ?

The answer is:

  • Yes, when using the Basic or the Digest authentication scheme.
  • No, when using the Windows or NTLM client credential type.

To verify this constraint, you can proceed as follows:

  • Open the BizTalk Administration Console.
  • Create a new WCF-BasicHttp or WCF-WsHttp Static Solicit-Response Send Port
  • Click the Configure button.
  • Select the Security tab.
  • Choose the Transport security mode.

At this point, if you select the Basic or Digest transport client credential type from the corresponding drop-down list:

  • You can click the Edit button in the User name credentials section, as highlighted in the picture below.

 

  • You can specify the Username and Password of the account that the Send Port will impersonate when invoking the target WCF service. As an alternative, you can leverage the Single Sign-On to redeem a ticket and pick a user at runtime from a certain affiliate application.

Instead, if you select the Ntlm or Windows transport client credential type from the corresponding drop-down list, the Edit button in the User name credentials section is greyed out, as shown in the picture below:

So at this point some of you might ask yourselves:

How can I impersonate a statically-defined user, different from the service account of the host process running my WCF Send Port, when invoking an underlying WCF service that uses the Transport security mode along with the Ntlm or Windows authentication scheme?

The answer is straightforward, you can achieve this objective using the WCF-Custom adapter and writing a custom WCF channel. Indeed, I didn’t create a new component from scratch, I just used grabbed some code from MSDN, and extended the component I wrote one year ago for my previous post on BizTalk and Protocol Transition .  In particular, I made the following changes:

  • I extended the following components:
    • InspectingBindingExtensionElement
    • InspectingBindingElement
    • InspectingChannelFactory
    • InspectingRequestChannel
  • to expose two additional properties:
    • WindowsUserName: gets or sets the domain account in the form of DOMAIN\Username that the Send Port will impersonate at runtime.
    • WindowsUserPassword: gets or sets the password of the domain account.
  • Then I extended the WindowsUserPositionEnum type and therefore the WindowsUserPosition property to include a new mode called Static. As a consequence, the custom channel at runtime will retrieve the client credentials in a different way depending on the value of the WindowsUserPosition property exposed by the InspectingBindingExtensionElement component:
    • Context: username will be read from the message context property identified by the ContextPropertyName and ContextPropertyNamespace properties exposed by the InspectingBindingExtensionElement. In this case, the environment must be properly configured to use Protocol Transition. See my previous post for more details.
    • Message: username will be read from the message using the XPath expression contained in the WindowsUserXPath property. Even in this case, the environment must be properly configured to use Protocol Transition. See my previous post for more details.
    • Static: the custom channel will use the client credentials contained in the WindowsUserName and WindowsUserPassword to impersonate the corresponding domain account before invoking the downstream WCF service. Note: this pattern requires the component to uses the client credentials to invoke the LogonUser function at runtime, but it does not require to configure the BizTalk environment for Protocol Transition.
  • I finally extended the InspectingRequestChannel and InspectingHelper classes to support the new Static mode. In particular, the custom channel at runtime performs the following steps to impersonate a given domain account declaratively defined in the WCF Send Port configuration:
    • It calls the LogonUser static method exposed by the InspectingHelper class which in turn invokes the LogonUser Windows function which returns a token handle.
    • The channel creates a new WindowsIdentity object using the constructor that accepts the user token returned by the previous call.
    • Then, it invokes the Impersonate method exposed by the WindowsIdentity object.
    • The channel invokes the underlying channel.
    • In the finally block, when the call is complete, the channel invokes the Undo method on the WindowsImpersonationContext object returned by the Impersonate method, an then it invokes the CloseHandle static method exposed by the InspectingHelper class which in turn invokes the CloseHandle Windows function.

For your convenience, I report below the new code for the InspectingRequestChannel and InspectingHelper classes (I purposely omitted parts for ease of reading):

InspectingHelper class

#region Copyright
//-------------------------------------------------
// Author:  Paolo Salvatori
// Email:   [email protected]
// History: 2008-09-17 Created
//-------------------------------------------------
#endregion

#region Using Directives
using System;
using System.Diagnostics;
using System.Configuration;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Security.Permissions;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.DirectoryServices.ActiveDirectory;
using System.Xml;
using System.IO;
using System.Text;
using Microsoft.BizTalk.XPath;
#endregion

namespace Microsoft.BizTalk.CAT.Samples.ProtocolTransition.WCFExtensionLibrary
{
    /// <summary>
    /// This class exposes the logic to impersonate another user using the Protocol Transition mechanism.
    /// </summary>
    public class InspectingHelper
    {
        #region DllImport
        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        public static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword,
            int dwLogonType, int dwLogonProvider, ref IntPtr phToken);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        public extern static bool CloseHandle(IntPtr handle);
        #endregion

        #region Private Constants
        ...
// The following constants are used when calling the LogonUser external function private const int LOGON32_PROVIDER_DEFAULT = 0; //This parameter causes LogonUser to create a primary token. private const int LOGON32_LOGON_INTERACTIVE = 2; #endregion #region Private Static Fields private static string domainFQDN = string.Empty; #endregion #region Public Static Constructor static InspectingHelper() { try { Domain domain = Domain.GetComputerDomain(); if (domain != null) { domainFQDN = domain.Name; } } catch (Exception ex) { Debug.WriteLine(string.Format(MessageFormat, ex.Message)); } } #endregion #region Static Public Methods public static string GetUserPrincipalName(ref Message message, WindowsUserPositionEnum windowsUserPosition, string contextPropertyName, string contextPropertyNamespace, string windowsUserXPath, string windowsUserName, string windowsUserPassword, int maxBufferSize, bool traceEnabled, out string userName, out string domainName) { string windowsUser = null; domainName = null; userName = null; try { switch (windowsUserPosition) { case WindowsUserPositionEnum.Message: if (message != null && !string.IsNullOrEmpty(windowsUserXPath)) { MessageBuffer messageBuffer = message.CreateBufferedCopy(maxBufferSize); if (messageBuffer == null) { throw new ApplicationException(MessageBufferCannotBeNull); } Message clone = messageBuffer.CreateMessage(); if (message == null) { throw new ApplicationException(CloneCannotBeNull); } message = messageBuffer.CreateMessage(); if (message == null) { throw new ApplicationException(MessageCannotBeNull); } XmlDictionaryReader xmlDictionaryReader = clone.GetReaderAtBodyContents(); if (xmlDictionaryReader == null) { throw new ApplicationException(XmlDictionaryReaderCannotBeNull); } XPathCollection xPathCollection = new XPathCollection(); if (xPathCollection == null) { throw new ApplicationException(XPathCollectionCannotBeNull); } XPathReader xPathReader = new XPathReader(xmlDictionaryReader, xPathCollection); if (xPathReader == null) { throw new ApplicationException(XPathReaderCannotBeNull); } xPathCollection.Add(windowsUserXPath); bool ok = false; while (xPathReader.ReadUntilMatch()) { if (xPathReader.Match(0) && !ok) { windowsUser = xPathReader.ReadString(); ok = true; } } } break; case WindowsUserPositionEnum.Context: if (string.IsNullOrEmpty(contextPropertyName)) { throw new ApplicationException(ContextPropertyNameCannotBeNull); } if (string.IsNullOrEmpty(contextPropertyNamespace)) { throw new ApplicationException(ContextPropertyNamespaceCannotBeNull); } string contextPropertyKey = string.Format(ContextPropertyKeyFormat,
contextPropertyNamespace,
contextPropertyName); if (message.Properties.ContainsKey(contextPropertyKey)) { windowsUser = message.Properties[contextPropertyKey] as string; } else { throw new ApplicationException(string.Format(NoContextPropertyFormat, contextPropertyKey)); } break; case WindowsUserPositionEnum.Static: windowsUser = windowsUserName; break; } if (!string.IsNullOrEmpty(windowsUser)) { string[] parts = windowsUser.Split(new char[] { Path.DirectorySeparatorChar }); if (parts != null && parts.Length > 1) { domainName = parts[0]; userName = parts[1]; Debug.WriteLineIf(traceEnabled, string.Format(CreatingUPNFormat, windowsUser)); string upn = string.Format(UserPrincipalNameFormat, parts[1], domainFQDN); Debug.WriteLineIf(traceEnabled, string.Format(UsingUserPrincipalNameFormat, upn)); return upn; } } } catch (Exception ex) { Debug.WriteLineIf(traceEnabled, string.Format(MessageFormat, ex.Message)); throw ex; } return null; } public static bool LogonUser(string userName, string domainName, string windowsUserPassword, bool traceEnabled, ref IntPtr tokenHandle) { Debug.WriteLineIf(traceEnabled, string.Format(StartLogonUserFormat,
domainName ?? Unknown,
userName ?? Unknown)); // Call LogonUser to obtain a handle to an access token. bool ok = LogonUser(userName, domainName, windowsUserPassword, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, ref tokenHandle); if (traceEnabled) { if (ok) { Debug.WriteLineIf(traceEnabled, string.Format(LogonUserSucceededFormat,
domainName ?? Unknown,
userName ?? Unknown)); } else { Debug.WriteLineIf(traceEnabled, string.Format(LogonUserFailedFormat,
domainName ?? Unknown,
userName ?? Unknown)); } } return ok; } public static bool CloseHandle(IntPtr tokenHandle, string userName, string domainName, bool traceEnabled) { Debug.WriteLineIf(traceEnabled, string.Format(StartCloseTokenFormat,
domainName ?? Unknown,
userName ?? Unknown)); bool ok = CloseHandle(tokenHandle); if (traceEnabled) { if (ok) { Debug.WriteLineIf(traceEnabled, string.Format(CloseTokenSucceededFormat,
domainName ?? Unknown,
userName ?? Unknown)); } else { Debug.WriteLineIf(traceEnabled, string.Format(CloseTokenFailedFormat,
domainName ?? Unknown,
userName ?? Unknown)); } } return ok; } #endregion } }

InspectingRequestChannel class

public class InspectingRequestChannel : InspectingChannelBase<IRequestChannel>,
                                        IRequestChannel
{
    ...

    public Message Request(Message message, TimeSpan timeout)
    {
        Message reply = null;
        string upn = null;
        WindowsImpersonationContext impersonationContext = null;
        IntPtr tokenHandle = new IntPtr(0);
        string userName = null;
        string domainName = null;

        try
        {
            if (componentEnabled)
            {
                WindowsIdentity identity = null;
                upn = InspectingHelper.GetUserPrincipalName(ref message,
                                                                windowsUserPosition,
                                                                contextPropertyName,
                                                                contextPropertyNamespace,
                                                                windowsUserXPath,
                                                                windowsUserName,
                                                                windowsUserPassword,
                                                                maxBufferSize,
                                                                traceEnabled,
                                                                out userName,
                                                                out domainName);
                if (windowsUserPosition == WindowsUserPositionEnum.Static)
                {

                    // Call LogonUser to obtain a handle to an access token.
                    bool returnValue = InspectingHelper.LogonUser(userName, 
                                                                  domainName, 
                                                                  windowsUserPassword,
                                                                  traceEnabled,
                                                                  ref tokenHandle);
                    // Protocol Transition is not necessary in this case
                    identity = new WindowsIdentity(tokenHandle);
                }
                else
                {
                    if (!string.IsNullOrEmpty(upn))
                    {
                        // Protocol Transition must be properly configured,
                        // otherwise the impersonation will fail
                        identity = new WindowsIdentity(upn);
                    }
                }
                Debug.WriteLineIf(traceEnabled, string.Format(ImpersonatingFormat, upn));
                impersonationContext = identity.Impersonate();
                Debug.WriteLineIf(traceEnabled, string.Format(ImpersonatedFormat, upn));
            }
            Debug.WriteLineIf(traceEnabled, CallingWebService);
            reply = this.InnerChannel.Request(message);
            Debug.WriteLineIf(traceEnabled, WebServiceCalled);
        }
        catch (Exception ex)
        {
            Debug.WriteLineIf(traceEnabled, string.Format(MessageFormat, ex.Message));
            throw ex;
        }
        finally
        {
            if (impersonationContext != null)
            {
                impersonationContext.Undo();
                Debug.WriteLineIf(traceEnabled, string.Format(ImpersonationUndoneFormat, upn ?? Unknown));
            }
            if (tokenHandle != IntPtr.Zero)
            {
                InspectingHelper.CloseHandle(tokenHandle,
                                             userName,
                                             domainName,
                                             traceEnabled);
            }
        }
        return reply;
    }
}

Test Case

To test my component, I created the following test case:

WinForm driver application submits a new request to a WCF-NetTcp Request-Response Receive Location

  1. The Message Agent submits the incoming request message to the MessageBox (BizTalkMsgBoxDb).
  2. The inbound request is consumed by a Solicit Response WCF-Custom Send Port. This latter uses a Filter Expression to receive all the documents published by the Receive Port hosting the WCF Receive Location.
  3. The inbound message is mapped to the request format by the downstream HelloWorldService web service. This latter is hosted by IIS and exposes a single WsHttpBinding endpoint.
  4. The WCF-Custom Send Port impersonates the user statically defined in the Port configuration and invokes the underlying HelloWorldService WCF service that in the scenario is hosted by a separate Application Pool (w3wp.exe) on the same IIS instance.
  5. The HelloWorldService WCF service returns a response message.
  6. The incoming response message is mapped to the format expected by the client application.
  7. The transformed response message is published to the MessageBox.
  8. The response message is retrieved by the Request-Response WCF Custom Receive Location which originally received the request call.
  9. The response message is returned to the client WinForm application.

The following picture shows the binding configuration of the WCF Send Port used to communicate with the HelloWorldService.

Finally, the picture below reports the trace captured during a test run.

Conclusions

As I explained in one of my recent posts, using WCF extensibility points allows you customize in-depth the default behavior of BizTalk WCF Adapters. In particular, the WCF-Custom Adapter provides the possibility to specify the customize the composition of the binding and hence of the channel stack that will be created and used at runtime to communicate with external applications.

In this article we have seen how to exploit this characteristic to workaround and bypass a constraint of WCF Adapters. As usual, I had just a few hours to write the code and write the article, so should you find an error or a problem in my component, please send me an email or leave a comment on my blog, thanks!

You can find the new version of the code here.

Visual Studio (BizTalk Server) 2010 development / BizTalk Server 2009 deployment mix

Can you use the updated development environment for BizTalk Server 2010 with Visual Studio 2010 while still deploying to BizTalk Server 2009?

Can you do it? On a file level, yes. On a project level, yes. On a solution level, doesn’t seem like it. On an assembly level, no.

I tried two scenarios.

  1. A simple messaging only scenario with a transformation on the receive port.
  2. An orchestration scenario picking up the file from the receive location, doing the mapping and delivering it to the send port.

I developed the solution on a BizTalk Server 2010 / Visual Studio 2010 combo and deployed it to BizTalk Server 2009.

So what worked and what didn’t?

Compile and deploy the .NET 4 assembly to BizTalk Server 2009

FAIL!

Why?

Even though I did Add a resource manually selecting the file to be a BizTalkAssembly BizTalk Server 2009 kept on reverting it back to File. Obviously it doesn’t recognize it, or recognizes that it’s an incorrect version of the framework.

Re-target the solution to .NET 3.5 or .NET 2.0

FAIL!

Why?

The following is taken from http://msdn.microsoft.com/en-us/library/ff629735(BTS.70).aspx

BizTalk Server 2010 supports building new applications only on .NET Framework version 4 . You can build both BizTalk applications as well as custom applications (such as custom functoids, custom pipeline components) using .NET Framework 4 . However, BizTalk Server 2010 continues to support the BizTalk Server 2006 R2 and BizTalk Server 2009 applications (BizTalk applications and custom applications) built on .NET Framework 3.5 SP2.

If you launch an existing BizTalk project in BizTalk Server 2010 , it automatically gets migrated, changing the .NET Framework version to 4.0. You cannot modify the .NET version number later in the Properties dialog box of the BizTalk project.

In essence it says that you cannot use the re-targeting functionality within Visual Studio 2010 for BizTalk Server 2010 projects. If you try, because the drop down is still there, you get the following dialog:

Using Files created by Visual Studio 2010, like .xsd, .btm or .odx

SUCCESS!

You can copy paste files from one project into another, without any issues I detected.

Open the project file created with Visual Studio 2010 and re-compile using Visual Studio 2008

SUCCESS! But with some issues

You can open a BizTalk project created in Visual Studio 2010 in Visual Studio 2008 and just re-compile it. Without any issues I detected. However, it does not seem like Visual Studio 2008 wants to open the solution file, which may be an issue in some situations, like for example if you wan’t to create automated builds. The structure of the solution file begins like this:

Microsoft Visual Studio Solution File, Format Version 11.00
# Visual Studio 2010
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalk Server Project1", 
"BizTalk Server Project1\BizTalk Server Project1.btproj", "{DE7C19FB-A5BA-46A0-9382-ACFB3EB91409}"

This may very well be what causes Visual Studio to dismiss it.

Hope this helps someone thinking along these lines 🙂

Disclaimer: This is tested with BizTalk Server 2010 Beta. All artifacts used while performing these tests are trivial. It might very well be that in some more complex scenario this might not work.

How To Boost Message Transformations Using the XslCompiledTransform class Extended

Problem Statement

Some of you posted the following feedback regarding my post How To Boost Message Transformations Using the XslCompiledTransform class:

“Very nice post. I just have a question. How would you handle multiple input messages in a map? I have a map that has 2 input source messages 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.”

Solution 

Well, before answering to this question, I’ll briefly explain how BizTalk Server handles maps with multiple input messages. BizTalk Server uses a trick to manage this situation. In fact, when you create a transformation map with multiple source document, BizTalk uses an envelope to wrap the individual input messages. For those of you who love to disassemble BizTalk code with Reflector, the envelope is created at runtime by the private class called CompositeStreamReader that can be found within the assembly Microsoft.XLANGs.Engine. In particular, the ConstructCompositeOutline method uses the code reported in the table below

public void ConstructCompositeOutline()
{
    ...
    XmlTextWriter writer = new XmlTextWriter(this.outlineStream, ...);
    writer.WriteStartElement("Root", "http://schemas.microsoft.com/BizTalk/2003/aggschema");
    for (int i = 0; i < this.readerCount; i++)
    {
        writer.WriteStartElement("InputMessagePart_" + i.ToString(), "");
        writer.WriteComment("_");
        writer.WriteEndElement();
    }
    writer.WriteEndElement();
    writer.Flush();
    ...
}

 

to create an envelope which targetNamespace is equal to ‘http://schemas.microsoft.com/BizTalk/2003/aggschema’ and that contains as many InputMessagePart elements as the incoming documents to the map.

<ns0:Root xmlns:ns0='http://schemas.microsoft.com/BizTalk/2003/aggschema'>
    <InputMessagePart_0>
        -
    </InputMessagePart_0>
    <InputMessagePart_1>
        -
    </InputMessagePart_1>
</ns0:Root>

 

Therefore, I decided to extend the code of my classes XslCompiledTransformHelper and XslTransformHelper to handle the case of maps with multiple input messages. In particular, I developed a new class called CompositeStream to wrap an array of Stream objects, one for each input message to the map, an return the above envelope. The implementation of this custom class is quite smart, because instead of copying the bytes of the input streams within a new buffer or a new stream, the Read method just makes up the content of the envelope with the data of the inbound streams to return a composite message with the format expected by the map. When the inbound streams are significantly large,  this approach allows saving time and memory for copying data from the inbound streams to a new object, regardless if this latter is a buffer or a stream. The code of the CompositeStream class is shown in the table below:

CompositeStream Class

 

#region Copyright
//-------------------------------------------------
// Author:  Paolo Salvatori
// Email:   [email protected]
// History: 2010-04-07 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.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 CompositeStream : Stream
    {
        #region Private Types
        enum State
        {
            Start,
            Overflow,
            Stream,
            End
        }
        #endregion

        #region Private Constants
        private const string DefaultPrefix = "babo";
        private const string StartRoot = "<{0}:Root xmlns:{0}='http://schemas.microsoft.com/BizTalk/2003/aggschema'>";
        private const string EndRoot = "</{0}:Root>";
        private const string StartInputMessagePart = "<InputMessagePart_{0}>";
        private const string EndInputMessagePart = "</InputMessagePart_{0}>";
        #endregion

        #region Private Fields
        private int currentStream = 0;
        private int currentIndex = 0;
        private string prefix;
        private State state;
        private byte[] overflowBuffer;
        private Stream[] streams;
        private bool endOfDocument = false;
        #endregion

        #region Public Constructors
        public CompositeStream(Stream[] streams)
        {
            this.prefix = DefaultPrefix;
            this.streams = streams;
            this.state = State.Start;
        }

        public CompositeStream(Stream[] streams, string prefix)
        {
            this.prefix = prefix;
            this.streams = streams;
            this.state = State.Start;
        }
        #endregion

        #region Public Properties
        public override bool CanRead
        {
            get 
            {
                return true;
            }
        }

        public override bool CanSeek
        {
            get
            {
                return true;
            }
        }

        public override bool CanTimeout
        {
            get
            {
                return false;
            }
        }

        public override bool CanWrite
        {
            get
            {
                return false;
            }
        }

        public override long Length
        {
            get
            {
                int prefixLength = prefix.Length;
                long length = 76 + 3 * prefixLength;
                if (streams != null &&
                    streams.Length > 0)
                {
                    string index;
                    for (int i = 0; i < streams.Length; i++)
                    {
                        if (streams[i].CanSeek)
                        {
                            index = i.ToString();
                            length += streams[i].Length + 39 + 2 * index.Length;
                        }
                        else
                        {
                            throw new NotImplementedException(); 
                        }
                    }
                }
                return length;
            }
        }

        public override long Position
        {
            get 
            { 
                if (state == State.Start)
                {
                    return 0L;
                }
                else
                {
                    throw new NotImplementedException(); 
                }
            }
            set
            {
                if (value == 0L)
                {
                    ResetStream();
                }
                else
                {
                    throw new NotImplementedException();
                } 
            }
        }
        #endregion

        #region Public Methods
        public override int Read(byte[] buffer, int offset, int count)
        {
            int bytesWritten = 0;
            int bytesRead = 0;
            int length = 0;
            byte[] localBuffer;
            StringBuilder builder;

            if (state == State.End)
            {
                return 0;
            }
            while (bytesWritten < count &&
                   state != State.End)
            {
                switch (state)
                {
                    case State.Start:
                        builder = new StringBuilder(128);
                        builder.AppendFormat(StartRoot, prefix);
                        if (streams != null &&
                            streams.Length > 0)
                        {
                            builder.AppendFormat(StartInputMessagePart, currentStream);
                        }
                        localBuffer = Encoding.UTF8.GetBytes(builder.ToString());

                        if (localBuffer.Length <= count)
                        {
                            Array.Copy(localBuffer, 0, buffer, offset, localBuffer.Length);
                            bytesWritten += localBuffer.Length;
                            offset += bytesWritten;
                            state = State.Stream;
                        }
                        else
                        {
                            Array.Copy(localBuffer, 0, buffer, offset, count);
                            overflowBuffer = localBuffer;
                            currentIndex = count;
                            state = State.Overflow;
                            return count;
                        }
                        break;
                    case State.Overflow:
                        length = overflowBuffer.Length - currentIndex;
                        if (length <= count)
                        {
                            Array.Copy(overflowBuffer, currentIndex, buffer, offset, length);
                            bytesWritten += length;
                            offset += length;
                            overflowBuffer = null;
                            currentIndex = 0;
                            if (endOfDocument)
                            {
                                state = State.End;
                            }
                            else
                            {
                                state = State.Stream;
                            }
                        }
                        else
                        {
                            Array.Copy(overflowBuffer, currentIndex, buffer, offset, count);
                            currentIndex += count;
                            return count;
                        }
                        break;
                    case State.Stream:
                        length = count - bytesWritten;
                        bytesRead = streams[currentStream].Read(buffer, offset, length);
                        bytesWritten += bytesRead;
                        offset += bytesRead;
                        if (bytesWritten < count)
                        {
                            builder = new StringBuilder(128);
                            builder.AppendFormat(EndInputMessagePart, currentStream);
                            currentStream++;
                            if (currentStream < streams.Length)
                            {
                                builder.AppendFormat(StartInputMessagePart, currentStream);
                                localBuffer = Encoding.UTF8.GetBytes(builder.ToString());
                            }
                            else
                            {
                                builder.AppendFormat(EndRoot, prefix);
                                localBuffer = Encoding.UTF8.GetBytes(builder.ToString());
                                endOfDocument = true;
                            }
                            if (localBuffer.Length <= count - bytesWritten)
                            {
                                Array.Copy(localBuffer, 0, buffer, offset, localBuffer.Length);
                                bytesWritten += localBuffer.Length;
                                offset += localBuffer.Length;
                                if (endOfDocument)
                                {
                                    if (bytesWritten <= count)
                                    {
                                        state = State.End;
                                    }
                                    return bytesWritten;
                                }
                                break;
                            }
                            else
                            {
                                length = count - bytesWritten;
                                Array.Copy(localBuffer, 0, buffer, offset, length);
                                overflowBuffer = localBuffer;
                                currentIndex = length;
                                state = State.Overflow;
                                return count;
                            }
                        }
                        else
                        {
                            return count;
                        }
                        break;                        
                }
            }
            return bytesWritten;
        }

        public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
        {
            return base.BeginRead(buffer, offset, count, callback, state);
        }

        public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
        {
            return base.BeginWrite(buffer, offset, count, callback, state);
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            throw new NotImplementedException();
        }

        public override void SetLength(long value)
        {
            throw new NotImplementedException();
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            if (offset == 0 &&
                origin == SeekOrigin.Begin)
            {
                ResetStream();
            }
            else
            {
                throw new NotImplementedException();
            }
            return 0;
        }

        public override void Flush()
        {
            throw new NotImplementedException();
        }
        #endregion

        #region Private Methods
        private void ResetStream()
        {
            for (int i = 0; i < streams.Length; i++)
            {
                if (streams[i].CanSeek)
                {
                    streams[i].Seek(0, SeekOrigin.Begin);
                }
                else
                {
                    throw new NotImplementedException(); 
                }
            }
            state = State.Start;
            endOfDocument = false;
            currentStream = 0;
            currentIndex = 0;
        }
        #endregion
    }
}

 

Then I extended the XslCompiledTransformHelper class with a set of new methods that accept as parameter an array of objects of type Stream or XLANGMessage and use an instance of the CompositeStream class to apply a transformation map to these latter.

XslCompiledTransformHelper Class

#region Copyright
//-------------------------------------------------
// Author:  Paolo Salvatori
// Email:   [email protected]
// 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
    {
... 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[] 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; } ... } }

 

In a similar way, I extended the XslTransformHelper class to support maps with multiple input documents. For brevity, I omitted the code of this latter, but you can download the new version of both classes here.

Test

To test the new methods exposed by the classes XslCompiledTransformHelper and XslTransformHelper, I created three XML schemas:

  • Credentials
  • Address
  • Customer

In particular, the Credentials and Address schemas define two complex types used to build the Customer schema. Then I created a transformation map (see the picture below) called AddressAndCredentialsToCustomer that accepts 2 input messages, the first of type Credentials and the second of type Address, and returns a document of type Customer.

Finally I created 2 Unit Test methods called:

  • TestXslTransformHelperWithMultipleInputs
  • TestXslCompiledTransformHelperWithMultipleInputs

to test the CompositeStream and the new methods exposed by the the classes XslCompiledTransformHelper and XslTransformHelper. You can change the entries contained in the appSettings section of the App.Config file within the UnitAndLoadTests project to test the 2 classes against your documents and multi-input-message maps.

Conclusions

I updated the original post to reflect the extensions I made. Here you can find a new version of my helper classes and the artifacts (schemas, maps, unit tests) I used to test them.

Note: I spent less than one day to write and test the new code. In particular, I conducted a basic code coverage of the CompositeStream class, but I didn’t test it with more than 2 streams or with large messages. If you find any error, please send me a repro and I’ll do my best, time permitting, to fix the code as soon as possible. Instead, should you find an error in my code and decide to sort it out by yourselves, please let me have the fixed version. 😉

How to exploit the Text In Row table option to boost BizTalk Server Performance

 

Text in Row Table Option in SQL Server

In SQL Server, text, ntext, or image fields are large character or binary strings (up to 2 gigabytes) that by default stored outside a data row. The data row contains only a 16-byte text pointer that points to the root node of a tree built of internal pointers. These pointers map the pages in which the string fragments are stored.

SQL Server provides a table option called text in row to declare that the contents of the fields of type text, ntext, or image whose dimensions are smaller than those of a data page (8Kb) must be stored in the data row. When a large value type or a large object data type column value is stored in the data row, the Database Engine does not have to access a separate page or set of pages to read or write the character or binary string. This makes reading and writing the in-row strings about as fast as reading or writing limited size varchar, nvarchar, or varbinary strings. Similarly, when the values are stored off-row, the Database Engine incurs an additional page read or write.

For more information on this topic, see the following articles:

SQL Server 2005

SQL Server 2008

How to exploit the Text in Row table option in BizTalk Server

The following section explains how and when applying the text in row table option to boost BizTalk performance.

Parts Table

When the message size is less than 8k the text in row table option can be applied to the Parts table that has 2 fields of type image:

  • imgPart: contains a message Part or message part fragment
  • imgPropBag: contains the message part property bag.

This technique can lead to a decrease of CPU usage up to 20-30% on the SQL and BizTalk nodes and to a significant improvement in terms of latency and throughput, when messages are smaller than 8kb.

Spool Table

Similarly, when the average size of the message context is less than 8 kb, this technique can be extended to the Spool table. This latter contains a column of type image:

  • imgContext: this field contains the message context.

To successfully apply this technique to the Spool table, you must eliminate unnecessary context properties and distinguished fields to reduce the size of the message context below 8 Kb.

DynamicStateInfo Tables

Finally, the text in row table option can be applied to the DynamicStateInfo_Host tables that contain the serialized state of orchestrations when they encounter a persistence point during their execution. These tables, one for each host, contain a field of type image:

  • imgData: contains binary-serialized orchestrations

When orchestrations running within a host HostA are so small that their size once serialized is less than 8 kb, the text in row technique can successfully be applied to the DynamicStateInfo_HostA table. Therefore it’s advisable to keep the internal state of orchestrations as small as possible. This technique can significantly reduce the time spent by the XLANG Engine to serialize, persist and restore the internal state of an orchestration in case of persistence point. Moreover, when the size of this latter is smaller than 8 Kb, it allows using the text in row technique with DynamicStateInfo_Host tables. The following techniques can be used to reduce the memory footprint of orchestrations:

· Create variables and messages as late as possible and release them as early as possible for example introducing non transactional scopes inside your orchestrations and declaring variables and messages within these inner scopes instead of declaring them at the top-most level.

· Use static variables and components whenever possible. When a persistence point occurs, these latter will not be serialized along with the orchestration.

Text In Row in Action

During a recent Performance and Stability review with a customer, I applied the text in row technique to the Parts table (see the picture below) of the BizTalkMsgBoxDb as the average size of messages processed by the BizTalk application was less than 2 Kb.

EXEC sp_tableoption N'Parts', 'text in row', '4000'

This led to a decrease of 22% in % Processor Time on BizTalk nodes and to a significant reduction of I/O contention on the SQL Server instance hosting the BizTalkMsgBoxDb.

References

For more information on this topic, see the following articles:

  • “What you can and can’t do with the MessageBox Database server” topic in the BizTalk Core Engine’s Weblog.
  • “BizTalk Server Database Optimization” article on MSDN.

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

[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.

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

#region Copyright
//-------------------------------------------------
// Author:  Paolo Salvatori
// Email:   [email protected]
// 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:   [email protected]
// 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.

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

<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>

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.

 

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.

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.

 

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.

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

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.

Execute method

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;
}

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

[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));
}

  • 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

<?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>

 

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:

  • http://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. 😉

How to create a custom WCF Channel that debatches an inbound message

Introduction

In the following post I’ll explain in detail a demo that I presented at TechReady 9 and TechEd 2009 in Berlin.

The Problem

Consider the following scenario:

Your BizTalk application receives an XML request message through a two-way Receive Location (e.g. WCF, SOAP, HTTP). The inbound document contains multiple elements. For each item contained in the request message, the application needs to synchronously invoke a downstream WCF service that returns a response message. The application needs to process all the elements contained in the request document as a unit of work and aggregate the result of individual calls in a single response message that will be returned to the original caller.

Let’s make an example to better scope the problem. Let’s assume that our BizTalk application is exposed via a two-way WCF Receive Location and receives a request message, as the one shown below, containing multiple Operation elements.

CalculatorRequest Message

<CalculatorRequest xmlns="http://Microsoft.BizTalk.CAT.Samples.CalculatorRequest">
  <Method>XmlDocumentOrchestration</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>
      <Operation>
      <Operator>+</Operator>
      <Operand1>100</Operand1>
      <Operand2>32</Operand2>
    </Operation>
  </Operations>
</CalculatorRequest>

 

For each Operation element contained in the inbound XML document the application has to read the child Operator element and accordingly invoke one of the four methods (Add, Subtract, Multiply, Divide) exposed by the downstream WCF service. This latter is a variation of the CalculatorService that can be found on MSDN between the WCF samples. The return value of the individual calls to the CalculatorService must be aggregated in a single response message, as the one shown below. This latter will be finally returned to the original caller.

CalculatorResponse Message

<CalculatorResponse xmlns="http://Microsoft.BizTalk.CAT.Samples.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>
            <Result>
                  <Value>132</Value>
                  <Error>None</Error>
            </Result>
      </Results>
</CalculatorResponse>

 

The easier way to solve the above problem is to create a BizTalk application that exploits an orchestration to implement the Scatter-Gather design pattern. The orchestration in question should perform the following steps:

  • Receive the request message.
  • Extract and loop through the individual Operation elements, for example using the XPath function.
  • Use a Decide shape with 4 branches, one for each possible value of the Operator element.
  • Within each branch, apply a transformation map and invoke one of the 4 Operations (Add, Subtract, Multiply, Divide) exposed by a two-way Solicit-Response Logical Port.
  • Receive the response message from the underlying WCF service.
  • At each iteration, save the result in a collection variable.
  • Create and return a response message containing results.

The following graph depicts the architecture of the solution described above. In the remainder of this post, I’ll refer to this solution as LogicalPortsOrchestration.

 

Message Flow:

  1. A WCF-BasicHttp or WCF-Custom Request-Response Receive Location receives a new CalculatorRequest xml document from a Test Agent.
  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 LogicalPortsOrchestration. This latter uses a Direct Port receive the CalculatorRequest messages with the Method promoted property = “LogicalPortsOrchestration”.
  4. The LogicalPortsOrchestration uses a loop to retrieve operations and for each item it invokes the downstream Calculator WCF service using a Logical Solicit-Response Port bound to a Physical Solicit-Response WCF-BasicHttp Send Port. The request message for the Calculator WCF web service is created using an helper component and published to the (BizTalkMsgBoxDb).
  5. The request message is consumed by a WCF-BasicHttp Send Port.
  6. The WCF-BasicHttp Send Port invokes one of the methods (Add, Subtract, Multiply, Divide) exposed by the Calculator WCF service.
  7. The Calculator WCF service returns a response message.
  8. The response message is published to the MessageBox (BizTalkMsgBoxDb).
  9. The response message is returned to the caller LogicalPortsOrchestration. The orchestration repeats this pattern for each operation within the inbound CalculatorRequest xml document.
  10. The LogicalPortsOrchestration creates and publishes the CalculatorResponse message to the MessageBox (BizTalkMsgBoxDb).
  11. The response message is retrieved by the Request-Response WCF-BasicHttp or WCF-Custom Receive Location.
  12. The response message is returned to the Test Agent.

The solution described above works fine but as you can easily note it requires 4 distinct roundtrips to the MessageBox for each individual call to the underlying WCF service and therefore this architectural approach is not adequate when performance goals in terms of latency and throughput are very aggressive. More in general, whenever possible you should minimize the use of orchestrations and privilege messaging only patterns to increase the overall throughput and reduce the latency of the business processes. However, the Gather-Scatter and Service Composition are 2 scenarios that can be easily implemented using an orchestration, so the question is: how can I solve the same problem using a different pattern?

An alternative solution could be using the Inline Send design pattern to solve the problem: in this case to invoke the underlying CalculatorService our orchestration would not use a Logical Port bound to Physical WCF Send Port,  but it rather use a custom WCF proxy component within an Expression shape that would replace the Send and Receive shapes. Note that not using Adapters and Physical Send Ports, the application would not benefit from their functional capabilities such as batching, retries, correlation sets initialization, declarative configuration and secondary transports. The advantage of the Inline Send technique is that for each Solicit Response call made by the orchestration, it allows to eliminate 4 roundtrips to the MessageBox:

  • Orchestration Logical Port –> Request –> MessageBox
  • MessageBox –> Request –> Physical Send Port
  • Physical Send Port –> Response –> MessageBox
  • MessageBox –> Response –> Orchestration Logical Port

The following graph depicts the architecture of the solution based on the Inline Send pattern. In the remainder of this post, I’ll refer to this solution as InlineSendOrchestration.

Message Flow:

  1. A WCF-BasicHttp or WCF-Custom Request-Response Receive Location receives a new CalculatorRequest xml document from a Test Agent.
  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 InlineSendOrchestration. This latter uses a Direct Port receive the CalculatorRequest messages with the Method promoted property = “InlineSendOrchestration”.
  4. The InlineSendOrchestration uses a loop to retrieve operations and for each item it invokes the downstream Calculator WCF service using a generic WCF proxy component called InlineSendProxy. The endpoint used by this object must be configured in the BtsNtSvc.exe.config and BtsNtSvc64.exe.config configurations files. See the BtsNtSvc.exe.config file in the Setup folder for a sample of how configuring this component to access the downstream Calculator WCF web service.
  5. The InlineSendProxy component directly invokes one of the methods (Add, Subtract, Multiply, Divide) exposed by the Calculator WCF service.
  6. The Calculator WCF service returns a response message.
  7. The response message is written in the outbound CalculatorResponse document using an XmlWriter and a VirtualStream objects. The orchestration repeats this pattern for each operation within the inbound CalculatorRequest xml document.
  8. The InlineSendOrchestration publishes the CalculatorResponse message to the MessageBox (BizTalkMsgBoxDb).
  9. The response message is retrieved by the Request-Response WCF-BasicHttp or WCF-Custom Receive Location.
  10. The response message is returned to the Test Agent.

In February 2009, the BizTalk Customer Advisory Team conducted a performance lab to determine and compare the performance in terms of latency and throughput of the LogicalPortsOrchestration and InlineSendOrchestration patterns. To this purpose, we used a lab rig composed of 2 BizTalk Server 2009 (Beta version) nodes (2 Dual-Core CPUs, 8 GB RAM), a SQL Server 2008 machine to host the MessageBox. The following table reports the results obtained using a request message that contained 5 distinct Operations:

Test Case Concurrent Test Client Users Messages/Second Average Response Time (Sec)
LogicalPortsOrchestration 100 60.64 1.61
InlineSendOrchestration 100 236.96 0.24

 

As you can easily note reviewing the numbers reported in the table above, the InlineSendOrchestration implementation provides better performance than the LogicalPortsOrchestration solution, both in terms of latency and throughput. Therefore, a question arises naturally: should I renounce to the capabilities provided by Adapters and apply the Inline Send pattern to debatch an incoming message and implement the Scatter-Gather pattern in a performant way? Fortunately, the answer is no as there’s another architectural approach to solve the problem using a messaging-only approach that does not require any orchestration.

The following picture depicts the architecture of the third solution that I’ll refer to as MessagingOnly. Unfortunately, this solution was developed after the performance lab, so no comparative tests were conducted to compare its latency and throughput with the 2 preceding implementations. Nevertheless, at the end of this article I provide a link to the code of the 3 implementations, so you can eventually run your own tests and eventually customize the solution to fit your needs.

Message Flow:

  1. A WCF-BasicHttp or WCF-Custom Request-Response Receive Location receives a new CalculatorRequest xml document from a Test Agent.
  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 is consumed by the POG.MessagingOnly.WCF-Custom.SendPort WCF-Custom Send Port. This latter uses a Filter Expression to receive CalculatorRequest messages with the Method promoted property = “MessagingOnly”.
  4. The WCF-Custom Send Port uses a Custom Binding which contains the following binding elements: CalculatorServiceBindingElement, TextMessageEncodingBindingElement, HttpTransportBindingElement. The custom CalculatorServiceBindingElement at runtime creates a ServiceFactoryChannelFactory object that in turn creates a CalculatorServiceRequestChannel object. The Request method of this latter debatches the Operation elements contained in the inbound CalculatorRequest message, and for each of them  makes a separate call to the downstream CalculatorService using the reference to the inner HttpTransportChannel.
  5. The CalculatorService returns a response message. The custom channel executed by the WCF Adapter repeats this pattern for each Operation element within the inbound CalculatorRequest xml document. The custom channel stores the return value of individual calls in a collection variable and finally uses results to generate the CalculatorResponse message (Scatter-Gather pattern) that is finally returned by the WCF-Custom Adapter.
  6. The POG.MessagingOnly.WCF-Custom.SendPort WCF-Custom Send Port publishes the CalculatorResponse message to the MessageBox (BizTalkMsgBoxDb).
  7. The response message is retrieved by the Request-Response WCF-BasicHttp or WCF-Custom Receive Location.
  8. The response message is returned to the Test Agent.

As I have noted in my previous article, WCF Bindings provide a mechanism for configuring channel stacks. In other words, a binding defines a precise recipe for building a channel stack using a transport channel, a message encoder, and a set of protocol channels. WCF ships with several built-in bindings that target common communication scenarios, such as the BasicHttpBinding, WsHttpBinding and NetTcpBinding and BizTalk Server provides a full range of WCF Adapters that correspond 1:1 to the most commonly used WCF bindings:

  • WCF-BasicHttp
  • WCF-WSHttp
  • WCF-NetTcp
  • etc.

However, if you need maximum flexibility and you need to use one or multiple custom protocol channels at runtime, you can use the CustomBinding that gives you the possibility to control which binding elements compose your binding. Likewise, in a BizTalk application, if you want to extend the default behavior of the WCF Adapters with a custom component (Service Behavior, Endpoint Behavior, Custom Binding, Custom Binding Element), you have to use the WCF-Custom and/or WCF-CustomIsolated Adapters. In fact, these latter offer you complete control over the channel stack and behaviors configuration, and as a consequence, they are the only WCF adapters you really need. Compared with the other WCF Adapters, they are the only ones to provide the possibility to:

  • Implement and exploit extensibility points.
  • Have full access to properties exposed by bindings/behaviors.
  • Enable the use of the bamInterceptor endpoint behavior.
  • Export/Import the binding configuration.
  • Disable a receive location on failure.
  • Run an http-based RL within an in-process host.
  • Use bindings (e.g. wsDualHttpBinding) for which a WCF Adapter does not exist.

The Code

Before looking at the code of the custom channel, let’s complicate the scenario as follows: let’s assume that the underlying WCF Calculator service invoked by the BizTalk application is hosted by IIS 7.0 and exposes an endpoint that uses a CustomBinding composed of the following binding elements:

  • TransactionFlowBindingElement
  • TextMessageEncodingBindingElement
  • HttpTransportBindingElement

The following picture reports the web.config used by the Calculator service:

Web.config

 

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
        <add name="LogDB"
             providerName="System.Data.SqlClient"
             connectionString="Data Source=.;Initial Catalog=LogDB;Integrated Security=SSPI;" />
    </connectionStrings>

    <system.diagnostics>
        <sources>
            <source name="System.ServiceModel.MessageLogging" switchValue="Warning, ActivityTracing">
                <listeners>
                    <add type="System.Diagnostics.DefaultTraceListener" name="Default">
                        <filter type="" />
                    </add>
                    <add name="ServiceModelMessageLoggingListener">
                        <filter type="" />
                    </add>
                </listeners>
            </source>
        </sources>
        <sharedListeners>
            <add initializeData="C:\calculatorservicetxn.svclog"
                 type="System.Diagnostics.XmlWriterTraceListener, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
                 name="ServiceModelMessageLoggingListener"
                 traceOutputOptions="Timestamp">
                <filter type="" />
            </add>
        </sharedListeners>
    </system.diagnostics>
    
    <system.serviceModel>    
        <diagnostics performanceCounters="All">
            <messageLogging logEntireMessage="false"
                            logMalformedMessages="false"
                            logMessagesAtServiceLevel="false"
                            logMessagesAtTransportLevel="false"
                            maxSizeOfMessageToLog="1000000" />
        </diagnostics>

    <!-- Service Endpoints -->
        <services>
            <service behaviorConfiguration="CalculatorServiceBehavior"
                     name="Microsoft.ServiceModel.Samples.CalculatorService">
                <endpoint address=""
                          binding="customBinding"
                          bindingConfiguration="customBinding"
                          name="customBinding"
                          contract="Microsoft.ServiceModel.Samples.ICalculator" />
            </service>
        </services>
        
    <!-- Binding Condfiguration -->
        <bindings>
            <customBinding>
                <binding name="customBinding">
                    <transactionFlow transactionProtocol="WSAtomicTransactionOctober2004" />
                    <textMessageEncoding />
                    <httpTransport />
                </binding>
            </customBinding>
        </bindings>

        <!-- Service Behaviors -->
        <behaviors>
            <serviceBehaviors>
                <behavior name="CalculatorServiceBehavior">
                    <serviceMetadata httpGetEnabled="true" />
                    <serviceDebug includeExceptionDetailInFaults="true" />
                    <serviceThrottling maxConcurrentCalls="200" 
                                       maxConcurrentSessions="200"
                                       maxConcurrentInstances="200" />
                </behavior>
            </serviceBehaviors>
        </behaviors>
    </system.serviceModel>
    
    <system.web>
        <customErrors mode="RemoteOnly"/>
        <trust level="Full" />
    </system.web>
</configuration>

 

The Calculator service requires the client application to create and flow a transaction using the WS-AtomicTransaction protocol. In particular, the WCF service will use this client-initiated transaction to log the data of the invoked operation to a SQL database. To implement this behavior, all the service operations exposed by the ICalculatorService contract interface have been decorated with the TransactionFlowAttribute. In particular, the TransactionFlowOption.Mandatory value has been used in the constructor of the TransactionFlow attribute to indicate that the client application must initiate and transmit a transaction, otherwise the service operation will immediately throw an exception.

In the CalculatorService class, all the service operations have been decorated with the OperationBehaviorAttribute. In particular, the value of the TransactionScopeRequired and TransactionAutoComplete properties has been set to true. This indicates that service operations require a transaction scope for their execution and this latter automatically completes if no unhandled exceptions occur and the method returns successfully.
All the service operations invoke the RecordToLog method that logs the current operation data and transactionId to the LogDB on SQL Server. This sample on MSDN demonstrates various aspects of creating a transactional service and the use of a client-initiated transaction to coordinate service operations.

Note in particular that the Transaction.Current.TransactionInformation.DistributedIdentifier static property is used by the RecordToLog method to retrieve the unique identifier of the client-initiated distributed transaction.

CalculatorService Class

#region Using Directives
using System;
using System.Diagnostics;
using System.ServiceModel;
using System.Transactions;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
#endregion

namespace Microsoft.ServiceModel.Samples
{
    // Define a service contract.
    [ServiceContract(Namespace = "http://Microsoft.ServiceModel.Samples")]
    public interface ICalculator
    {
        #region Contract Operations
        [OperationContract]
        [TransactionFlow(TransactionFlowOption.Mandatory)]
        double Add(double n1, double n2);

        [OperationContract]
        [TransactionFlow(TransactionFlowOption.Mandatory)]
        double Subtract(double n1, double n2);

        [OperationContract]
        [TransactionFlow(TransactionFlowOption.Mandatory)]
        double Multiply(double n1, double n2);

        [OperationContract]
        [TransactionFlow(TransactionFlowOption.Mandatory)]
        double Divide(double n1, double n2);
        #endregion
    }

    // Service class which implements the service contract.
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Single)]
    public class CalculatorService : ICalculator
    {
        #region Private Constants
        private const string AttemptedToDivideByZero = "Attempted to divide by zero.";
        private const string TransactionIdFormat = "[CalculatorService] TransactionId: {0}";
        private const string CallCompletedFormat = "[CalculatorService] Message [{0}] stored to LogDB.";
        private const string MessageFormat = "[CalculatorService] {0}";
        private const string InsertLogEntrySP = "usp_InsertLogEntry";
        private const string IdParameter = "@Id";
        private const string MessageParameter = "@Message";
        private const string TypeParameter = "@Type";
        private const string SourceParameter = "@Source";
        private const string TransactionIdParameter = "@TransactionID";
        private const string Information = "Information";
        private const string Source = "CalculatorService";
        private const string None = "None";
        private const string AddFormat = "{0} + {1} = {2}";
        private const string SubtractFormat = "{0} - {1} = {2}";
        private const string MultiplyFormat = "{0} * {1} = {2}";
        private const string DivideFormat = "{0} / {1} = {2}";
        #endregion

        #region Private Static Fields
        private static string connectionString = null;
        #endregion

        #region Static Constructor
        static CalculatorService()
        {
            try
            {
                if (ConfigurationManager.ConnectionStrings != null &&
                    ConfigurationManager.ConnectionStrings.Count > 0)
                {
                    connectionString = ConfigurationManager.ConnectionStrings["LogDB"].ConnectionString;
                }
            }
            catch (Exception ex)
            {
                Trace.WriteLine(string.Format(MessageFormat, ex.Message));
            }
        }
        #endregion

        #region Public Methods
        [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]
        public double Add(double n1, double n2)
        {
            double n3 = n1 + n2;
            RecordToLog(String.Format(AddFormat, n1, n2, n3));
            return n3;
        }

        [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]
        public double Subtract(double n1, double n2)
        {
            double n3 = n1 - n2;
            RecordToLog(String.Format(SubtractFormat, n1, n2, n3));
            return n3;
        }

        [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]
        public double Multiply(double n1, double n2)
        {
            double n3 = n1 * n2;
            RecordToLog(String.Format(MultiplyFormat, n1, n2, n3));
            return n3;
        }

        [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]
        public double Divide(double n1, double n2)
        {
            if (n2 == 0)
            {
                Trace.WriteLine(AttemptedToDivideByZero);
                throw new DivideByZeroException();
            }
            double n3 = n1 / n2;
            RecordToLog(String.Format(DivideFormat, n1, n2, n3));
            return n3;
        }

        #endregion

        #region Private Static Methods
        private static void RecordToLog(string message)
        {
            try
            {
                if (!string.IsNullOrEmpty(connectionString))
                {
                    string transactionId = None;
                    if (Transaction.Current != null &&
                        Transaction.Current.TransactionInformation != null)
                    {
                        transactionId = Transaction.Current.TransactionInformation.DistributedIdentifier.ToString();
                    }
                    Trace.WriteLine(string.Format(TransactionIdFormat, transactionId));
                    using (SqlConnection sqlConnection = new SqlConnection(connectionString))
                    {
                        sqlConnection.Open();
                        SqlParameter sqlParameter = null;

                        using (SqlCommand sqlCommand = new SqlCommand(InsertLogEntrySP, sqlConnection))
                        {
                            sqlParameter = new SqlParameter(IdParameter, SqlDbType.UniqueIdentifier);
                            sqlParameter.Direction = ParameterDirection.Input;
                            sqlParameter.Value = Guid.NewGuid();
                            sqlCommand.Parameters.Add(sqlParameter);

                            sqlParameter = new SqlParameter(MessageParameter, SqlDbType.VarChar, 512);
                            sqlParameter.Direction = ParameterDirection.Input;
                            sqlParameter.Value = message.Length <= 512 ? message : message.Substring(0, 512);
                            sqlCommand.Parameters.Add(sqlParameter);

                            sqlParameter = new SqlParameter(TypeParameter, SqlDbType.VarChar, 64);
                            sqlParameter.Direction = ParameterDirection.Input;
                            sqlParameter.Value = Information;
                            sqlCommand.Parameters.Add(sqlParameter);

                            sqlParameter = new SqlParameter(SourceParameter, SqlDbType.VarChar, 64);
                            sqlParameter.Direction = ParameterDirection.Input;
                            sqlParameter.Value = Source;
                            sqlCommand.Parameters.Add(sqlParameter);

                            sqlParameter = new SqlParameter(TransactionIdParameter, SqlDbType.VarChar, 64);
                            sqlParameter.Direction = ParameterDirection.Input;
                            sqlParameter.Value = transactionId;
                            sqlCommand.Parameters.Add(sqlParameter);

                            sqlCommand.CommandType = CommandType.StoredProcedure;
                            sqlCommand.ExecuteNonQuery();

                            Trace.WriteLine(string.Format(CallCompletedFormat, message));
                        }                        
                    }
                }
            }
            catch (Exception ex)
            {
                Trace.WriteLine(string.Format(MessageFormat, ex.Message));
            }
        }
        #endregion
    }
}

 

Now let’s assume that all the operations contained in a CalculatorRequest document must be processed as a unit of work within the same distributed transaction.  In this case, the BizTalk application will have to perform the following steps:

  • Initiate a MSDTC transaction.
  • Debatch the incoming CalculatorRequest document to extract individual Operation elements.
  • For each Operation, create a request message and invoke the corresponding operation exposed by the CalculatorService, making sure to flow the distributed transaction as part of each service call.

The following graph depicts the architecture of the described solution.

Message Flow:

  1. A WCF-BasicHttp or WCF-Custom Request-Response Receive Location receives a new CalculatorRequest xml document from a Test Agent.
  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 is consumed by the POG.MessagingOnly.WCF-Custom.SendPort WCF-Custom Send Port. This latter uses a Filter Expression to receive CalculatorRequest messages with the Method promoted property = “MessagingOnly”.
  4. The WCF-Custom Send Port uses a Custom Binding which contains the following binding elements: TransactionFlowBindingElement, CalculatorServiceBindingElement, TextMessageEncodingBindingElement, HttpTransportBindingElement. The TransactionFlowBindingElement at runtime creates a TransactionFlowChannel that creates the CoordinationContext header that is used to coordinate the distributed transaction across BizTalk and the WCF service. The custom CalculatorServiceBindingElement at runtime creates a ServiceFactoryChannelFactory object that in turn creates a CalculatorServiceRequestChannel object. The Request method of this latter debatches the Operation elements contained in the inbound CalculatorRequest message, and for each of them  creates a new WCF message, copies to it the CoordinationContext header from the CalculatorRequest message, and makes a separate call to the downstream CalculatorService using the reference to the inner HttpTransportChannel.
  5. The CalculatorService logs the operation data to the LogDB on SQL Server using the BizTalk-initiated transaction.
  6. The CalculatorService returns a response message. The custom channel executed by the WCF Adapter repeats this pattern for each Operation element within the inbound CalculatorRequest xml document. The custom channel stores the return value of individual calls in a collection variable and finally uses results to generate the CalculatorResponse message (Scatter-Gather pattern) that is finally returned by the WCF-Custom Adapter.
  7. The POG.MessagingOnly.WCF-Custom.SendPort WCF-Custom Send Port publishes the CalculatorResponse message to the MessageBox (BizTalkMsgBoxDb).
  8. The response message is retrieved by the Request-Response WCF-BasicHttp or WCF-Custom Receive Location.
  9. The response message is returned to the Test Agent.

On the left hand side, the following picture shows the custom binding used by the WCF-Custom Send Port, while on the right hand side, it describes more in detail the individual steps performed by the WCF-Custom Send Port.

Message Flow:

  1. The BizTalk Messaging Runtime passes the CalculatorRequest xml document to the WCF-Custom Adapter.
  2. The WCF-Custom Adapter generates a WCF Message instance from the IBaseMessage object. The WCF Message contains a SOAP Envelope and in particular the Body element contains the CalculatorRequest as payload.
  3. The TransactionRequestChannel creates the CoordinationContext header that is used to coordinate the distributed transaction across BizTalk and the CalculatorService.
  4. The CalculatorServiceRequestChannel debatches the first operation element contained in the inbound CalculatorRequest message, copies the CoordinationContext header from the original batch message to the new message and makes a separate call to the downstream CalculatorService using the reference to the inner HttpTransportChannel.
  5. The CalculatorServiceRequestChannel stores the return value of the first call in a collection.
  6. The CalculatorServiceRequestChannel debatches the second operation element contained in the inbound CalculatorRequest message, copies the CoordinationContext header from the original batch message to the new message and makes a separate call to the downstream CalculatorService using the reference to the inner HttpTransportChannel. The CalculatorServiceRequestChannel  repeats the same pattern for the remaining operation items contained in the inbound batch message.
  7. The CalculatorServiceRequestChannel stores the return value of the second call in a collection.
  8. The CalculatorServiceRequestChannel finally uses the result value of individual calls to generate the CalculatorResponse message (Scatter-Gather pattern) that is finally returned by the WCF-Custom Adapter to the BizTalk Messaging Runtime.

Let’s look at the code. To create a custom protocol channel to debatch the request message at runtime within the WCF Send Port I built a Class Library called WCFExtensionLibrary composed of multiple components. As we noted in the first part of this article, on the sending side, a binding is used to build a IChannelFactory, which in turn builds a channel stack and returns a reference to the top channel in the stack. The application can then use this channel to send messages. A binding consists of an ordered set of binding elements that inherit from the BindingElement class. Each binding element at runtime creates a IChannelFactory, that in turn creates a channel that can be used to process the message in a given point of the channel stack. BizTalk developers can see the protocol channels that compose a channel stack as the pipeline components within a pipeline on a Receive Location or Send Port, and the transport channel as the transport Adapter used to receive or send a message. The only difference between the 2 approaches is that to create a custom pipeline component it’s necessary to create a single class, while to create a custom channel at runtime it’s necessary to create the multiple classes. Let’s see in detail the components that I created to register, configure, and execute my custom channel within the WCF-Custom Send Port.

CalculatorServiceBindingElement Class

This class inherits from the BindingElement class. This custom binding element exposes the following properties:

  • TraceEnabled: Gets or Sets a value indicating whether tracing is enabled.
  • MaxBufferSize: Gets or sets the maximum size for a buffer used the custom channel when creating a WCF message.
  • MessageVersion: Gets or sets the SOAP message and WS-Addressing versions that are expected by the target WCF service. This information is used at runtime by the custom channel to specify the version of request messages to send to the CalculatorService.

The BuildChannelFactory method initializes the CalculatorServiceChannelFactory that is responsible for creating an instance of the CalculatorServiceRequestChannel at runtime.

CalculatorServiceBindingElement Class

 

 

#region Using Directives
using System;
using System.ServiceModel.Channels;
using System.ServiceModel; 
#endregion

namespace Microsoft.BizTalk.CAT.Samples.WCFExtensionLibrary
{
    public class CalculatorServiceBindingElement : BindingElement
    {
        #region Private Fields
        private bool traceEnabled = true;
        private int maxBufferSize = 2097152;
        private MessageVersionEnum messageVersion = MessageVersionEnum.Default;
        #endregion

        #region Public Constructors
        public CalculatorServiceBindingElement()
        {
        }

        protected CalculatorServiceBindingElement(CalculatorServiceBindingElement other)
            : base(other)
        {
            this.traceEnabled = other.traceEnabled;
            this.maxBufferSize = other.maxBufferSize;
            this.messageVersion = other.messageVersion;
        } 
        #endregion

        #region Public Methods
        public override BindingElement Clone()
        {
            return new CalculatorServiceBindingElement(this);
        }

        public override bool CanBuildChannelFactory<TChannel>(BindingContext context)
        {
            return context.CanBuildInnerChannelFactory<TChannel>();
        }

        public override bool CanBuildChannelListener<TChannel>(BindingContext context)
        {
            return context.CanBuildInnerChannelListener<TChannel>();
        }

        public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)
        {
            return new CalculatorServiceChannelFactory<TChannel>(context, traceEnabled, maxBufferSize, messageVersion);
        }

        public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context)
        {
            return new CalculatorServiceChannelListener<TChannel>(context, traceEnabled, maxBufferSize, messageVersion);
        }

        public override T GetProperty<T>(BindingContext context)
        {
            return context.GetInnerProperty<T>();
        } 
        #endregion

        #region Public Properties
        public bool TraceEnabled
        {
            get
            {
                return this.traceEnabled;
            }
            set
            {
                this.traceEnabled = value;
            }
        }

        public int MaxBufferSize
        {
            get
            {
                return this.maxBufferSize;
            }
            set
            {
                this.maxBufferSize = value;
            }
        }

        public MessageVersionEnum MessageVersion
        {
            get
            {
                return this.messageVersion;
            }
            set
            {
                this.messageVersion = value;
            }
        }
        #endregion
    }
}

 

CalculatorServiceBindingExtensionElement Class

This class inherits from the BindingElementExtensionElement class. In general, a BindingElementExtensionElement enables the use of a custom BindingElement implementation from a machine or application configuration file. In our case, the CalculatorServiceBindingExtensionElement has been created to achieve two results:

  • Register the CalculatorServiceBindingElement in the machine.config to make it visible to BizTalk when defining a WCF-Custom/WCF-CustomIsolated Receive Location or a WCF-Custom Send Port.
  • Define the properties that can be set in a declarative way when configuring a WCF-Custom/WCF-CustomIsolated Receive Location or a WCF-Custom Send Port.

The CalculatorServiceBindingExtensionElement class exposes the same properties as the CalculatorServiceBindingElement. The CreateBindingElement method creates an instance of CalculatorServiceBindingElement class and assign a value to its properties. In practice, the following classes: 

  • CalculatorServiceBindingExtensionElement
  • CalculatorServiceBindingElement
  • CalculatorServiceChannelFactory
  • CalculatorServiceRequestChannel

all expose the same properties. As we’ll see later on, these latter can be set when defining a WCF-Custom/WCF-CustomIsolated Receive Location or a WCF-Custom Send Port and at runtime they are propagated from the custom BindingElementExtensionElement to the custom channel as follows:

BindingElementExtensionElement –> CalculatorServiceBindingElement–>CalculatorServiceChannelFactory –> CalculatorServiceRequestChannel

CalculatorServiceBindingExtensionElement Class

namespace Microsoft.BizTalk.CAT.Samples.WCFExtensionLibrary
{
    public enum MessageVersionEnum
    {
        Default,
        None,
        Soap11,
        Soap11WSAddressing10,
        Soap11WSAddressingAugust2004,
        Soap12, 
        Soap12WSAddressing10,
        Soap12WSAddressingAugust2004
    }

    public class CalculatorServiceBindingExtensionElement : BindingElementExtensionElement
    {
        #region Private Constants
        private const string TraceEnabledProperty = "TraceEnabled";
        private const string MaxBufferSizeProperty = "MaxBufferSize";
        private const string MessageVersionProperty = "MessageVersion";
        #endregion

        #region Private Fields
        private CalculatorServiceBindingElement calculatorServiceBindingElement;
        #endregion

        #region Public Constructors
        public CalculatorServiceBindingExtensionElement()
        {
        }
        #endregion

        #region Public Properties
        public override Type BindingElementType
        {
            get
            {
                return typeof(CalculatorServiceBindingElement);
            }
        }
        #endregion

        #region Public Methods
        public override void ApplyConfiguration(BindingElement bindingElement)
        {
            base.ApplyConfiguration(bindingElement);
            calculatorServiceBindingElement = (CalculatorServiceBindingElement)bindingElement;
        }
        #endregion

        #region Protected Methods
        protected override BindingElement CreateBindingElement()
        {
            calculatorServiceBindingElement = new CalculatorServiceBindingElement();
            calculatorServiceBindingElement.TraceEnabled = this.TraceEnabled;
            calculatorServiceBindingElement.MaxBufferSize = this.MaxBufferSize;
            calculatorServiceBindingElement.MessageVersion = this.MessageVersion;
            this.ApplyConfiguration(calculatorServiceBindingElement);
            return calculatorServiceBindingElement;
        }
        #endregion

        #region Configuration Properties
        [ConfigurationProperty(TraceEnabledProperty, DefaultValue = true)]
        public bool TraceEnabled
        {
            get
            {
                return (bool)base[TraceEnabledProperty];
            }
            set
            {
                base[TraceEnabledProperty] = value;
            }
        }

        [ConfigurationProperty(MaxBufferSizeProperty, DefaultValue = 2097152)]
        [IntegerValidator(MinValue = 0)]
        public int MaxBufferSize
        {
            get
            {
                return (int)base[MaxBufferSizeProperty];
            }
            set
            {
                base[MaxBufferSizeProperty] = value;
            }
        }

        [ConfigurationProperty(MessageVersionProperty, DefaultValue = MessageVersionEnum.Default)]
        public MessageVersionEnum MessageVersion
        {
            get
            {
                return (MessageVersionEnum)base[MessageVersionProperty];
            }
            set
            {
                base[MessageVersionProperty] = value;
            }
        }
        #endregion
    }
}

CalculatorServiceChannelFactory and CalculatorServiceRequestChannel Classes

The CalculatorServiceChannelFactory inherits from ChannelFactoryBase that, as the name suggests, provides a common base implementation for channel factories on the client to create channels of a specified type connected to a specified address. In particular, at runtime the OnCreateChannel method creates and returns a new instance of the CalculatorServiceRequestChannel class. Before doing that, the method uses the reference to the inner channel factory to create an instance of the inner channel, that in our case is an instance of the HttpTransportChannel class, and then passes this reference in the constructor of the CalculatorServiceRequestChannel. The custom channel will exploit the reference to the http transport channel to send multiple messages to the CalculatorService, one for each Operation element contained in the inbound request message.

The CalculatorServiceRequestChannel is the class responsible for processing the CalculatorRequest message at runtime. The Request method uses a XmlDictionaryReader object  to read through and disassembles the Operation elements contained within the inbound xml message and for each of them it creates and submits a separate message to the underlying CalculatorService. The The CreateOperationMessage method copies the headers of the CalculatorRequest message to each individual message sent to WCF service. One of these headers is the CoordinationContext used to coordinate the distributed transaction. To invoke the WCF service, the custom channel invokes the Request method exposed by the inner channel. Individual call responses are stored in a collection called  responseList. Finally, the Request method invokes the CreateResponseMessage function to create and return the response message that aggregates the result of individual calls.

CalculatorServiceChannelFactory and CalculatorServiceRequestChannel Classes

#region Using Directives
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Transactions;
using System.Xml;
using System.Text;
#endregion

namespace Microsoft.BizTalk.CAT.Samples.WCFExtensionLibrary
{
    /// <summary>
    /// ChannelFactory that performs message inspection
    /// </summary>
    class CalculatorServiceChannelFactory<TChannel>
        : ChannelFactoryBase<TChannel>
    {
        #region Private Constants
        private const string ErrorMessageFormat = 
"<CalculatorResponse xmlns=\"http://Microsoft.BizTalk.CAT.Samples.Schemas.CalculatorResponse\"><Status>{0}
</Status></CalculatorResponse>"
; private const string CalculatorResponseNamespace = "http://Microsoft.BizTalk.CAT.Samples.Schemas.CalculatorResponse"; private const string GenericErrorMessage = "An error occured while processing the request."; private const string OperationUnknownErrorMessageFormat = "The operation failed because the operator {0} is unknown."; private const string OperationFailedErrorMessageFormat = "The operation number {0} returned an error. {1}"; private const string OperationFormat = "[CalculatorServiceRequestChannel] {0} {1} {2} = {3}"; private const string OperationFailed = "[CalculatorServiceRequestChannel] {0} {1} {2} Failed: {3}"; private const string MessageReceived = "[CalculatorServiceRequestChannel] Request message received."; private const string MessageSuccessfullyProcessed =
"[CalculatorServiceRequestChannel] Response message successfully processed."; private const string TransactionIdFormat = "[CalculatorServiceRequestChannel] TransactionId: {0}"; #endregion #region Private Fields IChannelFactory<TChannel> innerChannelFactory; private bool traceEnabled = true; private int maxBufferSize = 2097152; private MessageVersionEnum messageVersion = MessageVersionEnum.Default; #endregion #region Public Constructors public CalculatorServiceChannelFactory(BindingContext context, bool traceEnabled, int maxBufferSize, MessageVersionEnum messageVersion) { this.innerChannelFactory = context.BuildInnerChannelFactory<TChannel>(); this.traceEnabled = traceEnabled; this.maxBufferSize = maxBufferSize; this.messageVersion = messageVersion; if (this.innerChannelFactory == null) { throw new InvalidOperationException("CalculatorServiceChannelFactory requires an inner IChannelFactory."); } } #endregion #region Public Methods public override T GetProperty<T>() { T baseProperty = base.GetProperty<T>(); if (baseProperty != null) { return baseProperty; } return this.innerChannelFactory.GetProperty<T>(); } #endregion #region Protected Methods protected override void OnOpen(TimeSpan timeout) { this.innerChannelFactory.Open(timeout); } protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state) { return this.innerChannelFactory.BeginOpen(timeout, callback, state); } protected override void OnEndOpen(IAsyncResult result) { this.innerChannelFactory.EndOpen(result); } protected override void OnClose(TimeSpan timeout) { this.innerChannelFactory.Close(timeout); } protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state) { return this.innerChannelFactory.BeginClose(timeout, callback, state); } protected override void OnEndClose(IAsyncResult result) { this.innerChannelFactory.EndClose(result); } protected override TChannel OnCreateChannel(EndpointAddress to, Uri via) { TChannel innerChannel = this.innerChannelFactory.CreateChannel(to, via); if (typeof(TChannel) == typeof(IRequestChannel)) { return (TChannel)(object)new CalculatorServiceRequestChannel(this, (IRequestChannel)innerChannel, traceEnabled, maxBufferSize, messageVersion); } throw new InvalidOperationException(); } #endregion class CalculatorServiceRequestChannel : CalculatorServiceChannelBase<IRequestChannel>, IRequestChannel { #region Private Fields private bool traceEnabled = true; private int maxBufferSize = 2097152; private MessageVersion messageVersion = MessageVersion.Default; #endregion #region Public Constructors public CalculatorServiceRequestChannel(CalculatorServiceChannelFactory<TChannel> factory, IRequestChannel innerChannel, bool traceEnabled, int maxBufferSize, MessageVersionEnum messageVersion) : base(factory, innerChannel) { this.traceEnabled = traceEnabled; this.maxBufferSize = maxBufferSize; switch (messageVersion) { case MessageVersionEnum.Default: this.messageVersion = MessageVersion.Default; break; case MessageVersionEnum.None: this.messageVersion = MessageVersion.None; break; case MessageVersionEnum.Soap11: this.messageVersion = MessageVersion.Soap11; break; case MessageVersionEnum.Soap11WSAddressing10: this.messageVersion = MessageVersion.Soap11WSAddressing10; break; case MessageVersionEnum.Soap11WSAddressingAugust2004: this.messageVersion = MessageVersion.Soap11WSAddressingAugust2004; break; case MessageVersionEnum.Soap12: this.messageVersion = MessageVersion.Soap12; break; case MessageVersionEnum.Soap12WSAddressing10: this.messageVersion = MessageVersion.Soap12WSAddressing10; break; case MessageVersionEnum.Soap12WSAddressingAugust2004: this.messageVersion = MessageVersion.Soap12WSAddressingAugust2004; break; default: this.messageVersion = MessageVersion.Default; break; } } #endregion #region Public Properties public EndpointAddress RemoteAddress { get { return this.InnerChannel.RemoteAddress; } } public Uri Via { get { return this.InnerChannel.Via; } } #endregion #region Public Methods public IAsyncResult BeginRequest(Message message, AsyncCallback callback, object state) { return this.BeginRequest(message, this.DefaultSendTimeout, callback, state); } public IAsyncResult BeginRequest(Message message, TimeSpan timeout, AsyncCallback callback, object state) { try { Message reply = this.Request(message); return new TypedCompletedAsyncResult<Message>(reply, callback, state); } catch (Exception e) { throw e; } } public Message EndRequest(IAsyncResult result) { TypedCompletedAsyncResult<Message> reply = (TypedCompletedAsyncResult<Message>)result; return reply.Data; } public Message Request(Message message) { return this.Request(message, this.DefaultSendTimeout); } public Message Request(Message message, TimeSpan timeout) { Message reply = null; if (message != null) { Message operationMessage; List<Response> responseList = new List<Response>(); string op = null; StringBuilder statusBuilder = new StringBuilder(); string error = null; double operand1 = 0; double operand2 = 0; bool ok = true; int i = 0; try { Trace.WriteLineIf(traceEnabled, MessageReceived); if (traceEnabled) { string transactionId = "None"; if (Transaction.Current != null && Transaction.Current.TransactionInformation != null) { transactionId = Transaction.Current.TransactionInformation.DistributedIdentifier.ToString(); } Trace.WriteLine(string.Format(TransactionIdFormat, transactionId)); } using (XmlDictionaryReader reader = message.GetReaderAtBodyContents()) { double result; while (reader.Read() && ok) { if (reader.NodeType == XmlNodeType.Element && reader.LocalName == "Operator") { error = "None"; op = reader.ReadElementContentAsString(); operand1 = reader.ReadElementContentAsDouble(); operand2 = reader.ReadElementContentAsDouble(); reply = null; i++; try { switch (op) { case "+": operationMessage = CreateOperationMessage("Add",
operand1, operand2, message); reply = this.InnerChannel.Request(operationMessage); break; case "-": operationMessage = CreateOperationMessage("Subtract",
operand1, operand2, message); reply = this.InnerChannel.Request(operationMessage); break; case "*": operationMessage = CreateOperationMessage("Multiply",
operand1, operand2, message); reply = this.InnerChannel.Request(operationMessage); break; case "/": operationMessage = CreateOperationMessage("Divide",
operand1, operand2, message); reply = this.InnerChannel.Request(operationMessage); break; default: error = string.Format(OperationUnknownErrorMessageFormat, op); SetStatus(statusBuilder,
string.Format(OperationFailedErrorMessageFormat, i, error)); ok = false; break; } } catch (Exception innerException) { error = innerException.Message; if (statusBuilder.Length > 0) { statusBuilder.Append(" - "); } ok = false; } if (reply != null && !reply.IsFault) { result = GetValue(reply); Trace.WriteLineIf(traceEnabled, string.Format(OperationFormat,
operand1, op, operand2, result)); } else { if (reply != null && reply.IsFault) { MessageFault fault = MessageFault.CreateFault(reply, maxBufferSize); error = fault.Reason.ToString(); } Trace.WriteLineIf(traceEnabled, string.Format(OperationFailed, operand1,
op, operand2, error)); SetStatus(statusBuilder, string.Format(OperationFailedErrorMessageFormat, i, error)); result = 0; } responseList.Add(new Response(error, result)); } } } string status = statusBuilder.ToString(); if (string.IsNullOrEmpty(status)) { status = "Ok"; } return CreateResponseMessage(status, responseList, null); } catch (Exception ex) { Trace.WriteLineIf(traceEnabled, ex.Message); if (statusBuilder.Length > 0) { statusBuilder.Append(" - "); } statusBuilder.Append(ex.Message); return null; } finally { Trace.WriteLineIf(traceEnabled, MessageSuccessfullyProcessed); } } else { Trace.WriteLineIf(traceEnabled, String.Format("Message action {0}", message.Headers.Action)); reply = this.InnerChannel.Request(message); Trace.WriteLineIf(traceEnabled, String.Format("Reply is: {0}", reply)); return reply; } } #endregion #region Private Methods private void SetStatus(StringBuilder statusBuilder, string message) { if (statusBuilder.Length > 0) { statusBuilder.Append(" - "); } statusBuilder.Append(message); } private Message CreateOperationMessage(string op, double operand1, double operand2, Message message) { string action = string.Format("http://Microsoft.ServiceModel.Samples/ICalculator/{0}", op); Message operationMessage = Message.CreateMessage(messageVersion, action, new OperationBodyWriter(op, operand1, operand2)); if (message != null) { if (message.Headers != null && message.Headers.Count > 0) { operationMessage.Headers.Clear(); operationMessage.Headers.CopyHeadersFrom(message); operationMessage.Headers.Action = action; } if (message.Properties != null && message.Properties.Count > 0) { operationMessage.Properties.CopyProperties(message.Properties); } } if (messageVersion != MessageVersion.Soap11) { operationMessage.Headers.MessageId = new UniqueId(); } return operationMessage; } private Message CreateResponseMessage(string status, List<Response> responseList, Message message) { Message reply = Message.CreateMessage(MessageVersion.Default, "*", new ResponseBodyWriter(status, responseList)); if (message != null) { if (message.Properties != null && message.Properties.Count > 0) { reply.Properties.CopyProperties(message.Properties); } } reply.Headers.MessageId = new UniqueId(); return reply; } private double GetValue(Message message) { XmlDictionaryReader reader = message.GetReaderAtBodyContents(); while (reader.Read()) { if (reader.Name == "AddResult" || reader.Name == "SubtractResult" || reader.Name == "MultiplyResult" || reader.Name == "DivideResult") { return reader.ReadElementContentAsDouble(); } } return 0; } #endregion } class OperationBodyWriter : BodyWriter { #region Private Fields private string op; private double operand1; private double operand2; #endregion #region Public Constructors public OperationBodyWriter(string op, double operand1, double operand2) : base(false) { this.op = op; this.operand1 = operand1; this.operand2 = operand2; } #endregion #region Protected Methods protected override void OnWriteBodyContents(XmlDictionaryWriter writer) { writer.WriteStartElement(op, "http://Microsoft.ServiceModel.Samples"); writer.WriteStartElement("n1"); writer.WriteValue(operand1); writer.WriteEndElement(); writer.WriteStartElement("n2"); writer.WriteValue(operand2); writer.WriteEndElement(); writer.WriteEndElement(); } #endregion } class ResponseBodyWriter : BodyWriter { #region Private Fields private string status; private List<Response> responseList; private Dictionary<int, Response> responseDictionary; #endregion #region Public Constructors public ResponseBodyWriter(string status, List<Response> responseList) : base(false) { this.status = status; this.responseList = responseList; } public ResponseBodyWriter(string status, Dictionary<int, Response> responseDictionary) : base(false) { this.status = status; this.responseDictionary = responseDictionary; } #endregion #region Protected Methods protected override void OnWriteBodyContents(XmlDictionaryWriter writer) { if (responseList != null) { writer.WriteStartElement("CalculatorResponse", CalculatorResponseNamespace); writer.WriteStartElement("Status", CalculatorResponseNamespace); writer.WriteString(status); writer.WriteEndElement(); writer.WriteStartElement("Results", CalculatorResponseNamespace); for (int i = 0; i < responseList.Count; i++) { writer.WriteStartElement("Result", CalculatorResponseNamespace); writer.WriteStartElement("Value", CalculatorResponseNamespace); writer.WriteString(responseList[i].Value.ToString()); writer.WriteEndElement(); writer.WriteStartElement("Error", CalculatorResponseNamespace); writer.WriteString(responseList[i].Error); writer.WriteEndElement(); writer.WriteEndElement(); } writer.WriteEndElement(); writer.WriteEndElement(); } if (responseDictionary != null) { writer.WriteStartElement("CalculatorResponse", CalculatorResponseNamespace); writer.WriteStartElement("Status", CalculatorResponseNamespace); writer.WriteString(status); writer.WriteEndElement(); writer.WriteStartElement("Results", CalculatorResponseNamespace); for (int i = 0; i < responseDictionary.Count; i++) { writer.WriteStartElement("Result", CalculatorResponseNamespace); writer.WriteStartElement("Value", CalculatorResponseNamespace); writer.WriteString(responseList[i].Value.ToString()); writer.WriteEndElement(); writer.WriteStartElement("Error", CalculatorResponseNamespace); writer.WriteString(responseDictionary[i].Error); writer.WriteEndElement(); writer.WriteEndElement(); } writer.WriteEndElement(); writer.WriteEndElement(); } } #endregion } public class Response { #region Private Fields private string error; private double value; #endregion #region Public Constructors public Response() { this.error = default(string); this.value = default(double); } public Response(string error, double value) { this.error = error; this.value = value; } #endregion #region Public Properties public string Error { get { return this.error; } set { this.error = value; } } public double Value { get { return this.value; } set { this.value = value; } } #endregion } public class Operation { #region Private Fields private string op; private double operand1; private double operand2; #endregion #region Public Constructors public Operation() { this.op = default(string); this.operand1 = 0; this.operand1 = 0; } public Operation(string op, double operand1, double operand2) { this.op = op; this.operand1 = operand1; this.operand2 = operand2; } #endregion #region Public Properties public string Operator { get { return this.op; } set { this.op = value; } } public double Operand1 { get { return this.operand1; } set { this.operand1 = value; } } public double Operand2 { get { return this.operand2; } set { this.operand2 = value; } } #endregion } } }

 

To register the WCFExtensionLibrary assembly containing my custom channel and use this latter within my BizTalk application I have to perform 3 operations:

  • Install the assemblies implementing the WCF extensibility points in the global assembly cache (GAC).
  • Modify the machine.config file on the BizTalk Server computer.
  • Configure the WCF-Custom Send Port by using the BizTalk Server Administration console.

Installing the WCFExtensionLibrary in the GAC

This step is pretty straightforward and can be accomplished using the gacutil tool. You can automatically install your assembly to the GAC whenever you build the class library by including the execution of the gacutil tool in the post-build event command-line of the project, as shown in following picture:

An easy and handy way to verify that the assembly has been successfully installed in the GAC is to use the following command:

gacutil /lr Microsoft.BizTalk.CAT.Samples.WCFExtensionLibrary

How to configure the machine.config file for a WCF binding element extension

To accomplish this task you can proceed as follow:

  • Use a text editor as the Notepad to open the machine.config that be found in the following folder:
    – %windir%\Microsoft.NET\Framework\v2.0.50727\CONFIG on a 32 bit BizTalk Server node.
    – %windir%\Microsoft.NET\Framework64\v2.0.50727\CONFIG on a 32 bit BizTalk Server node.
  • Add the following element containing the Fully-Qualified Name (FQDN) of the CalculatorServiceBindingExtensionElement class to the <configuration><system.serviceModel>\<extensions><bindingElementExtensions> node: 
     
<add name="calculatorService" type="Microsoft.BizTalk.CAT.Samples.WCFExtensionLibrary.CalculatorServiceBindingExtensionElement,
Microsoft.BizTalk.CAT.Samples.WCFExtensionLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d7f63d8d08d8f3a2" />


How to configure the WCF-Custom Send Port by using the BizTalk Admin Console

Using the BizTalk Server Administration console, you can proceed as follows:

  • Create a new Two-Way Solicit-Response Static Send Port and select the WCF-Custom Adapter. Then select the PassThruTransmit as Send Pipeline and the PassThruReceive as Receive Pipeline. They do not contain any pipeline component and do not perform any processing of the message. For this reason, they ensure maximum performance in receiving or sending messages.

  • Click the Configure button. Enter the URL of the CalculatorService in the Address (URI) textbox:
  • Select the Binding Tab then choose the customBinding from the Binding Type drop-down list. Then right click the Binding panel on the left hand side and select the Add Extension item from the context menu
  • Select the transactionFlow and then repeat the same step to select the calculatorService binding element extension element.

 

  • Click the transactionFlow element in the Binding panel an set the value of the transactionProtocol property to WSAtomicTransactionOctober2004. If you review the configuration file (web.config) of the CalculatorService you’ll find out that this is the transaction protocol used by the downstream WCF service.
  • Click the calculatorService element in the Binding panel. As you can see, you can set a value for each of the properties exposed by the custom binding element extension element in the same way as you can configure the properties of a pipeline component. This technique is very powerful and handy as it allows to control the runtime behavior of your custom channel defining in a declarative way.

 

 

 

  • Click the httpTransport element in the Binding panel. As you can see, you can access and change the value of all the properties (authenticationScheme, transferMode,  maxReceivedMessageSize, etc.) exposed by binding element extension element and thus control the runtime behavior of the transport channel.

  • Select the Message Tab and perform the following steps:
    – Select Path in the Inbound BizTalk message body section and enter the following expression in the Body path expression:

    /*[local-name()=’Fault’]/*[local-name()=’Detail’]/* | /*[(local-name()=’AddResponse’)] | /*[(local-name()=’CalculatorResponse’)]

    The body path expression is evaluated against the immediate child element of the SOAP Body element of an incoming message. This property is valid only for solicit-response ports. This pseudo-XPath expression (its syntax is indeed a subset of the XPath syntax) defines the xml element that will be extracted and returned by the Adapter. I used the Union (|) operator to select the CalculatorResponse element in case of success, while in case of error I specified to extract the Detail element from the SOAP Fault returned by the CalculatorService.

    – Select Xml from the Node encoding drop-down list.
    – Select the Propagate fault message checkbox to route a fault message to a subscribing application (such as another receive port or orchestration schedule).
    – Select Use Transaction and set the Isolation Level to Read Committed. In this way you instruct the WCF-Custom Adapter to initiate a distributed transaction before passing the message to the channel stack. Note: the Isolation Level selected on the Send Port must match the Isolation Level used by the downstream WCF service, otherwise the call will immediately fail.

Running the Sample

For brevity, I’ll assume you have already deployed and configured all the remaining artifacts that compose the solution (e.g. the WCF Receive Location, the CalculatorService, etc.). To test application you can proceed as follows:

  • Open SQL Server Management Studio and run the following command in a Query Window to clean up the Log table in the LogDB. 
     
USE LogDb
GO
SET NOCOUNT ON
GO
DELETE Log
GO

 

  • Open the DebugView to capture the trace generated by the CalculatorServiceRequestChannel and  CalculatorService.
  • Open the Calculator Client application, choose MessagingOnly from the Method drop-down list, press the Create button to create a new request document and then the Submit button to send the message to WCF-Custom or WCF-BasicHttp Receive Location exposed by BizTalk Server.
  • The CalculatorServiceRequestChannel and  CalculatorService produce the following output captured by DebugView:
  • As you can see above, the TransactionId traced by the CalculatorServiceRequestChannel is the same traced by the CalculatorService during each call. To confirm that all the service calls participated to the same distributed transaction, I ran the following TSQL script within the Query Window: 
     
USE LogDb
GO
SET NOCOUNT ON
GO
SELECT * FROM Log ORDER BY DateCreated DESC 
GO

 

  • The picture below reports the data written by the individual calls. Note that the TransactionId is the same in all rows and matches the one traced down by the DebugView.

 

 

 

 

To verify that all service calls execute in the context of the same transaction, you can run negative test. For example,  if you try to submit a message that contains an invalid Operation as 72 / 0, this latter will cause the transaction to abort and as an effect nothing will be traced on the Log table.

Conclusions

Customizing the runtime behavior of WCF Adapters using WCF extensibility points (service behaviors, endpoint behaviors, custom channels, etc.) is a powerful technique as explained in my previous posts. Here you can download the PerformanceOptimizationGuide solution which contains other use cases in addition to the WCF custom channel project described in this post.  Feel free to download and customize the code and please let me know your feedbacks. 🙂