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.
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.
There are several things I do in my code that you should be aware of:
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.
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();
...
}
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.