Sunday, November 19, 2006 11:10 AM bart

Windows Vista - Introducing TxF in C# (part 2) - Using System.Transactions and the DTC

Introduction

In the previous TxF related post you saw how to interact with the KTM in a very low level way through interop. Basically, we did deal with the transaction directly instead of relying on the System.Transactions namespace. In today's post we'll deal with the same use case, a transactional file delete, but this time using System.Transactions and the Distributed Transaction Coordinator (DTC). A little plumbing will need to be done, but as you'll see encapsulation comes to the rescue, allowing us to hide all nitty gritty details.

What we want to do

Let's take off by a trip to the promised land. This is excatly the kind of thing we want to do (or something similar):

 

using (TransactionScope tx = new TransactionScope()) { DeleteFile(file1); DeleteFile(file2); tx.Complete(); }
We even might want to make a transaction spanning across TxF and, for instance, SQL Server database operations. In short, we want the System.Transactions way of working. Let's see how we can do this.

 

Realizing our dream

On to the code. But before we do so, a small overview of what we have to do:

  1. Create some DeleteFile method (you might want to declare it as a static method in a File class, for instance in the namespace System.IO.TxF).
  2. In that method, see whether we are in a transaction scope or not (using Transaction.Current).
    • If we are running outside a transaction, you could do quite some things: you could create one (no!), you could throw an exception (maybe) or you could do a non-transacted file delete (our choice).
    • If a transaction is in flight, we need to derive the KTM transaction handle from it (which is the most tricky part) and use it to call the transacted file delete function.
  3. Call the DeleteFile method inside a TransactionScope to make it transactional.

Here's our DeleteFile method (cut-and-paste in the demo skeleton that will come next):

 

1 #region Transactional file operations 2 3 [DllImport("Kernel32.dll")] 4 static extern bool DeleteFileTransactedW([MarshalAs(UnmanagedType.LPWStr)]string file, IntPtr transaction); 5 6 [DllImport("Kernel32.dll")] 7 static extern bool CloseHandle(IntPtr handle); 8 9 static void DeleteFile(string file) 10 { 11 if (Transaction.Current != null) 12 { 13 IKernelTransaction tx = (IKernelTransaction)TransactionInterop.GetDtcTransaction(Transaction.Current); 14 IntPtr txh; 15 tx.GetHandle(out txh); 16 17 if (txh == IntPtr.Zero) 18 throw new Exception(); //Q-n-D 19 20 if (!DeleteFileTransactedW(file, txh)) 21 throw new Exception(); //Q-n-D 22 23 CloseHandle(txh); 24 } 25 else 26 { 27 File.Delete(file); 28 } 29 } 30 31 [ComImport] 32 [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 33 [Guid("79427A2B-F895-40e0-BE79-B57DC82ED231")] 34 internal interface IKernelTransaction 35 { 36 void GetHandle([Out] out IntPtr handle); 37 } 38 39 #endregion
Let's take a look inside. Lines 3 and 4 are an old friend from our previous post. Inside the DeleteFile method, some interesting things are going on. First (11) we check whether or not we have a transaction in progress. If not (25-28) we just perform a non-transacted file delete (notice you might want to change this, e.g. to throw an exception). Otherwise, we do this:

 

  • Line 13 is a call to a much unknown TransactionInterop class that allows use to get the IDtcTransaction for a transaction (in our case the current one). We do cast this immediately to a IKernelTransaction (see further).
  • On line 15 we obtain the handle to the KTM kernel transaction. Notice we have to close it too (line 23) once we're done with it (we could do this in a better way by means of a SafeHandle kinda thing, which might be the subject of a later post).
  • Finally, the transacted file delete is done on line 20. If it fails, we fail the method by means of a (quick-n-dirty == Q-n-D) far-too-generic (:$) exception on line 21. In reality you'd analyze the last Win32 error and throw an appropriate exception. Anyhow, throwing an exception will cause the transaction to fail as a whole.

But what's that ugly thing on lines 31-37? In short, it a tlbimp-like thing derived from transact.h (see Windows SDK). Basically it's an import of a COM interface with a specified GUID, that derives from our pre-.NET friend IUnknown. Here's the original declaration (see how it maps to the C# equivalent declaration):

 

MIDL_INTERFACE("79427A2B-F895-40e0-BE79-B57DC82ED231") IKernelTransaction : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE GetHandle( /* [out] */ HANDLE *pHandle) = 0; };

 

The demo

Wanna see it in action? Here's the demo, mutated from the demo in the previous post. Just copy-past the code above in it:

using System; using System.Runtime.InteropServices; using System.IO; using System.Transactions; namespace TxF { class Program { static void Main(string[] args) { // // Demo setup. // string file1 = "c:\\temp\\txf1.txt"; string file2 = "c:\\temp\\txf2.txt"; using (StreamWriter sw = System.IO.File.CreateText(file1)) sw.WriteLine("Hello World"); using (StreamWriter sw = System.IO.File.CreateText(file2)) sw.WriteLine("Hello World"); // // Start the demo. // Console.WriteLine("Press <ENTER> to start the transaction."); Console.ReadLine(); // // Make it transacted. // using (TransactionScope tx = new TransactionScope()) { // // Delete the files (transacted). // DeleteFile(file1); DeleteFile(file2); // // Commit or rollback? // char c; do { Console.WriteLine("{0} {1}.", file1, System.IO.File.Exists(file1) ? "still exists" : "has vanished"); Console.WriteLine("{0} {1}.", file2, System.IO.File.Exists(file2) ? "still exists" : "has vanished"); Console.Write("Commit transaction (Y/N)? "); c = (char)Console.Read(); } while (c != 'Y' && c != 'y' && c != 'N' && c != 'n'); if (c == 'Y' || c == 'y') tx.Complete(); } } /* * REPLACE THIS #region Transactional file operations ... #endregion * */ } }
The demo script is the same as in the previous post, just see what happens if:
  • You commit the transaction.
  • You don't commit (= rollback) the transaction.
  • If you omit the TransactionScope.
  • If you lock any of the files prior to execution.
  • If you try to lock a file after DeleteFile has been called.

Use the locker.exe tool from the previous post to perform these experiments. Have fun!

Coming next on TxF adventures...

Even more transactional functions in a small starter System.IO.TxF namespace (step-by-step). Keep watching!

kick it on DotNetKicks.com

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Filed under: , ,

Comments

# The November 06 Month Report

Friday, December 01, 2006 4:06 AM by B# .NET Blog

Yet another great (well, at least in my opinion) month of Daily Blogging . Once more, feedback from readers