Click here to Skip to main content
Email Password   helpLost your password?

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:

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.

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:

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.

[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.

<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:

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:

<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.

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

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralDoesn't work for me
Murph Dogg
7:10 4 Nov '08  
I have followed this down to the letter. I have 2 methods one that inserts into a table and another that is going to throw an exception. The first inserts and the second throws the error, and when I check the table, the inserted record is right there...I would expect it to be rolled back, but not the case.

One caveat is that it will not let me set it to TransactionFlowOption.Mandatory...I have to set it to TransactionFlowOption.Allowed. If i try to set it to TransactionFlowOption.Mandatory, then I get:

At least one operation on the 'Service1' contract is configured with the TransactionFlowAttribute attribute set to Mandatory but the channel's binding 'WSHttpBinding' is not configured with a TransactionFlowBindingElement. The TransactionFlowAttribute attribute set to Mandatory cannot be used without a TransactionFlowBindingElement.

Heres my config

<system.serviceModel>
<services>
<service name="TransactionService.Service1" behaviorConfiguration="TransactionService.Service1Behavior">
<endpoint address="" binding="wsHttpBinding" name="Binding1" contract="TransactionService.IService1"></endpoint>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
</service>
</services>
<bindings>
<wsHttpBinding>
<binding name="Binding1" transactionFlow="true"></binding>
</wsHttpBinding>
</bindings>
<behaviors>
<serviceBehaviors>
<behavior name="TransactionService.Service1Behavior">
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="true"/>
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>


Murph
Generalthe transaction was aborted
maximvs
19:57 9 Jun '08  
Hi,

Today I tried this solution, however, always aborts the transactions.
When I run the solution, the transaction block ends and does not throw any exception, however, the transactions statistics of the MSDTC show that every time I try to run the example, the transaction is aborted.

I have no idea of this behavior on my environment: Windows XP Professional SP2, VS2005 Team System with SP1.

Thanks,

Vidal Gutiérrez Ch.

GeneralRe: the transaction was aborted
Dustin Metzgar
13:28 10 Jun '08  
Vidal,

Before you get to the end of the transaction block, do you explicitly call commit? I think the general flow is:
using (TransactionScope scope = new TransactionScope()) {
// Do transaction work...
scope.Commit();
}
You have to call Commit, otherwise it will always abort the transaction. If that's not it, is there an exception being thrown?

“Ooh... A lesson in not changing history from Mr. I'm-my-own-grandpa” - Prof. Farnsworth

GeneralRe: the transaction was aborted
maximvs
13:43 10 Jun '08  
I have added the sentence scope.Complete() just before the end of the using block as you suggest

using (TransactionScope scope = new TransactionScope()) {
// Do transaction work...
scope.Complete();
}

and an exception has been thrown: "the transaction was aborted"

Vidal Gutiérrez Ch.

GeneralRe: the transaction was aborted
Dustin Metzgar
20:05 10 Jun '08  
Well, it looks like things have changed quite a bit since I last updated this article. The article really does need to be rewritten. I'd also like to remove NHibernate from it since that was mostly a carry over from putting NHibernate into a distributed transaction, then into TIP, and then into WS-AT. I will do my best to get this updated by tomorrow night. I did manage to get the code working but I believe I made more changes than necessary, so I will roll back my VM and start from the beginning.

“Ooh... A lesson in not changing history from Mr. I'm-my-own-grandpa” - Prof. Farnsworth

GeneralRe: the transaction was aborted
Dustin Metzgar
19:33 11 Jun '08  
I have updated the article and the source code. Unfortunately, I cannot update the article on Code Project myself. I have to submit it to them so they can update it. It's been submitted to them already and I'm not sure what the turnaround time usually is. If you need me to send it to you, email me.

“Ooh... A lesson in not changing history from Mr. I'm-my-own-grandpa” - Prof. Farnsworth

GeneralRe: the transaction was aborted
maximvs
9:25 13 Jun '08  
Thanks Dustin,

This morning I have tested the las version of the code and works perfectly.

Vidal Gutiérrez Ch.

GeneralBuilding and running on XP, VS.2005
Ingo Lundberg
6:18 24 Apr '07  
Hi!

Tnx, for the stuff.

Had a little trouble getting the sample to run.
Figured it out. The supplied NHibernate.dll refers to a version of log4net that wasn't in the zip. Retrieved NH 1.2 from SourceForge.

Thought you might want to know.


Regards,
Ingo

QuestionHelp..error in WCF..
Jason Law
22:13 11 Mar '07  
May I know what is the following error means and how to fix it?

{"The header 'CoordinationContext' from the namespace 'http://schemas.xmlsoap.org/ws/2004/10/wscoor' was not understood by the recipient of this message, causing the message to not be processed. This error typically indicates that the sender of this message has enabled a communication protocol that the receiver cannot process. Please ensure that the configuration of the client's binding is consistent with the service's binding. "}


Jason Law

AnswerRe: Help..error in WCF..
Dustin Metzgar
3:00 12 Mar '07  
At first glance, it sounds like the client and the server are expecting different things. In particular, one is trying to use WS-Coordination but the other is not expecting that. Are your client and server both WCF? Did you generate the client proxy using the tool? Here's a link to a set of WCF tools that you can use that might help you: WCF Tools[^].

“Ooh... A lesson in not changing history for Mr. I'm-my-own-grandpa” - Prof. Farnsworth

GeneralRe: Help..error in WCF..
Jason Law
16:00 12 Mar '07  
Thank you so much, it is true that the client and the server are expecting different thing, I have resolved the issue.
Thanks again. Smile

Jason Law

GeneralWS-AtomicTransaction with .NET Framework 3.0
msteinle
3:52 24 Jan '07  
Hello Dustin,

have you planned to update your article for the final release of .NET Framework 3.0 / Windows Vista?
I tried to run your example with the final release and found out that there are quite a lot of differences to the beta version you used.

Regards,
Martin
GeneralRe: WS-AtomicTransaction with .NET Framework 3.0
Dustin Metzgar
12:25 24 Jan '07  
Martin,

Well, that's a very good question. I had initially planned to keep my article updated as the technology changed, but I got lazy. Also, nobody asked about it (until now), so I forgot about it. With the changes to WCF slowing down and getting solid, I think now would be a good time to revisit this article. I'll send you another reply to this message to let you know when the article gets updated.

Thanks,
Dustin


GeneralRe: WS-AtomicTransaction with .NET Framework 3.0
Dustin Metzgar
9:08 6 Feb '07  
Well, I updated the code and the article last week and submitted it to Code Project. Unfortunately, the editors are either busy or ignoring me and haven't updated the article yet. If you want, I can send it to you directly.


GeneralRe: WS-AtomicTransaction with .NET Framework 3.0
msteinle
21:10 6 Feb '07  
Thank you for your support.
In the meantime, I had enough time to make it work myself, using your article as a good base.
I even managed interop with a Java (JBoss) client using ws-transactions and ws-security.


GeneralRe: WS-AtomicTransaction with .NET Framework 3.0
Dustin Metzgar
3:25 7 Feb '07  
Fantastic! Could you tell me which version of JBoss you were using? I heard they were doing it, just never investigated to find out which version.


GeneralRe: WS-AtomicTransaction with .NET Framework 3.0
msteinle
3:40 7 Feb '07  
I am using JBoss 4.0.3sp1 with JBossTS 4.2.2 package (JBossTS is the new name of arjuna transactions).
It was rather straight forward to get interop running after both JBossTS and WSC with transactions worked. The only difficult thing (for me) was to configure SSL with mutual authentication as WCF requires this for communication between transaction participants.
Perhaps I have to mention that I had to use a custom binding, because the web service stack JBoss 4.0.3sp1 (ws4ee) supports only soap 1.1. And, very weird, I found that JBoss has a problem with wsdl documents using xsd:import in its wsdl:types section, what wsdl documents created by WCF massively do. But after manually integrating the imported xsds into the wsdl, communication worked.
QuestionRe: WS-AtomicTransaction with .NET Framework 3.0 [modified]
Jason Law
18:03 1 Mar '07  
Hello Dustin,

Can you please send me your updated code and article?

Thanks in advance.



-- modified at 23:56 Thursday 1st March, 2007

Jason Law

AnswerRe: WS-AtomicTransaction with .NET Framework 3.0 [modified]
Dustin Metzgar
18:43 1 Mar '07  
Here, try this link (Link omitted since article was updated). I'm resubmitting the article changes to the Code Project editors. In the meantime, I hope everyone can use this link to get to the new code.

P.S. - I'd suggest modifying your message to remove your email address so you don't become a spam target.

Logifusion[^]

-- modified at 17:20 Monday 5th March, 2007
GeneralThank you very much ^_^
Jason Law
18:57 1 Mar '07  


Jason Law
GeneralError on DB insertion
DP IGT
9:38 11 Jun '06  
Hi Dustin,

Can you tell me why I get the error, "System.ServiceModel.FaultException : could not insert: [DataContainers.Table1]" when I run the TestHarness? The table is in the database, the connection string is set to open the correct database, yet I get an insertion error on the first call to "Save" in the first test. Any help you could provide would be greatly appreciated.Smile

Thanks,
Derrick
GeneralRe: Error on DB insertion
Dustin Metzgar
5:25 13 Jun '06  
I suspect this might be because the DTC is not set to run. Check out the previous article[^] and get the .reg file in it. That should turn on the DTC. Let me know if that works or not.


Logifusion[^]
GeneralRe: Error on DB insertion [modified]
DP IGT
11:41 13 Jun '06  
Thanks Dustin,

So far your article has been really helpful. It provides a lot of information regarding WS Transactions. Unfortunately, I still get errors in the TestHarness when I try to run. I wish I could get more detailed error explanations but it looks like there is a problem opening the DB...

Here are the errors I get in the TestHarness:
TestHarness.TestHarness.Test01_SaveDelete : System.ServiceModel.FaultException : cannot open connection
TestHarness.TestHarness.Test02_Transaction : System.ServiceModel.FaultException : cannot open connection

I made sure that DTC is running, I added the necessary changes to the registry as outlined in your article. I ran the SQL script to create the proper tables.

Within SQL I created a special database called WSTestDB for testing this. Everything (TestHarness, Service, and SQL) is running on the same local machine, and here are my changes to the connection string.
name="MyDatabase" connectionString="trusted_connection=true;Initial Catalog=WSTestDB;Data Source=(local);Connect Timeout=30; Pooling=true"

Any ideas on why I am getting these errors? D'Oh!

Thanks,
Derrick

-- modified at 16:42 Tuesday 13th June, 2006
GeneralRe: Error on DB insertion
Dustin Metzgar
11:53 13 Jun '06  
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
DP IGT
16:34 13 Jun '06  
Ok, Dustin you were right, it was the permissions on my database tables. ASPNET was not allowed to access the tables. Once I changed its permissions (for this test) I was able to update the database with no problems and the program worked fine. Obviously, I need to decide what type of users I want to access my DB from a Web Service.
Thanks for your help! Great article!


Last Updated 12 Jun 2008 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010