In this post I’ll try to talk a little bit about how you can get msmq work and sql 2005 work to go together in a transactional context. I think it’s fair to say none of the information is new, but I did a bit of digging around the net to find it, so maybe having it all in one place would be of help. Any thoughts or suggestions are more than welcome and I’d be happy to update the post for posterity J So…
…what do you do if you need to:
a) Receive a message from a MessageQueue
b) Based on that message, save something to a Sql table
c) Send a message to (another) MessageQueue
d) Have all of the above work or fail together (like in…uhm…a transaction ;))
As you might have guessed by now, you need to somehow “wrap” all of the above operations in a transaction, or, let’s say, in the same transactional context. In .net 2.0+, this is deceptively simple:
using (TransactionScope transaction = new TransactionScope())
{
// do some work
transaction.Complete();
}
So all you have to do is make sure that all of the operations you need to do (like a,b and c above) are inside the TransactionScope and the work will be transactional.
Now some things to be aware of:
1. The above will work with MSMQ out of the box, but only with some ado.net database providers, namely those whose implementation looks to see if there is an ambient transaction and enlist in it. The Sql and Oracle providers, at least, support this, so they will work ok using this approach.
2. You need to have the DTC (Distributed Transaction Coordinator) enabled and configured correctly. I won’t go into details about how to do this, as there is LOTS of information about it on the net. You need it because you have two different transaction types (one for MSMQ and one for the database), whose managers need to work together in order to achieve the “transactional context” goal. They do this by enlisting the help of a “higher power”, namely the DTS.
3. For your message queues to support transactions, they need to be marked as “Transactional” at creation time. Be careful about this one: I found no obvious way (from the MMC console) to change this after the queue was created AND the behavior of the queue I observed was silent (for example, when sending a message to a transactional queue without using a transaction, the message is not sent, but you don’t get an exception either, so it fails silently).
4. There’s a class called MessageQueueTransaction. The usage pattern of this class is similar to an Ado transaction, having Begin() and Commit() methods and also requiring that when you do MessageQueue operations, you use overloaded methods which take the transaction as a parameter, like MessageQueue.Send(Message msg, MessageQueueTransaction transaction). BUT, when using a transactional MessageQueue within a TransactionScope, you don’t need this class. What you do is create a MessageQueue normally (like in a transaction-less context) and use it. The only thing you need to do is tell the queue to use transactions. You do this using overloads of Send() and Receive(), like MessageQueu.Send(Message msg, MessageQueueTransactionType transactionType):
using (TransactionScope transaction = new TransactionScope())
{
Message inputMsg = inputQueue.Receive(MessageQueueTransactionType.Automatic);
// do some work
transaction.Complete();
}
5. Beware of long-running transactions. For example, if your use case is such that you need to receive a message from a queue first, then write something to the DB, you might be tempted to do the following:
using (TransactionScope transaction = new TransactionScope())
{
using (MessageQueue someQueue = new MessageQueue("<queue connection>"))
{
Message msg = someQueue.Receive();
// do something else
}
transaction.Complete();
}
This is not ok! It’s wrong because the Receive() method is synchronous and if there’s nothing in the message queue, it will block until there is. This means that your transaction will remain open for that amount of time and will probably time-out, resulting in a MessageQueueException with a message along the lines of “The transaction operations sequence is invalid”.
The correct way to do this is using the BeginPeek() (not BeginReceive(), since this is not designed to work for transactional queues) method, subscribe to the PeekCompleted event of the queue and, in the event handler, use a transaction to actually Receive() any messages from the queue, like so:
inputQueue = new MessageQueue(("<queue connection>");
inputQueue.PeekCompleted += inputQueue_PeekCompleted;
inputQueue.BeginPeek();
private void inputQueue_PeekCompleted(object sender, PeekCompletedEventArgs e)
{
using (TransactionScope transaction = new TransactionScope())
{
Message inputMsg = inputQueue.Receive(MessageQueueTransactionType.Automatic);
// do some work
transaction.Complete();
}
inputQueue.BeginPeek();
}
6. If you only need to do transactional work related to message queues, without any other resources that need to be in the transactional context (like databases), don’t use the TransactionScope, but rather the MessageQueueTransaction class I mentioned earlier. Using this class to control transactions related only to message queues comes with a lesser performance penalty than using a full distributed transaction controlled by DTC.
Links and resources:
Using the Microsoft MessageQueue
MSMQ Transactions on CodeProject
MSDN Help on BeginReceive()
Nine Tips to Enterprise –proof MSMQ