Click here to Skip to main content
15,867,686 members
Articles / Web Development / IIS
Article

Transactional Web Services with WS-AtomicTransaction

Rate me:
Please Sign up or sign in to vote.
3.91/5 (12 votes)
12 Jun 2008CPOL6 min read 136.5K   491   48   30
Explains a simple way of using WS-AtomicTransaction to create distributed transactions across Web services

Introduction

In enterprise systems, transactionality can be pretty important. A classic example is transferring money between bank accounts. The amount has to be subtracted from one account and then added to another account. If a failure occurs at any point in this process, the whole thing should be rolled back as if it never happened. If the servers performing the work fail at any time during the process, the work should be saved so that when those servers come back up, they can continue or roll back. This is the ACID principle. ACID standing for Atomic, Consistent, Isolated, and Durable. Atomic means that the transaction is one whole unit of work. Consistent means that the results are predictable. Isolated means that this unit of work does not depend on some other work somewhere else. Durable means that if something goes wrong at any point, the transaction can be recovered. Some things just have to be done transactionally.

The .NET 2 Framework introduced the System.Transactions namespace. Before that, transactions in .NET had to be either homogeneous like a database transaction or distributed via System.EnterpriseServices. Enterprise Services is rather bulky and requires a knowledge of how COM+ components are built and installed. But with .NET 2, you could create a distributed transaction simply by doing this:

C#
using (TransactionScope scope = new TransactionScope()) {
  // Execute against database 1
  // Execute against database 2
  scope.Complete();
}

But what if you want to distribute a transaction across a Web service? There was a way, but it was tricky. You had to go back to System.EnterpriseServices and COM+ to do it. COM+ has a feature called the Transaction Internet Protocol which can be used to distribute transactions across systems. Check out[^] my previous article on TIP if you're curious.

After writing my article on TIP, I was contacted by Jack Loomis, a program manager at Microsoft, regarding the new support for WS-AtomicTransaction in Indigo (WCF). He wanted me to rewrite my article using WS-AT. This new offering in WCF was much easier than TIP even back then in the beta stages. Now, it's much easier to use. I hope to show you in this article how simple it is to create a transaction on a client, call a Web service, and have that Web service participate in the transaction started on the client side.

Create a Database

The first step is to create a database to test with. Most of us should have SQL Server or SQL Server Express available. I believe Express will support distributed transactions but some of the lighter flavors of SQL Server will not. You could also certainly do this with DB2, Oracle, MySql, etc. The code included with this article assumes that you already have a database and login for it. There is a simple SQL script to create a table called MyCategory. The MyCategory table has three columns: CategoryId (an identity column), CategoryName, and Description. This table is just an example and there is nothing special about it or the script.

Service Contract

The next step is to sketch out what the service contract will look like for our WCF service. The source code included has an assembly called Common that has the service contract interface in it. This interface is shared between client and server just to reduce the amount of generated code and also keep my proxy class simple. You certainly don't have to do it this way. You could create a Web reference on the client side instead.

The service contract will have a method to create a category, delete a category, and get a list of all the categories.

C#
using System.Collections.Generic;
using System.ServiceModel;

namespace Common
{
    [ServiceContract]
    public interface ITransactionalWebService
    {
        [OperationContract]
        [TransactionFlow(TransactionFlowOption.Mandatory)]
        int CreateCategory(Category category);

        [OperationContract]
        [TransactionFlow(TransactionFlowOption.Mandatory)]
        void DeleteCategory(int categoryId);

        [OperationContract]
        List<Category> GetAllCategories();
    }
}

When creating or deleting a category, I would want these operations to be considered as part of the transaction. So I add the attribute TransactionFlow with the TransactionFlowOption set to Mandatory. This means that the operation must be called with a transaction flow. The transaction flow is turned on in configuration and that is shown later in the article.

The Category class used in the contract above is pretty simple. It has all public fields in it. I was going to use properties with the notation public string Name { get; set; } but I wanted to keep the source code to just .NET 3 only. Here is the Category class:

C#
using System.Runtime.Serialization;

namespace Common
{
    [DataContract]
    public class Category
    {
        [DataMember]
        public int CategoryId;

        [DataMember]
        public string Name;

        [DataMember]
        public string Description;
    }
}

Transactional WCF Service

The next step is to create the service itself and have it implement the service contract above.

C#
[ServiceBehavior]
public class TransactionalWebService : ITransactionalWebService
{
    [OperationBehavior(TransactionScopeRequired = true)]
    public int CreateCategory(Category category) { ... }

    [OperationBehavior(TransactionScopeRequired = true)]
    public void DeleteCategory(int categoryId) { ... }

    [OperationBehavior]
    public List<Category> GetAllCategories() { .. }
}

