posted on Tuesday, February 15, 2005 11:10 PM by taylorza

Propagating Distributed Transactions – A .NET Remoting Solution

Note that this article is an early draft. The final version will include a link to the sample code. If you cannot wait please contact me for the current version of the sample.

One of the really powerful and I believe underused services of Windows 2003 and now Windows XP SP-2 is Services Without Components. This allows your application to participate in a distributed transaction without having to develop COM+ components, non-COM based solutions can take advantage of some of the same services as have been available to COM+ developers for years.

One of the stumbling blocks faced when using these newly exposed services is that they do not currently integrate well with the .NET Remoting as present in the .NET Framework 1.1. While this has been addressed very elegantly in Whidbey I fall into the group of developers that have to provide real solutions today. To this end I will share with you the basic principals that I have used to develop a Transactional Remoting Framework. The solution I will present here is a small subset of the complete solution which incorporates not only .NET Remoting but also Web Services and all relatively seamlessly.

As you may well know distributed transactions on the MS Windows platform are most commonly supported by Microsoft’s Distributed Transaction Coordinator (DTC) service. DTC supports a number of distributed transaction specifications, OLETx, XA and TIP. Microsoft resource managers such as SQL Server will participate in a distributed transaction using the OLETx protocol while other resource managers like Oracle use XA.

For our purposes we will make use of TIP (Transaction Internet Protocol) to assist us in our quest to propagate transactions across remoting boundaries. Firstly on Windows 2003 and Windows XP SP2 the TIP protocol is disabled so we will need to enable TIP support.

Enabling TIP on Windows 2003 and Windows XP SP2

There are two ways we can do this, one is via the windows UI and the other is via the registry. First I will describe the safer alternative.

1. From Administrative Tools open Component Services
2. Locate the Component Services node
3. Open the Component Services node and under which you will find the Computers node
4. Open the Computers node, here you will find the My Computer node.
5. Right click the My Computer node and select the Properties menu item.
6. No the My Computer Properties window should be open, locate the MSDTC tab and select it.
7. Near the bottom of the Tab Page you will see a button labeled Security Configuration. Click this button.
8. You should now have the Security Configuration dialog on the screen.
9. On this dialog ensure that the following check boxes are checked

a. Network DTC Access
b. Transaction Internet Protocol (TIP) Transactions

10. Leave the rest of the settings as they are and click OK, click OK on the properties dialog to dismiss the properties dialog. At this point DTC should restart automatically to enable the new options you have selected.

Now for the simple way to enable TIP support. We are going to fiddle in the registry so please read the standard MS disclaimer here regarding changing settings in the registry.

Open the following registry key in regedit.

HKEY_LOCAL_MACHINE\Software\Microsoft\MSDTC\Security

Under this key you will find a number of values, the ones we are interested in are

NetworkDtcAccess and NetworkDtcAccessTip. Ensure that both values are set to 1 and restart the MSDTC service either from the Services control panel applet or on the command line as follows.

C:>net stop msdtc

C:>net start msdtc

Back to the Transactions

From this point on I will refrain from explicitly stating Windows 2003 or Windows XP SP2 and assume that this is the given environment running .NET Framework 1.1. I will address similar issues in .NET Framework 1.0 in a later article (If I am forced too).

As described in many resources available on the Internet, initiating a distributed transaction is relatively simple. First you need to add a reference to you project to the System.EnterpriseServices assembly. Once you have done that, enlisting a new distributed transaction can be achieve with the following code.

ServiceConfig config = new ServiceConfig();
config.Transaction = TransactionOption.RequiresNew;
ServiceDomain.Enter(config);

The option TransactionOption.RequiresNew ensures that a new transaction is created regardless of other existing transactions. If you require enlisting in an existing transaction or enlisting a new transaction if one does not exist, you need only change the transaction option as follows:

ServiceConfig config = new ServiceConfig();
config.Transaction = TransactionOption.Required; ServiceDomain.Enter(config);

See MSDN for more detail on the options available.

Of course this is not the end of it; you still need to cast your vote as to the success of your portion of the work partaking in the distributed transaction. Distributed transactions go through a number of stages in determining the final outcome of the entire transaction, but the short version and at the risk of over simplifying is that each participating job votes for the success of the transaction and should one of the participating jobs vote negatively the entire transactions is aborted. Casting your vote for the outcome is as easy as calling

ContextUtil.SetComplete();
or
ContextUtil.SetAbort();

A call to ContextUtil.SetCompete() indicates successful completion of the job, while ContextUtil.SetAbort() indicates that a failure. Now you should be able to easily test this to see if it works by performing a database update, but rather than using a normal ADO.NET or database transaction place your database code between the call to ServiceDomain.Enter() and a call to ContextUtil.SetComplete() or ContextUtil.SetAbort(). The following example is written to be clear, not robust a more correct version would make use of try/catch/finally block.

