Freigeben über


Transaction Flow in WCF

It should be no surprise to anyone that I am totally infatuated with WCF. I am. Totally. It should also be a very little surprise that I am constantly amazed at the people that thought out this wonderful framework. I am. Totally.

This time the amazement is about transactions and how they can flow from the client to the server. I think this really a major step in the direction of having clients that can just do the right thing: It is always nice to know that your server behaves well, but if you serve all the right data etc. to the client and the client is faulty, you have gained little. Now, with WS-AT (WS-Atomic Transactions, which this is the specification that WCF adheres to) the correctness has just been taken a step out further: The client may in the end decide to abort (or, rather vote to abort) an otherwise healthy distributed transaction that involved the server.

Up until the time when a cowoker asked me if I knew anything about WS-AT, I had only briefly come across the concept in my investigations into WCF. The question prompted me to sit down and figure out how this works. I wrote a very small and primitive prototype that was just the archetype of transactional examples: The ATM. In digging into this, I learned to appreciate more than just WCF: The System.Transactions namespace will be your good friend. The entire notion of the "ambient transaction" is pretty cool I think (so far, I like that you don't explicitly have to do stuff like in the old COM+ days).

So, here are the highlights of my implementation:

   [ServiceContract(SessionMode = SessionMode.Required)]

   public interface ITransactedServer

   {

      [OperationContract]

      [TransactionFlow(TransactionFlowOption.Mandatory)]

      void Credit(int accountNo, double amount);

      [OperationContract]

      [TransactionFlow(TransactionFlowOption.Mandatory)]

      void Debit(int accountNo, double amount, bool shouldAbort);

      [OperationContract]

      [TransactionFlow(TransactionFlowOption.Allowed)]

      double GetBalance(int accountNo);

   }

I am guessing that the only new part here is the TransactionFlow attribute and the TransactionFlowOption enumeration. What is actually being declared is that the methods Credit and Debit both require a transaction to function. If there is no transaction available (i.e. ambient), a new one will be created automatically. GetBalance, on the other hand, does not require a transaction, but will allow a transaction to be ambient (it won't complain if there is one, and it won't complain if there isn't one - eager to please, I guess).

The next part is to set up a binding that will allow transactions to flow. While I am generally not a big fan of configuration files for other purposes than development work, the easiest way to set this up is via a configuration snippet like this:

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

   <system.serviceModel>

      <services>

         ...

      </services>

      <bindings>

         <netTcpBinding>

            <binding name="TransactionFlowBinding" transactionFlow="true">

               <reliableSession enabled="true" ordered="true" />

            </binding>

         </netTcpBinding>

      </bindings>

   </system.serviceModel>

</configuration>

The actual server code implementation is described below. I have omitted a few details for brevity - the class uses an array of doubles to store the account balances. Pretty simplistic, but it works for this demonstration purpose. Also, the shouldAbort parameter to the Debit method was force a negative vote in the voting process. I used this to demonstrate that a roll-back can be initiated both on the server and on the client. One of the sweet things here is that both the Credit and Debit method enlists in the ambient transaction by getting a reference to Transaction.Current. Other than that, the code should be selfexplanatory.

[ServiceBehavior(TransactionAutoCompleteOnSessionClose = false, ReleaseServiceInstanceOnTransactionComplete = false)]

public class TransactedServer : ITransactedServer

{

#region ITranactedServer Members

[OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]

public void Credit(int accountNo, double amount)

{

Transaction currentTx = Transaction.Current;

Console.WriteLine("Credit is in tx {0}", currentTx.TransactionInformation.DistributedIdentifier.ToString());

// Because of the TransactionAutoComplete = true above, this method

// will vote favorably in the outcome of the transaction if no

// unhandled exception occurs

if (0 > amount)

{

// This will force a roll-back since there

// is an unhandled exception

throw new ArgumentException("amount will have to be positive");

}

balances[accountNo] += amount;

}

[OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)]

public void Debit(int accountNo, double amount, bool shouldAbort)

{

Transaction currentTx = Transaction.Current;

Console.WriteLine("Debit is in tx {0}", currentTx.TransactionInformation.DistributedIdentifier.ToString());

// If the amount to be debited will put the balance negative,

// roll back the transaction

if (amount > balances[accountNo])

{

return;

}

if (0 > amount)

{

return;

}

balances[accountNo] -= amount;

if(false == shouldAbort)

OperationContext.Current.SetTransactionComplete();

}

[OperationBehavior(TransactionScopeRequired = false)]

public double GetBalance(int accountNo)

{

return this.balances[accountNo];

}

#endregion

}

The client is also very simple. This is also very sweet. With very little work, you have a transactional client! The client uses a configuration file also. The important part of this is the binding configuration that allows transactions to flow. Note the false parameter to the Debit call. If you change that, the transaction that you started should abort leaving the original values in the balances.

class Program

{

static void Main(string[] args)

{

Console.WriteLine("Press <ENTER> to start the client");

Console.ReadLine();

ChannelFactory<TxServer.ITransactedServer> factory = new ChannelFactory<TxServer.ITransactedServer>("TransactedServerConf");

TxServer.ITransactedServer channel = null;

using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required))

{

try

{

channel = factory.CreateChannel();

try

{

Transaction currentTx = Transaction.Current;

Console.WriteLine("Client is in tx {0}", currentTx.TransactionInformation.DistributedIdentifier.ToString());

channel.Credit(0, 100);

channel.Debit(1, 150, false);

}

catch (Exception)

{

}

finally

{

double firstAccountBalance = channel.GetBalance(0);

double secondAccountBalance = channel.GetBalance(1);

scope.Complete();

}

}

catch (Exception)

{

}

finally

{

((ICommunicationObject)channel).Close();

}

}

Console.WriteLine("Done");

}

}

Try it out and see what you get! It is nice!

Comments

  • Anonymous
    March 28, 2007
    The following question was asked of me regarding this post: "Did you have to enable WS-Atomic Transaction Network Support to make this work?  Did you have to use an SSL cert?" The answer to both questions is "No". I have been getting some questions about using transactions in WCF services and clients, so I am expecting to make a post shortly with some more findings.

  • Anonymous
    January 02, 2008
    I believe you did not need WS-Atomic Transaction or Certs because you used a tcp binding.  It is required with http bindings.

  • Anonymous
    May 19, 2009
    What if we have another operation in client side database and it fails. Will server side operation also fail?

  • Anonymous
    May 20, 2009
    If it is in the same transaction scope, it should, right? Am I missing something?