The only differences above from a non-transactional service are the TransactionScopeRequired settings on the OperationBehavior. This means what you think it does. This flag doesn't need to be set to true. The operation could always work with or without a transaction. For the purposes of this example, we'll force the need for a transaction scope so it will either enlist in the transaction that is "flowed" in or will create one to execute the operation.

Service Web.Config

The configuration settings for the web.config require some changes to the binding.

XML
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
    <system.serviceModel>
        <services>
            <service name="TransactionalWebService" 
                 behaviorConfiguration="ServiceBehavior">
                <endpoint address="" binding="wsHttpBinding" 
                     bindingConfiguration="Binding1"
                    contract="Common.ITransactionalWebService"/>
                <endpoint contract="IMetadataExchange" binding="mexHttpBinding" 
                     address="mex" />
            </service>
        </services>
        <bindings>
            <wsHttpBinding>
                <binding name="Binding1" transactionFlow="true">
                </binding>
            </wsHttpBinding>
        </bindings>
        <behaviors>
            <serviceBehaviors>
                <behavior name="ServiceBehavior" returnUnknownExceptionsAsFaults="True">
                    <serviceMetadata httpGetEnabled="true" />
                </behavior>
            </serviceBehaviors>
        </behaviors>
    </system.serviceModel>
</configuration>

The most important setting here is the transactionFlow setting on the binding has to be set to true.

Client Proxy

Next, create a proxy to call the service from the client side. Instead of adding a Web reference, I created my own custom proxy to keep everything to the bare minimum. The code is very straightforward:

C#
using System.Collections.Generic;
using System.ServiceModel;
using Common;

namespace ClientSide
{
    public class TransactionalWebServiceProxy : ClientBase<ITransactionalWebService>, 
        ITransactionalWebService
    {
        public int CreateCategory(Category category)
        {
            return base.Channel.CreateCategory(category);
        }

        public void DeleteCategory(int categoryId)
        {
            base.Channel.DeleteCategory(categoryId);
        }

        public List<Category> GetAllCategories()
        {
            return base.Channel.GetAllCategories();
        }
    }
}

The default constructor usually means it will just go to the configuration file for its information. The configuration file looks like this:

XML
<configuration>
    <system.serviceModel>
        <bindings>
            <wsHttpBinding>
                <binding name="WSHttpBinding_TransactionalService" 
                        transactionFlow="true">
                </binding>
            </wsHttpBinding>
        </bindings>
        <client>
            <endpoint address="http://localhost/WsatTest1WebService/Service.svc"
                binding="wsHttpBinding"
                bindingConfiguration="WSHttpBinding_TransactionalService"
                contract="Common.ITransactionalWebService"
                name="ITransactionalWebService">
            </endpoint>
        </client>
    </system.serviceModel>
</configuration>

Notice that the transaction flow is set to true on the client side binding as well.

Test Program

There is a test console application included in the source. It creates a transaction scope and calls the proxy to perform some manipulation of data on the database. One test is just a litmus test to make sure everything works. The second test creates a category and then tries to delete one which doesn't exist (which the service treats as an error). Here is how the second test case works.

C#
int origNumRows = -1;
try
{
    using (TransactionalWebServiceProxy proxy = new TransactionalWebServiceProxy())
    {
        // Get the original number of categories from the table.
        origNumRows = proxy.GetAllCategories().Count;

        // Notice that we've already used the proxy without flowing a transaction
        // to call GetAllCategories.  To call the Create and Delete methods, we
        // need to have a transaction scope because we declared it as mandatory.
        using (TransactionScope scope = new TransactionScope())
        {
            // Create a normal category.  The service code will close the connection
            // after its done.  So, if it didn't participate in the distributed
            // transaction, then there will be a category record out there that
            // doesn't belong.
            Category category = new Category();
            category.Name = "I don't belong";
            category.Description = "Delete Me";
            category.CategoryId = proxy.CreateCategory(category);

            // Try to delete something which doesn't exist.
            proxy.DeleteCategory(666);

            // We should never get to the line below since the above method call
            // should throw an exception.
            scope.Complete();
        }
    }
}
catch {} // We're expecting an exception to occur.

// The exception above cause the channel to be faulted.  If we used the proxy object
// created above and called another method on it, it would fail with a message indicated
// that the channel has faulted.
using (TransactionalWebServiceProxy proxy = new TransactionalWebServiceProxy())
{
    int newNumRows = proxy.GetAllCategories().Count;
    if (newNumRows != origNumRows)
        Console.WriteLine("Failure");
    else
        Console.WriteLine("Success");
}

Summary

There is a lot of material out there on MSDN and around the Internet on how to write transactional Web services in WCF, so I understand that this article is not covering a new subject anymore. However, when it was originally written back in March of 2006, it was new. A lot has changed since then and the contents of this article became invalid with new releases. It's frustrating to turn up a bunch of old information during a Web search because it sends you down the wrong path for a while. So, I decided to update the article just to make sure I don't cause anybody (else) any more grief.

