Some time ago, I was asked by a customer whether or not BizTalk WCF Adapters support the Duplex Message Exchange Pattern. The latter is characterized by the ability of both the service and the client to send messages to each other independently either using one-way or request/reply messaging. This form of bi-directional communication is useful for services that must communicate directly to the client or for providing an asynchronous experience to either side of a message exchange, including event-like behavior. The BizTalk WCF Adapters support this message exchange pattern, but unfortunately this feature is undocumented. Therefore, I decided to create a full demo where a client .NET application exchange messages with an Orchestration via a two-way Request-Response WCF Receive Location that supports callbacks and duplex communication. Subsequently, I decided to extend this demo and introduce a WCF Workflow Service between the WinForm client application and the underlying Orchestration to implement a more complex scenario The final objective was to investigate how exploiting the Correlation mechanisms provided by WF 4.0 to get a workflow running within IIS/AppFabric to exchange messages with a downstream orchestration in an asynchronous mode.
This article will be composed of 3 parts:
The Duplex Message Exchange Pattern is fully supported by WCF and is extensively documented on MSDN. As I already mentioned in the introduction, the Duplex pattern allows two applications to act both as service endpoints and send messages to each other independently in an asynchronous way. There are many reasons for using the Duplex Message Exchange Pattern when communicating with a BizTalk solution:
In general, if you have a choice between a synchronous and asynchronous call, you should choose the latter approach. In fact, a synchronous call typically blocks the client thread till the operation completes, whereas an asynchronous call is non-blocking and only initiates the operation. This way the consumer application can continue its execution without waiting for the call to complete and get notified by the service when the result of its request is ready.
For more information on how implementing the Duplex Message Exchange Pattern with WCF, you can read the following articles:
The following picture represents the architecture of the first use case. The idea behind the application is quite straightforward: a Windows Forms application submits a question to an orchestration called SyncMagic8Ball via a WCF-NetTcp Receive Location. The orchestration is a BizTalk version of the notorious Magic 8 Ball toy and it randomly returns one of 20 standardized answers.
In order to establish an asynchronous communication, the client application and service, in our case BizTalk Server, have to meet the following conditions:
To satisfy the second and third conditions, I have created 3 different Two-Way Request-Response Receive Locations with the following characteristics:
The following picture shows the 3 Receive Locations inside the BizTalk Administration Console:
Bindings have different characteristics in terms of response time and throughput, so the general advice to increase performance is using the NetTcpBinding and NetNamedPipeBinding whenever possible.
The request messages sent by the Windows Forms Client Application have the following format:
Request Message
<Request xmlns="http://microsoft.appfabric.cat/10/samples/duplexmep"> <Id>9aff8596-ec87-494d-801e-30f286d449a4</Id> <Question>Will the world end in 2012?</Question> <Delay>0</Delay> </Request>
The corresponding response messages returned by the SyncMagic8Ball orchestration have the following format:
Response Message
<Response xmlns="http://microsoft.appfabric.cat/10/samples/duplexmep"> <Id>9aff8596-ec87-494d-801e-30f286d449a4</Id> <Answer>Most likely</Answer> </Response>
Both message types are defined by an XML Schema contained in the Schemas project that you can find in the companion code for this article.
The following picture shows the structure of the SyncMagic8Ball orchestration.
As you can easily notice, the orchestration uses the Two-Way Request-Response Logical Port to receive the inbound request message and to return the corresponding response message. The Trace Request Expression Shape contains the following code to extract the information from the request message. The namespace of the LogHelper and XPathHelper static classes have been eliminated for ease of reading.
LogHelper.WriteLine(System.String.Format("[SyncMagic8Ball] Transport: {0}", RequestMessage(BTS.InboundTransportType))); id = XPathHelper.GetValue(RequestMessage, 0, "Id Element XPath Expression"); if (!System.Int32.TryParse(XPathHelper.GetValue(RequestMessage, 0, "Delay Element XPath Expression"), out delayInSeconds)) { delayInSeconds = 0; } LogHelper.WriteLine(System.String.Format("[SyncMagic8Ball] Id: {0}", id)); LogHelper.WriteLine(System.String.Format("[SyncMagic8Ball] Question: {0}", XPathHelper.GetValue(RequestMessage, 0, "Question Element XPath Expression"))); LogHelper.WriteLine(System.String.Format("[SyncMagic8Ball] Delay: {0}", delayInSeconds));
You can use DebugView, as shown in the picture below, to monitor the trace produced by the orchestration and helper components.
Note My LogHelper class traces messages to the standard output using the capability supplied by the Trace class. This component is primarily intended to be used for debugging a BizTalk application in a test environment, rather than to be used in a production environment. If you are looking for a tracing framework which combines the high performance and flexibility provided by the Event Tracing for Windows (ETW) infrastructure, you can read the following whitepaper by Valery Mizonov:
The value of the Delay Shape is defined as follows:
new System.TimeSpan(0, 0, delayInSeconds);
Therefore, the orchestration waits for the time interval in seconds specified in the request message before returning the response message to the caller.
Note To extract the value of individual fields from the inbound document, I could have used Distinguished Fields defined on the XML Schema for the request message. These are context properties whose name is defined by the XPath Expression used to retrieve data from an input XML document. If the XML Schema of the document is quite complex, the XPath Expression in question can be very long and therefore the corresponding Distinguished Field can occupy a significant amount of space in the message context. Therefore, I recommend to implement the following best practices in any BizTalk application:
When the document in question is extremely small, you can also load the message into an XmlDocument object and use the SelectSingleNode method to extract the information you need using an XPath Expression. However, the general advice is to minimize the usage of XmlDocument variables in orchestrations and in .NET code. Loading a message into an XmlDocument variable has significant overhead, especially for large messages. This overhead is in terms of memory usage and system resources required to build the in-memory structures. The use of an XmlDocument instance forces the message content to be entirely loaded into memory in order to build the object graph for the DOM. The total amount of memory used by an instance of this class can be around 10 times the actual message size. For more information and evidence, see the following articles I wrote on this topic:
The request message in my demo is relatively small; nevertheless, I decided to use an helper class that adopts a streaming approach to extract data from the XLANGMessage passed in as argument:
public class XPathHelper { #region Private Constants private const string MessageCannotBeNull = "[XPathReader] The message cannot be null."; #endregion #region Public Static Methods public static string GetValue(XLANGMessage message, int partIndex, string xpath) { try { if (message == null) { throw new ApplicationException(MessageCannotBeNull); } using (Stream stream = message[partIndex].RetrieveAs(typeof(Stream)) as Stream) { XmlTextReader xmlTextReader = new XmlTextReader(stream); XPathCollection xPathCollection = new XPathCollection(); XPathReader xPathReader = new XPathReader(xmlTextReader, xPathCollection); xPathCollection.Add(xpath); while (xPathReader.Read()) { if (xPathReader.HasAttributes) { for (int i = 0; i < xPathReader.AttributeCount; i++) { xPathReader.MoveToAttribute(i); if (xPathReader.Match(xPathCollection[0])) { return xPathReader.GetAttribute(i); } } } if (xPathReader.Match(xPathCollection[0])) { return xPathReader.ReadString(); } } } } finally { message.Dispose(); } return string.Empty; } #endregion }
The Set Response Message Assignment shape invokes the SetResponse static method exposed by the ResponseHelper class to generate the response message:
ResponseHelper Class
public class ResponseHelper { #region Private Constants private const string MessageFormat = "[ResponseManager] {0}"; private const string ResponseNamespace = "http://microsoft.appfabric.cat/10/samples/duplexmep"; private const string Response = "Response"; private const string Id = "Id"; private const string Answer = "Answer"; #endregion #region Static Constructor private static string[] answers = null; #endregion #region Static Constructor static ResponseHelper() { answers = new string[]{"As I see it, yes", "It is certain", "It is decidedly so", "Most likely", "Outlook good", "Signs point to yes", "Without a doubt", "Yes", "Yes – definitely", "You may rely on it", "Reply hazy, try again", "Ask again later", "Better not tell you now", "Cannot predict now", "Concentrate and ask again", "Don't count on it", "My reply is no", "My sources say no", "Outlook not so good", "Very doubtful"}; } #endregion #region Public Static Methods public static void SetResponse(XLANGMessage message, string id) { try { VirtualStream stream = new VirtualStream(); using (XmlWriter writer = XmlWriter.Create(stream)) { writer.WriteStartDocument(); writer.WriteStartElement(Response, ResponseNamespace); writer.WriteStartElement(Id, ResponseNamespace); writer.WriteString(id); writer.WriteEndElement(); writer.WriteStartElement(Answer, ResponseNamespace); Random random = new Random(unchecked((int)DateTime.Now.Ticks)); writer.WriteString(answers[random.Next(0, 20)]); writer.WriteEndElement(); writer.WriteEndElement(); } stream.Seek(0, SeekOrigin.Begin); message[0].LoadFrom(stream); } catch (Exception ex) { LogHelper.WriteLine(ex.Message); } finally { message.Dispose(); } } #endregion }
As you can see, the ResponseHelper class uses a VirtualStream object an XmlWriter class instance to initialize the content of the response message. Once again, the response message in this case is so small that I could have used an XmlDocument object to load the content of the response message. However, my intention was to show you how to take advantage of the streaming approach to read and write the content of an XLANGMessage object within an orchestration.
Let’s take a look at the code used by the Windows Forms Client Application to send a request message and receive the corresponding response using a Callback contract.
I started defining the Data and Message Contracts for the request and response messages. These 2 types of contracts have different roles in WCF:
For more information on Data and Message Contracts, you can read the following articles:
In my solution, I created 2 separate projects called DataContracts and MessageContracts respectively. For your convenience, I included below the code of the 4 classes used to define the Data and Message Contract of the request and response messages.
BizTalkRequest Class
[DataContract(Name="Request", Namespace="http://microsoft.appfabric.cat/10/samples/duplexmep")] public partial class BizTalkRequest : IExtensibleDataObject { #region Private Fields private ExtensionDataObject extensionData; private string id; private string question; private int delay; #endregion #region Public Constructors public BizTalkRequest() { this.id = Guid.NewGuid().ToString(); this.question = null; this.delay = 0; } public BizTalkRequest(string question, int delay) { this.id = Guid.NewGuid().ToString(); this.question = question; this.delay = delay; } #endregion #region Public Properties public ExtensionDataObject ExtensionData { get { return this.extensionData; } set { this.extensionData = value; } } [DataMemberAttribute(IsRequired = true, Order = 1)] public string Id { get { return this.id; } set { this.id = value; } } [DataMemberAttribute(IsRequired = true, Order = 2)] public string Question { get { return this.question; } set { this.question = value; } } [DataMemberAttribute(IsRequired = true, Order = 3)] public int Delay { get { return this.delay; } set { this.delay = value; } } #endregion }
BizTalkResponse Class
[DataContract(Name = "Response", Namespace = "http://microsoft.appfabric.cat/10/samples/duplexmep")] public partial class BizTalkResponse : IExtensibleDataObject { #region Private Fields private ExtensionDataObject extensionData; private string id; private string answer; #endregion #region Public Properties public ExtensionDataObject ExtensionData { get { return this.extensionData; } set { this.extensionData = value; } } [DataMemberAttribute(IsRequired = true, Order = 1)] public string Id { get { return this.id; } set { this.id = value; } } [DataMemberAttribute(IsRequired = true, Order = 2)] public string Answer { get { return this.answer; } set { this.answer = value; } } #endregion }
BizTalkRequestMessage Class
[MessageContract(IsWrapped=false)] public class BizTalkRequestMessage { #region Private Fields private BizTalkRequest request; #endregion #region Public Constructors public BizTalkRequestMessage() { this.request = null; } public BizTalkRequestMessage(BizTalkRequest request) { this.request = request; } #endregion #region Public Properties [MessageBodyMember(Namespace = "http://microsoft.appfabric.cat/10/samples/duplexmep")] public BizTalkRequest Request { get { return this.request; } set { this.request = value; } } #endregion }
BizTalkResponseMessage Class
[MessageContract(IsWrapped = false)] public class BizTalkResponseMessage { #region Private Fields private BizTalkResponse response; #endregion #region Public Constructors public BizTalkResponseMessage() { this.response = null; } public BizTalkResponseMessage(BizTalkResponse response) { this.response = response; } #endregion #region Public Properties [MessageBodyMember(Namespace = "http://microsoft.appfabric.cat/10/samples/duplexmep")] public BizTalkResponse Response { get { return this.response; } set { this.response = value; } } #endregion }
Indeed, I could have used just Data Contracts to model messages as Message Contracts add a degree of complexity. So why I decided to use Message Contracts? Well, the reason is quite straightforward. By assigning false to the IsWrapped property exposed by the MessageContractAttribute, you specify that the message body won’t be contained in a wrapper element. Typically, the wrapper element of a request message is the name of the operation invoked and it’s defined in the WSDL. Setting the value of the IsWrapped property to false, you can simply select the Body option in both the Inbound BizTalk message body and Outbound WCF message body sections on the Messages tab when configuring a WCF Receive Location. On the contrary, you should define a Path in the Inbound BizTalk message body section to extract the payload from the inbound message, and specify a template in the Outbound WCF message body section to include the outgoing response message within a wrapper element.
The next step was to define the Service Contracts used by the client application to exchange messages with BizTalk Server. To this purpose, I created a new project in my solution called ServiceContracts, and then I started off by defining the Service Contract Interface that models the server side of the duplex contract. I declared the signature of a One-Way method called AskQuestion. I specified a parameter of type BizTalkRequestMessage (see the Data and Message Contracts section) and void as return type. Then I decorated the method with the XmlSerializerFormatAttribute to indicate to the WCF Runtime to use the XmlSerializer instead of the DataContractSerializer. Please note that the WCF Adapters and BizTalk in general use the XmlSerializer, thus I had to explicitly set the correct serializer in the contract definition. Next, I decorated the method with the OperationContractAttribute to indicate that the method is one way and to specify the WS-Addressing Action of the request message. Finally I decorated the interface with the ServiceContractAttribute.
IMagic8BallBizTalk Interface
[ServiceContract(Namespace = http://microsoft.appfabric.cat/10/samples/duplexmep, SessionMode = SessionMode.Required, CallbackContract = typeof(IMagic8BallBizTalkCallback), ConfigurationName = "IMagic8BallBizTalk")] public interface IMagic8BallBizTalk { [XmlSerializerFormat] [OperationContract(Action = "AskQuestion", IsOneWay = true)] void AskQuestion(BizTalkRequestMessage requestMessage); }
Then I defined the the callback interface as follows:
IMagic8BallBizTalkCallback Interface
[ServiceContract(Namespace = http://microsoft.appfabric.cat/10/samples/duplexmep, ConfigurationName = "IMagic8BallBizTalkCallback")] public interface IMagic8BallBizTalkCallback { [XmlSerializerFormat] [OperationContract(Action = "AskQuestionResponse", IsOneWay = true)] void AskQuestionResponse(BizTalkResponseMessage responseMessage); }
Finally, I linked the two interfaces into a Duplex Contract by assigning the type of the callback interface to the CallbackContract property in the ServiceContractAttribute that decorates the IMagic8BallBizTalk interface.
The next step was to implement the IMagic8BallBizTalkCallback callback interface in order to receive response messages from BizTalk Server. To this purpose I created a new class in the Client project called Magic8BallBizTalkCallback that implements the IMagic8BallBizTalkCallback callback interface.
Magic8BallBizTalkCallback Class
[ServiceBehavior] public class Magic8BallWFCallback : IMagic8BallWFCallback { #region Private Constants private const string ResponseFormat = "Response:\n\tId: {0}\n\tAnswer: {1}"; #endregion #region Private Static Fields private static MainForm form = null; #endregion #region Private Static Fields public static MainForm MainForm { get { return form; } set { form = value; } } #endregion #region IMagic8BallCallback Members [OperationBehavior] public void AskQuestionResponse(WFResponseMessage responseMessage) { if (responseMessage != null && responseMessage.Response != null && !string.IsNullOrEmpty(responseMessage.Response.Id) && !string.IsNullOrEmpty(responseMessage.Response.Answer)) { form.WriteToLog(string.Format(CultureInfo.CurrentCulture, ResponseFormat, responseMessage.Response.Id, responseMessage.Response.Answer)); } } #endregion } ... public partial class MainForm : Form { ... public void WriteToLog(string message) { if (InvokeRequired) { Invoke(new Action<string>(InternalWriteToLog), new object[] { message }); } else { InternalWriteToLog(message); } } ... }
In particular, when the WCF Receive Location invokes the AskQuestionResponse method to return a response message, the callback is handled by a ThreadPool thread other than the main thread running the client application. Since the InternalWriteToLog method is used to write the answer on a ListBox control owned by the main thread, the WriteToLog method exposed by the MainForm class uses the InvokeRequired property to check whether it's necessary to use the Invoke method because the caller is on a different thread than the main thread.
The next step was to write the code to invoke one of the 3 WCF Receive Locations exposed by by BizTalk application.
btnAsk_Click Method
private void btnAsk_Click(object sender, EventArgs e) { try { int delay = 0; if (string.IsNullOrEmpty(txtQuestion.Text)) { WriteToLog(QuestionCannotBeNull); txtQuestion.Focus(); return; } if (string.IsNullOrEmpty(txtDelay.Text) || !int.TryParse(txtDelay.Text, out delay)) { WriteToLog(DelayMustBeANumber); txtDelay.Focus(); return; } if (string.IsNullOrEmpty(cboEndpoint.Text)) { WriteToLog(NoEndpointsFound); } DuplexChannelFactory<IMagic8BallBizTalk> channelFactory = null; try { BizTalkRequest request = new BizTalkRequest(txtQuestion.Text, delay); BizTalkRequestMessage requestMessage = new BizTalkRequestMessage(request); WriteToLog(string.Format(CultureInfo.CurrentCulture, RequestFormat, cboEndpoint.Text, request.Id, request.Question)); InstanceContext context = new InstanceContext(new Magic8BallBizTalkCallback(this)); channelFactory = new DuplexChannelFactory<IMagic8BallBizTalk>(context, cboEndpoint.Text); IMagic8BallBizTalk channel = channelFactory.CreateChannel(); channel.AskQuestion(requestMessage); } 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(); } } } catch (Exception ex) { WriteToLog(ex.Message); } }
As you may have noticed, the method btnAsk_Click performs the following actions in order:
The final step was to define the client endpoints in the client configuration file in order to invoke the 3 WCF Receive Locations exposed by the BizTalk application.
Configuration File
<?xml version="1.0"?> <configuration> <system.serviceModel> <bindings> <netNamedPipeBinding> <binding name="netNamedPipeBinding" /> </netNamedPipeBinding> <netTcpBinding> <binding name="netTcpBinding"> <security mode="Transport"> <transport protectionLevel="None" /> </security> </binding> </netTcpBinding> <wsDualHttpBinding> <binding name="wsDualHttpBinding" /> </wsDualHttpBinding> </bindings> <client> <clear /> <endpoint address="net.tcp://localhost:7171/magic8ballbiztalk/sync" binding="netTcpBinding" bindingConfiguration="netTcpBinding" contract="IMagic8BallBizTalk" name="NetTcpEndpointBizTalk" /> <endpoint address="net.pipe://localhost/magic8ballbiztalk/sync" binding="netNamedPipeBinding" bindingConfiguration="netNamedPipeBinding" contract="IMagic8BallBizTalk" name="NetNamedPipeEndpointBizTalk" /> <endpoint address="http://localhost/magic8ballbiztalk/syncmagic8ball.svc" binding="wsDualHttpBinding" bindingConfiguration="wsDualHttpBinding" contract="IMagic8BallBizTalk" name="WsDualHttpEndpointBizTalk" /> </client> </system.serviceModel> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/> </startup> </configuration>
We are now ready to test the solution.
To test the application, you can proceed as follows:
Now, if you press the Ask button multiple times in a row, you can easily notice that the client application is called back by BizTalk in an asynchronous way. Therefore, the client application doesn't need to wait for the response to the previous question before posing a new one.
In order for the demo to work, it’s important that the Delay in seconds that you specify in the client application is less than the ReceiveTimeout configured in the WCF Receive Location. When using a WCF Adapter other than the WCF-Custom and WCF-CustomIsolated, you cannot specify a custom ReceiveTimeout. The following picture shows the Binding tab of a WCF-NetTcp Receive Location.
As a consequence, the default 10 minutes timeout will be used. Therefore, if you need to increase the ReceiveTimeout beyond 10 minutes, you can replace the WCF Adapter in question with the WCF-Custom and WCF-CustomIsolated adapter and select the same binding. This way, you can specify a custom value for the ReceiveTimeout property exposed by the binding, as shown in the following picture:
In this article we have seen how to exchange messages with an orchestration via a two-way WCF Receive Location using the Duplex Message Exchange Pattern. In the next article of the series, we’ll see how to implement an asynchronous communication between our client application and a WCF Workflow Service running within IIS\AppFabric Hosting Services using the Durable Duplex Correlation. In the meantime, here you can download the companion code for this article. As always, you feedbacks are more than welcome!
Acknowledge review and comments from Valery Mizonov and Christian Martinez. Thanks guys!