// Enlist in transaction
ServiceConfig config = new ServiceConfig();
config.Transaction = TransactionOption.Required; ServiceDomain.Enter(config);

// Perform database action
SqlConnection oCon = new SqlConnection("Data Source=(local); Initial Catalog=DTCExample; User ID=Test;");
SqlCommand oCmd = oCon.CreateCommand();
oCmd.CommandText = "update Accounts set Balance = Balance + @amount where AccountNo=@accountNo";
oCmd.Parameters.Add("@amount", "7309233244");
oCmd.Parameters.Add("@accountNo", 100);
oCon.Open();
oCmd.ExecuteNonQuery();
oCon.Close();

// Vote for transaction outcome
ContextUtil.SetComplete();

The above version of the code should successfully perform a database update, but simply changing the call to ContextUtil.SetComplete() to a call to ContextUtil.SetAbort() will cause the transaction to rollback. Notice that there was no need for you to explicitly initiate a database transaction; this was taken care of by DTC and ADO.NET.

So what have we achieved? Well unfortunately not very much. Other than an expensive way to initiate a database transaction we have not done anything that we could not have done more efficiently with ADO.NET or the database directly.

So why do this you ask? Enterprise software developed today is able to harness the processing and storage power of multiple machines distributed across the enterprise. This enables one task to be broken into multiple jobs and each job can be completed independently on different servers in the enterprise, if one of the jobs fails all of the jobs should be rolled back. OR a less extravagant reason, you need to perform an update on multiple heterogeneous databases and if any one of the updates fails you need all the participating databases to rollback the work they have performed as part of this job. Requirements of this nature require that you can enlist each job into a distributed transaction that can be completed atomically.

Solving the above problems using COM+ or previously MTS was relatively painless assuming like myself you loved all things COM/COM+. And as times have changed so have the ways we develop software and the flavor of the day is .NET and a good flavor it is. The only problem is that both Web Services and .NET Remoting do not support propagating distributed transactions. In other words, if server A initiates a transaction and makes a remote call to a method on server B the job performed on server B will not be aware of the transaction and will therefore not have anyway to enlist in that transaction.

This is an unfortunate situation, however there are a number of solutions, one of which I will be describing here. As a demonstration I have come up with the following problem domain, I will have two servers A and B which host business objects which can be accessed via remoting. The database is a simple database containing only one table an Accounts table, this table has two columns AccountNo and Balance. A client applications will be developed that will update the data in the Accounts table via the business objects hosted on servers A and B.

One of the actions performed by the client application is a funds transfer, where one account is debited and another credited. This is a perfect example of where a transaction would be required since if the debit succeeds and the credit fails we would want both to rollback otherwise you will have your hands full with two very upset customers. But to make the problem interesting our business objects only provide a means to adjust the balance of a single account at a time so you would require two remote calls, one to debit the first account and another to credit the second account. In this scenario a distributed transaction could be a possible solution. The client could initiate a distributed transaction and within that context make the calls, the remote functions enlist in the distributed transaction they will each vote on the outcome, should on of the functions fail the entire transaction is rolled back and our database is in a consistent state. To make it even more interesting I thought I would have the client make the call to each function on a different server using a different protocol.

So now that I have the scenario set I will we can start to get the basic infrastructure in place. On SQL Server I created a database called DTCExample the following script creates the single table we will require for this sample, the Accounts table and the two records we will be using.

create table Accounts
(
  AccountNo varchar(20) primary key,
  Balance decimal
)
go

insert into Accounts (AccountNo, Balance) values ('7309233244', 1000.00)
insert into Accounts (AccountNo, Balance) values ('7810072421', 1000.00)

Next we need to create the remoting server, which provided a single function AdjustAccountBalance. A version of this function which does not include any transaction details looks as follows:

public void AdjustAccountBalance(string account, decimal amount)
{
  SqlConnection oCon =
new SqlConnection("Data Source=(local); Initial Catalog=DTCExample; User ID=test;");
  SqlCommand oCmd = oCon.CreateCommand();
 
 
oCmd.CommandText = "update Accounts set Balance = Balance + @amount where AccountNo=@accountNo";
  oCmd.Parameters.Add("@amount", amount);
  oCmd.Parameters.Add("@accountNo", account);

  int rowsAffected;
  oCon.Open();
  rowsAffected = oCmd.ExecuteNonQuery();
  oCon.Close();
}

As you can see this rather naïve implementation gets the job done by performing an update on the database adjusting the relevant accounts balance by the amount passed to the function.

The client code will perform a funds transfer between two accounts on the database by calling the AdjustAccountBalance function on the remoting server. The code to achive this on the client looks like the following.

