Introduction
COM+ distributed transactions and NHibernate are two very powerful tools available to the .NET programmer. This article shows how to get the two to cooperate in a simple way.
Background
Distributed transactions are my favorite part of using COM+. By opening up a distributed transaction, I can get a bunch of different databases, message queues, files, email, and almost anything else I can think of into the same transaction. The two-stage commit tells me that they all work or they all abort. And the transaction can be restarted from where it left off if a server goes down.
NHibernate is another powerful tool that I enjoy using. If you are not familiar with this ORM (object-relational mapper), then click here to visit the main site. NHibernate is built off of standard ADO.NET and also has capabilities to use transactions. But a regular ADO.NET transaction is for one database connection only. A COM+ distributed transaction can span multiple data sources. I wanted my NHibernate code to participate in a distributed transaction. This article shows how I accomplished this.
Using the code
There are several things I do in my code that you should be aware of:
- The COM+ project has a post-build on it. The post-build uses regsvcs to unregister and re-register the COM+ DLL. This adds a little waiting time to compilation but saves me the trouble of having to do it by hand. To make regsvcs work, I added the framework directory to my path (C:\Windows\Microsoft.NET\Framework\v1.1.4322). As always, if you change your path variable, you have to restart Visual Studio.
- I have created an NUnit test harness project as well to run the actual tests.
- In the post-build for the test harness project, I copy the App.Config to the target directory and give it the DLL name with ".config" appended to it. This is so NUnit can pick it up.
- Usually, I set up my test harness projects to be the startup projects. To make NUnit start the project, bring up the project properties window, select Configuration Properties->Debugging from the left side. Set the Debug Mode to Program, and hit Apply. For the start application, find the NUnit GUI executable (C:\Program Files\NUnit 2.2\bin\nunit-gui.exe). For the command line arguments, put the filename of the DLL (CPNTestHarness.dll).
- I have included a SQL script to create the tables that I use in the example. Put these tables into your database and change the connection string located in the App.Config.
- You may have noticed the weird way that I set up the NHibernate configuration. Instead of using the App.Config to set it up, I do it with code and pull the connection string from the App.Config. This is just a personal preference. I generally find that, in enterprise environments an App.Config just won't do the job, and usually pull my configuration settings from the Microsoft enterprise libraries. So I just quickly changed the code to work with this example.
OrmManager class
This class is basically how I handle talking to NHibernate. It creates the configuration and gets the ISessionFactory
. It exposes only three methods for this example: Save(object)
, Delete(object)
, and GetAll(Type)
. These should all be pretty familiar to the average NHibernate user. I didn't include a specific fetch simply because I didn't need it to do the tests.
The real meat of the whole article centers around the EnlistIfPossible()
method. When you are using regular ADO.NET, you can enlist your database connection in the COM+ distributed transaction by using the method EnlistDistributedTransaction
. Unfortunately, this method is not part of any interface. It is also not part of the System.Data.IDbConnection
class, which is basically all that NHibernate is going to give us. What we do know is that most of the implementations of IDbConnection
have an EnlistDistributedTransaction
method. One exception is the System.Data.SqlServerCe
library.
So, to get our connections to participate in a distributed transaction, we simply have to call the EnlistDistributedTransaction
method on the connection object, if it's there. To do this, I use reflection:
private static void EnlistIfPossible(System.Data.IDbConnection conn)
{
if (ContextUtil.IsInTransaction)
{
MethodInfo mi = conn.GetType().GetMethod("EnlistDistributedTransaction",
BindingFlags.Public | BindingFlags.Instance);
if (mi != null)
{
mi.Invoke(conn, new object[] {
(System.EnterpriseServices.ITransaction)
ContextUtil.Transaction });
}
}
}
Pretty simple stuff. Now, whenever we open up an NHibernate session, we can enlist the database connection into the COM+ transaction. This is done for Save
and Delete
.
public void Save(object obj)
{
ISession session = SessionFactory.OpenSession();
try
{
EnlistIfPossible(session.Connection);
session.SaveOrUpdate(obj);
session.Flush();
if (ContextUtil.IsInTransaction)
ContextUtil.MyTransactionVote = TransactionVote.Commit;
}
catch
{
if (ContextUtil.IsInTransaction)
ContextUtil.MyTransactionVote = TransactionVote.Abort;
throw;
}
finally
{
if (session != null && session.IsOpen)
session.Close();
}
}
Note - I am by no means saying that it is OK to start a new session every time you do something in NHibernate. That's just plain slow. I did it in this example to illustrate the point that the connections are separate but still operate under the same distributed transaction. It's nice to not have to hold a transaction object yourself and just let COM+ handle it for you.
TransactionController class
The OrmManager
will participate in a transaction if it exists. But it will not create a new transaction if there is not one. This is so that the user can pick whether to do transactions or not. To handle the transactions, there is a class that wraps around OrmManager
, called TransactionController
. This class basically allows me to start and end a transaction and do a whole bunch of ORM code in between.
Beginning and ending a transaction is a pretty standard chunk of COM+ code:
public void BeginTransaction()
{
ServiceConfig sc = new ServiceConfig();
sc.Transaction = TransactionOption.RequiresNew;
ServiceDomain.Enter(sc);
ContextUtil.MyTransactionVote = TransactionVote.Commit;
...
}
public void EndTransaction()
{
if (ContextUtil.MyTransactionVote ==
TransactionVote.Commit)
ContextUtil.SetComplete();
else
ContextUtil.SetAbort();
ServiceDomain.Leave();
...
}
Test Harness
The NUnit test harness illustrates how the code can be used:
TransactionController tc = new TransactionController();
Table1 t1; // My NHibernate object
Table2 t2; // My NHibernate object
try
{
tc.BeginTransaction();
t1 = new Table1();
t1.Num = 1;
t2 = new Table2();
t2.Num = 2;
tc.Save(t1);
tc.Save(t2);
}
finally
{
tc.EndTransaction();
}
It's really that simple. COM+ and NHibernate free the developer from having to think about a lot of low-level programming. Of course, the real world isn't always this simple, but that's for another article.