Recently I’ve started playing around with Kernel Transaction Manager introduced with Vista. This remarkable (and largely unheralded) piece of technology allows developers to put transaction handling around certain IO based actions such as creating a directory or creating keys in the registry.
The name causes confusion for people because it implies that the transactions can only be used in kernel-mode transactions. This isn’t the case as KTM supports both kernel and user-mode transactions. The name actually means that the transaction engine is built into the Kernel. I suppose they didn’t just call it the Transaction Manager because they have shipped so many different transaction managers over the years that this would just end up confusing people.
First of all the bad news. As KTM exists solely in the kernel, it isn’t available on operating systems prior to Vista. If you use Vista or Server 2008, then you have all you need to start using it.
So, why do I think that transactional file activity is such good news? Well, it allows you to develop systems that follow the ACID principals for areas other than database activity. Suppose that you want to create a directory and write a file into it, but this should only persist if the operation the file depends on completes successfully. The old way of doing this would be to create the directory, write the file and then (if the operation fails), remove the file and directory. That’s a lot of work for you to keep track or, and potentially it’s very error prone. How much better it would be if you could create a transaction around these operations and only commit them if things work as you expect. I suppose an example is in order here.
First of all, you need to “hook” into the API to call the function. Sorry, but there is no managed code equivalent of this in the BCL. You have to wrap the API yourself.
[DllImport("kernel32.dll", SetLastError = true,
CharSet = CharSet.Auto)]
static extern bool RemoveDirectoryTransacted(
[MarshalAs(UnmanagedType.LPWStr)]string path,
IntPtr transaction);
This part of the code is necessary so that you can get access to the kernel transaction. As this is a COM call, you need to import the interface. You can call the interface what you like, the important thing is to use the Guid below, and the method name must be GetHandle.
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("79427A2B-F895-40e0-BE79-B57DC82ED231")]
internal interface IKernelTx
{
void GetHandle([Out] out IntPtr handle);
}
Then, you need to call this:
static void Main()
{
string path = @"c:\test";
if (!Directory.Exists(path))
Directory.CreateDirectory(path); using (TransactionScope tx = new TransactionScope())
{
IntPtr txh = null;
IKernelTx tx = (IKernelTx)TransactionInterop.
GetDtcTransaction(Transaction.Current);
tx.GetHandle(out txh);
RemoveDirectoryTransacted(test, txh);
}
}
There are a couple of bits to notice in this sample. First of all, we are using TransactionScope to create the transaction that we are going to work in. Now, you can’t simply pass this into the KTM method. You need to convert it into a pointer that the KTM method can work with (remember that the KTM is unmanaged). Anyway, it’s a simple matter to get the transaction. All you need to do is call the TransactionInterop.GetDtcTransaction for the current transaction. This maps to the COM interface I mentioned above, and you can retrieve the transaction pointer by calling GetHandle. Once you have this pointer, you can pass this into your transacted code.
Now if you run a sample like this, you will notice that the directory is not actually removed. Well, that’s what appears to happen but it’s not quite true. The directory is removed, but the removal is not committed because we haven’t told the transaction to commit. If you don’t commit the transaction then these operations are implicitly rolled back. So, how do you save the changes? Well, simply call Commit() on the transaction, and that will do it. In other words, after the RemoveDirectoryTransacted call, add tx.Commit();.
In a future post, I’ll discuss how you can be a good OS citizen and ensure this code doesn’t fail on older operating systems.