Further Reading

Microsoft has a forum out there for help on transactions: Transactions Programming[^]

There's also a great page out there with more information on Transaction Management in Windows[^].

History

  • 1.0: 2006-03-22: Initial revision
  • 2.0: 2007-01-30: Updated to match final beta version of .NET 3
  • 3.0: 2008-06-12: Updated again for release version of .NET 3

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Microsoft
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralDoesn't work for me Pin
Murph Dogg4-Nov-08 6:10
Murph Dogg4-Nov-08 6:10 
Generalthe transaction was aborted Pin
maximvs9-Jun-08 18:57
maximvs9-Jun-08 18:57 
GeneralRe: the transaction was aborted Pin
Dustin Metzgar10-Jun-08 12:28
Dustin Metzgar10-Jun-08 12:28 
GeneralRe: the transaction was aborted Pin
maximvs10-Jun-08 12:43
maximvs10-Jun-08 12:43 
GeneralRe: the transaction was aborted Pin
Dustin Metzgar10-Jun-08 19:05
Dustin Metzgar10-Jun-08 19:05 
GeneralRe: the transaction was aborted Pin
Dustin Metzgar11-Jun-08 18:33
Dustin Metzgar11-Jun-08 18:33 
GeneralRe: the transaction was aborted Pin
maximvs13-Jun-08 8:25
maximvs13-Jun-08 8:25 
GeneralBuilding and running on XP, VS.2005 Pin
Ingo Lundberg24-Apr-07 5:18
Ingo Lundberg24-Apr-07 5:18 
QuestionHelp..error in WCF.. Pin
Jason Law11-Mar-07 21:13
Jason Law11-Mar-07 21:13 
AnswerRe: Help..error in WCF.. Pin
Dustin Metzgar12-Mar-07 2:00
Dustin Metzgar12-Mar-07 2:00 
GeneralRe: Help..error in WCF.. Pin
Jason Law12-Mar-07 15:00
Jason Law12-Mar-07 15:00 
GeneralWS-AtomicTransaction with .NET Framework 3.0 Pin
msteinle24-Jan-07 2:52
msteinle24-Jan-07 2:52 
GeneralRe: WS-AtomicTransaction with .NET Framework 3.0 Pin
Dustin Metzgar24-Jan-07 11:25
Dustin Metzgar24-Jan-07 11:25 
GeneralRe: WS-AtomicTransaction with .NET Framework 3.0 Pin
Dustin Metzgar6-Feb-07 8:08
Dustin Metzgar6-Feb-07 8:08 
GeneralRe: WS-AtomicTransaction with .NET Framework 3.0 Pin
msteinle6-Feb-07 20:10
msteinle6-Feb-07 20:10 
GeneralRe: WS-AtomicTransaction with .NET Framework 3.0 Pin
Dustin Metzgar7-Feb-07 2:25
Dustin Metzgar7-Feb-07 2:25 
GeneralRe: WS-AtomicTransaction with .NET Framework 3.0 Pin
msteinle7-Feb-07 2:40
msteinle7-Feb-07 2:40 
QuestionRe: WS-AtomicTransaction with .NET Framework 3.0 [modified] Pin
Jason Law1-Mar-07 17:03
Jason Law1-Mar-07 17:03 
AnswerRe: WS-AtomicTransaction with .NET Framework 3.0 [modified] Pin
Dustin Metzgar1-Mar-07 17:43
Dustin Metzgar1-Mar-07 17:43 
GeneralThank you very much ^_^ Pin
Jason Law1-Mar-07 17:57
Jason Law1-Mar-07 17:57 
GeneralError on DB insertion Pin
DP IGT11-Jun-06 8:38
DP IGT11-Jun-06 8:38 
GeneralRe: Error on DB insertion Pin
Dustin Metzgar13-Jun-06 4:25
Dustin Metzgar13-Jun-06 4:25 
GeneralRe: Error on DB insertion [modified] Pin
DP IGT13-Jun-06 10:41
DP IGT13-Jun-06 10:41 
GeneralRe: Error on DB insertion Pin
Dustin Metzgar13-Jun-06 10:53
Dustin Metzgar13-Jun-06 10:53 
I guess the first thing that jumps out at me is that the user might not have the ability to make changes. Since this service is running in IIS, it will be under the ASPNET user (or NTAUTHORITY for 2003). Can you try impersonating an actual user on the box/domain or using a SQL server login?




Logifusion[^]
GeneralRe: Error on DB insertion Pin
DP IGT13-Jun-06 15:34
DP IGT13-Jun-06 15:34 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.