IDataService tcpService;
IDataService httpService;
tcpService = (IDataService)Activator.GetObject(
typeof(IDataService), "tcp://localhost:1235/DataService.rem");
httpService = (IDataService)Activator.GetObject(
typeof(IDataService), "http://localhost:1234/DataService.rem");

tcpService.AdjustAccountBalance("7309233244", 10);
httpService.AdjustAccountBalance("7810072421", -10);

As you can see this code connects to two separate instances of the server each using a different channel, and the AdjustAccountBalance call is made to each server.

If you look at the code snippets above you will quickly realize that there is a glaring problem. If one of the calls to AdjustAccountBalance fails for some reason the database will be in an inconsistent state i.e. only one of the accounts has been updated so some one has lost money, either the bank of the customer.

With minor adjustments to the code we can solve this problem, the following updated code includes calls to the transaction classes we will be looking at later which help overcome the above shortcoming.

Note: The class TransactionContext in this code will be discussed later, don’t go looking for it in the MSDN or on Google.

The server AdjustAccountBalance now looks like this.

public void AdjustAccountBalance(string account, decimal amount)
{
 
using (TransactionContext tx = TransactionContext.Enter())
 
{
   
SqlConnection oCon = new SqlConnection("Data Source=(local); Initial Catalog=logika; User ID=sa;");
    SqlCommand oCmd = oCon.CreateCommand();
    oCmd.CommandText = "update Accounts set Balance = Balance + @amount where AccountNo=@accountNo";
    oCmd.Parameters.Add("@amount", amount);
    oCmd.Parameters.Add("@accountNo", account);
   
   
int rowsAffected;
    oCon.Open();
    rowsAffected = oCmd.ExecuteNonQuery();
    oCon.Close();

    if (rowsAffected == 1)
     
tx.Complete();
  
}
}

And the client code is changed as follows.

using (TransactionContext tx = TransactionContext.Enter())
{
  tcpService.AdjustAccountBalance("7309233244", 10);
  httpService.AdjustAccountBalance("7810072421", -10);
  tx.Complete();
}

The server code has undergone the most drastic change. A transaction context is entered by calling TransactionContext.Enter() this returns an instance of the TransactionContext which can be used to indicated the outcome of the transaction. For this business function a simple criteria was chosen, if a single record was updated then we succeeded and can call tx.Complete() to indicate that everything went well.

On the client we enter a TransactionContext is the same way as the server, only our criterion for success is different. In this cas if we reach the end of the code block without incident or exception then we assume this part has completed successfully. If all parties have completed successfully then the transaction is committed otherwise the database remains unaffected. In the sample you can try adjusting the balance of an invalid account number; in that case the transaction will rollback and nether update will have an effect.

So where did TransactionContext come from? Well this is just a very basic wrapper around some of the concepts I am going to detail here. From what we know so far, starting the distributed transaction is relatively simple and we saw the code for that earlier in this article. But we also know that .NET Remoting does not flow this transaction to the server side, if we simply tried to enlist in a transaction on the server then a new transaction will be started since the server is not aware of the transaction enlisted on the client.

One solution and the one I am going to address here is to pass some form of transaction ID to the server which uniquely identifies the distributed transaction enlisted on the client, the server can use this ID to locate and enlist in the distributed transaction. Now the question is what ID we use. If you recall in the beginning of this article we enabled TIP support, this is the key, we will request the TIP Url of the current transaction on the client and send that along with the call to the remote server. Requesting the TIP url requires some minor gymnastics, but nothing to strenuous. Firstly we need to get the Transaction interface of the client side transaction, the following code demonstrates this:

// Enlist in transaction
ServiceConfig config = new ServiceConfig();
config.Transaction = TransactionOption.Required; ServiceDomain.Enter(config);
object trxn = ContextUtil.Transaction;

Next we need to query the transaction for the ITipTransaction interface. Those of you familiar with COM will want to perform a QueryInterface call, fortunately all this requires in C# is a cast to the correct type, the call to QueryInterface will be taken care of by .NET.

ITipTransaction trxn = (ITipTransaction)ContextUtil.Transaction;

The only question is, where does ITipTransaction come from. Well unfortunately we have to construct the interface ourselves; fortunately it is a rather simple interface. The C definition of the interface can be found in TxCoord.h in the include directory of the Platform SDK. From that definition we can define the following C# interface.

[ Guid("17CF72D0-BAC5-11d1-B1BF-00C04FC2F3EF") ]
[ InterfaceType(ComInterfaceType.InterfaceIsIUnknown) ]
public interface ITipTransaction
{
 
void Push([In, MarshalAs(UnmanagedType.LPStr)] string pszRemoteTmUrl,
    [In, Out, MarshalAs(UnmanagedType.LPStr)]
ref string ppszRemoteTxUrl);

