Before starting, I advise you to read my detailed article about .NET transactions and WCF implementation for more in depth knowledge about how transactions work. However, you can still go through this post directly to get the required knowledge about WCF MSMQ transactions.
When working with MSMQ and WCF, you have two options for bindings: NetMsmqBinding and MsmqIntegrationBinding.
- The NetMsmqBinding is intended to be used when both the client and service are WCF and communicate over the assigned endpoints.
- MsmqIntegrationBinding on the other hand can be used with already-written native MSMQ applications that use MSMQ COM or the System.Messaging APIs.
I will be using the NetMsmqBinding.
Getting to the point…
I am going to show you the code directly explaining the concepts as we go.
The service contract of my service application is defined below:
[OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]
public void GetData(int value)
TransactionScopeRequired indicates that my operation must be enlisted within a transaction and TransactionAutoComplete indicates that the transaction will be committed automatically once the operation finishes.
In the operation body I simply print something to the event viewer and interestingly manually roll back the transaction. Why did I do that? This will become clear later.
The relevant service configuration is where the magic happens. It is shown below:
First things first; I am using the netMsmqBinding and have set the address to a local TRANSACTIONAL private queue with the same name as my service application (check this post on how to set up WCF-MSMQ application on IIS 7). In this example, both the client and the service are hosted on the same machine so only the private queue will be used.
Now on to the binding configuration:
- exactlyOnce: when set to true means that transaction is enabled and the queue must be marked as “Transactional” on creation. This means that each message is guaranteed to be delivered one and only one time and a batch of messages are delivered in order.
- durable: indicates that messages are never lost. Messages are persisted to the disk so if the MSMQ service is restarted messages are preserved.
- receiveRetryCount: this setting indicates how many times the service will try to receive and process the message
- retryCycleDelay: specifies the time between retry counts. For example if receiveRetryCount is set to 2 and retryCycleDelay is set to 20 seconds, then the service will try to receive and process the message total of 2 times within an interval of 20 seconds between each try.
- maxRetryCycles: the number of cycles the retryCycleDelay and receiveRetryCount are run. For example if maxRetryCycles is set to 2, receiveRetryCount is set to 2, and retryCycleDelay is set to 20 seconds then the service will try to receive and process the message 2 times within and interval of 20 seconds and the whole cycle will be repeated 2 times.
- timeToLive: Indicates the maximum amount of time a message can live in the queue waiting to be delivered to the service. This value supersedes all retry logic; if the TTL is less that the total time the cycle is configured to process then the cycle terminates once the TTL is reached . The default value is 1 day.
So what happens if the cycle fails? One of two things depending on the reason of failure:
- If the failure is caused by the fact that the message cannot be delivered to the service, then the message will be placed in a transactional dead-letter queue. Setting deadLetterQueue specifies that the system dead letter queue will be used. A better idea would be to use a custom dead letter queue in order not to mix messages from different services in the same system dead letter queue. This is done by selecting “Custom” value for the deadLetterQueue setting and then specifying the name of this custom queue using setting customDeadLetterQueue.
- If the message is delivered to the service but the transaction fails while processing, then the message will be considered “poisoned” and setting receiveErrorHandling specifies what to do with the message. “Move” means putting the message in a special automatically created queue called “poison”. Other values for the receiveErrorHandling are:
- Fault: sends a fault to the listener that caused the ServiceHost to fault. The message must be removed from the application queue by some external mechanism before the application can continue to process messages from the queue
- Drop: drops the poison message and the message is never delivered to the application
- Reject: MSMQ will send a negative acknowledgement back to the sending queue that the application cannot receive the message. The message is placed in the sending queue dead-letter queue.
So all in all I have configured my service to try to receive and process the message 1 time, wait 20 seconds and try to receive again. This ends the 1 cycle I have configured. If after this the message still cannot be received or processed then I want to either store my message in the system dead letter queue or store it in a poison queue depending on the reason of failure (error in delivery or error in processing – respectively).
In my operation code now you will understand why I did manually roll back my transaction. I am simulating the required conditions to fail message processing and placing the message in the poison queue.
Now create a console client application and add a service reference to the service. Then use the below code to invoke the service:
using (TransactionScope sc = new TransactionScope(TransactionScopeOption.Required))
I am enlisting the client in a transaction and I send the message to the private queue. It is important to note that the transactions enlisted at the client and the service are not related. The client transaction ends once the message is delivered to the queue; then the service transaction is in play trying to deliver the message from the queue to the service.
Run the example and examine the complete process:
First: The client sends a message to the private queue. DTC is used internally to accomplish this. You can see the stats of the DTC by examining the Local DTC Transaction Statistics from the Component Services on Windows 7. See below:
The client transaction ends here. If the transaction fails sending the message to the queue, the client will be notified and the transaction will roll back. This will be most helpful when the client is sending a group of messages to the queue and it’s important for all the messages to be either delivered as a batch or none is delivered.
What happens for messages placed in the dead-letter queue or the poison queue? Custom application logic must be written to compensate these messages and do “something” about them.
Note that messages would have ended in the Transactional Dead-letter system queue had the message been failed to be delivered as opposed to fail during processing. The system queue is shown below:
What will happen in case of Public/Private queues?
Part 2 of this series will show the structure where the client and service are on separate machines (also MSMQ will be on a Windows Server Cluster…). But in a nutshell, in this case the following will happen:
The client WCF endpoint will be configured to send the message to the destination queue on the service machine (actually in case of MSMQ cluster it will send to a virtual IP, but this is a separate issue to be discussed in part 2), however, since the service is on another machine then the client application will silently deliver the message to an automatically created private outgoing queue. This queue will then try to deliver the message to the real destination queue.
The client will engage in one DTC transaction to deliver the message to the temp outgoing queue. The service will then be engaged in a separate DTC transaction to retrieve the message from the destination queue.
The same retry rules and poison/dead-letter message discussions mentioned above still apply.
But isn’t DTC heavy on Performance? Do I really need it?
Sure! And it depends.
Again if you follow the link (at the start) of my transactions article, you know that DTC implements a two-phase commit protocol and that definitely has a performance hit. However, whether you can live with or without transactions is governed by the scenario.
Let’s start with the obvious: in the scenario I presented above DTC is not required! The client needed the transactions “only” to commit sending messages to the queue. The service also needed the transaction “only” to commit receiving messages from the queue. So why did the DTC kick in? Simply because – as of this writing – it seems that the netMsmqBinding is configured to use DTC and there seems to be no way of changing that. But I do need transactions you might say, only not DTC; so what is the alternative? The answer would be MSMQ Internal Transactions. This is a light weight transaction implemented internally for MSMQ that does not need DTC. However, this seems to be not supported when using WCF netMsmqbinding; the only way you can use it is to use the System.Messaging APIs to send and receive from MSMQ.
To use the Internal Transaction you would use classes MessageQueue and MessageQueueTransaction from the System.Messaging API to send and receive messages within a transaction.
So why would I bother going for DTC (and netMsmqBinding) when I can go with internal transactions? Well because netMsmqBinding rocks…just kidding! Consider this scenario: when you receive the message at the service you want to use it to perform an update on a SQL server DB. If the update fails however, you want to place the message back in the queue so that you can fix the problem and retry again. Meaning that you want to enlist the receiving of the message and the DB update in a single atomic transaction: either all succeeds or the transaction rolls back and the message is back safe in the queue. In this case DTC is a must because the transaction is spanning two resource managers (again for more details see link at the start).
Note: I could not find any official say from MS that the netMsmqBinding does not support MSMQ internal transactions. I rather tested it myself and it did work.