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()) {
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())
{
origNumRows = proxy.GetAllCategories().Count;
using (TransactionScope scope = new TransactionScope())
{
Category category = new Category();
category.Name = "I don't belong";
category.Description = "Delete Me";
category.CategoryId = proxy.CreateCategory(category);
proxy.DeleteCategory(666);
scope.Complete();
}
}
}
catch {}
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