  void GetTransactionUrl([In, Out, MarshalAs(UnmanagedType.LPStr)]
   
ref string ppszLocalTxUrl);
}

And the key function for our purposes is GetTransactionUrl(). Calling this function will provide us with a url which identifies not only the transaction, but also the transaction manager that should be contacted to enlist in the transaction. The following code demonstrates how to retrieves the TIP url.

string url = null;
trxn.GetTransactionUrl(
ref url);

Now all that remains to be done is to get the TIP url to the remote server and have the remote server use the url to enlist in the transaction. First getting the url to the server, we could take the obvious approach and pass the url as an argument to the function call, that would work. There is another alternative and that is to use the CallContext, a CallContext provides a collection of Key/Value pairs which can be accessed along the call path. When a remoting call is made all the values in that collection that implement the ILogicalThreadAffinitive interface are automatically included in the CallContext on the remoting function. So we want our TIP url transparently transported for all our remote calls all we need to do is add the TIP url to the call context and ensure that the object that contains the TIP url implements ILogicalThreadAffinitive. For this purpose I have created the TransactionCallContext class which implements ILogicalThreadAffinitive and provides a property to expose the TIP url stored in a member of the class.

[Serializable]
public class TransactionCallContext :
System.Runtime.Remoting.Messaging.ILogicalThreadAffinative
{
 
private string _tipUrl = null;
 
private TransactionCallContext()
  {}

  private TransactionCallContext(string tipUrl)
  {
    _tipUrl = tipUrl;
  }

  public string TipUrl
  {
   
get {return _tipUrl;}
  }

  public static TransactionCallContext FromTipUrl(string tipUrl)
  {
   
TransactionCallContext ctx = new TransactionCallContext(tipUrl);
   
return ctx;
  }
}

Armed with out TransactionCallContext class we can now enlist a transaction and add the TIP url into the CallContext, this will ensure that our remote calls will have access to the TIP url via the CallContext created on the remote server. The following code demonstrates enlisting the transaction and putting the TIP url into the CallContext.

// Enlist in transaction
ServiceConfig config = new ServiceConfig();
config.Transaction = TransactionOption.Required; ServiceDomain.Enter(config);

string url = null;
ITipTransaction trxn = (ITipTransaction)ContextUtil.Transaction;
trxn.GetTransactionUrl(
ref url);
CallContext.SetData("DTCTxID",
TransactionCallContext.FromTipUrl(tx.TipUrl));

All we need to do now is ensure that the server code recognizes that there is a transaction ID in the CallContext and enlist in the transaction using the ID. To do this the server code must inspect the CallContext and enlist in the transaction if one is present.

ServiceConfig config = new ServiceConfig();
TransactionCallContext ctx =
  (TransactionCallContext)CallContext.GetData("DTCTxID");

config.Transaction = TransactionOption.Required;
if (ctx != null)
{
 
config.TipUrl = tipUrl;
}
ServiceDomain.Enter(config);

This code sets up a ServiceConfig as before, but if there is a TIP url in the CallContext then it assigns the url to the TipUrl property of the ServiceConfig instance. When the call to ServiceDomain.Enter() is made the local transaction manager contacts the remote transaction manager using the information contained in the TIP url and enlists in the distributed transaction identified by the transaction ID. Now all the work you do is part of the bigger distributed transaction. By wrapping all this code in a class you can you can achieve a clean code like I demonstrated earlier using a TransactionContext class.

Comments

# .net remoting的事务传播以及wcf分布式事务 @ Tuesday, March 25, 2008 8:47 PM

.net remoting的事务传播以及wcf分布式事务

Anonymous

# .net remoting的事务传播以及wcf分布式事务 @ Friday, April 25, 2008 10:36 AM

在.net1.1或者.net2.0中要实现分布式事务,如果不涉及远程调用,如调用remoting或者webservice的方法,应该说是一件非常简单的事情,只需要用COM (1.1/Servic...

Anonymous

# [转] .net remoting的事务传播以及wcf分布式事务 @ Thursday, June 19, 2008 3:00 AM

在.net1.1或者.net2.0中要实现分布式事务,如果不涉及远程调用,如调用remoting或者webservice的方法,应该说是一件非常简单的事情,只需要用COM (1.1/Servic...

Anonymous

# [转] .net remoting的事务传播以及wcf分布式事务 @ Thursday, June 19, 2008 3:07 AM

在.net1.1或者.net2.0中要实现分布式事务,如果不涉及远程调用,如调用remoting或者webservice的方法,应该说是一件非常简单的事情,只需要用COM (1.1/Servic...

Anonymous