Creating Transacted Files


I previously wrote this article on my blog, Think Big!.

Introduction

Lastly but not last, and after a long while, Windows Vista introduced a way to create transacted files or even to write to registry.

While market grows remarkably in the last years, its requirements increase as well. And every day you face a new problem that you must overcome to accommodate market requirements.

Transacted operations are one of the commonly demanded requirements by market.

While it's true that you can easily do database operations transactionally, it's not enough. Sometimes you will need to write files or make changes to registry in a transactional way.

With previous versions of Windows, you weren't able to create a file transactionally without writing a huge amount of disorganized code to create it manually. You couldn't use normal transactions or even COM+ Enterprise Services to create this type of transactions.

With Windows Vista you can easily create transacted files the common way you used to create normal files.


Unfortunately, .NET 3.5 also does not supports creating transacted files. So you need to dive into API to create it.
Not Windows Vista only that supports this type of transactions, Windows Server 2008, and future versions of Windows of course, also supports it.

Implementation

CreateFileTransacted() Function

To create a transacted file you can use the Kernel23.dll new function CreateFileTransacted().

This function takes the same arguments as the normal CreateFile() function plus three more arguments that we are interested of the first one of them, the transaction handle that will be used while creating the file and writing to it.

The definition of this function in C is as follows:

HANDLE CreateFileTransacted(
LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile,
HANDLE hTransaction,
PUSHORT pusMiniVersion,
PVOID pExtendedParameter);
We are interested in 6 parameters of these 9.
  • lpFileName:
    The path of the file being created.
  • dwDesiredAccess:
    Defines how file will be accessed. Read only (0x80000000), right only (0x40000000), or both read and write (0xC0000000).
  • dwShareMode:
    Defines the sharing mode -during the operation- for the file. Enabling reading file contents (0x1), writing to it (0x2), reading and writing (0x3), or deleting the file (0x4).
  • dwCreationDisposition:
    Action to take if file exist or do not exist. This argument can take one of the values:
    • 0x1:
      Create the file and throw an error if it exists.
    • 0x2:
      Always create the file even if it exists.
    • 0x3:
      Open the file and throw an error if it doesn't exist.
    • 0x4:
      Open the file or create it if it doesn't exist.
    • 0x5:
      Open the file and clear its contents. Or throw an error if it doesn't exist.
  • dwFlagsAndAttributes:
    Any additional options. This can be a combination of file attributes like Archive, Hidden, and Encrypted. Also it supports combining options like deleting the file after closing it.
  • hTransaction:
    A handle to our created transaction that we wish to use it in this operation. To get the handle you may take a step further into COM.

Also there're three arguments that we are not interested on, so you can safely pass it a NULL value:

  • lpSecurityAttributes:
    A SECURITY_ATTRIBUTES object contains an optional security descriptor information for the file.
  • hTemplateFile:
    A handle to a file to read it's attributes and options to fill the arguments automatically.
  • pusMiniVersion:
    The miniversion to be opened. When creating transacted file ,by specifying the hTransaction argument, this must be NULL.
  • pExtendedParameter:
    Reserved.

For a complete discussion of this API function, see MSDN Library.

PInvoking CreateFileTransacted() Function

PInvoke is a service that enables you to call unmanaged functions in DLLs, such as those in the Win32 API like the CreateFileTransacted() API function last mentioned.


PInvoke stands for Platform Invokation.

In order to PInvoke a function you need to know some things:
  1. Where this function resides (which DLL).
  2. Function definition and order of arguments -if found.-
  3. How to marshal between unmanaged types in .NET types.

Marshaling in .NET is the process of creating a bridge between new .NET types and legacy COM types. This is done by finding equivalents for those legacy types in the new data types or creating it if needed.

Most of the COM data types have equivalents in .NET, also it's very easy to create your own.

For our function, we could write it in C# as following:

[DllImport("kernel32.dll")]
public static extern
IntPtr CreateFileTransacted(
string lpFileName,
uint dwDesiredAccess,
uint dwShareMode,
IntPtr lpSecurityAttributes,
uint dwCreationDisposition,
uint dwFlagsAndAttributes,
IntPtr hTemplateFile,
IntPtr hHandle,
IntPtr pusMiniVersion,
IntPtr pExtendedParameter);

Code explanation:

static extern modifiers are required for creating PInvoke functions.

DllImport attribute used to define the DLL that contains the function.

System.IntPtr is the managed type equivalent to unmanaged HANDLE.

LPCSTR can be easily replaced by the managed System.String.


In unmanaged code DWORD is a 32-bit unsigned integer, so we could safely replace it with System.UInt32.
Because we need to pass a NULL value to the LPSECURITY_ATTRIBUTES argument we marshaled it as IntPtr, so we can use IntPtr.Zero to pass NULL values. We did that too with the last two arguments.
For creating the PInvoke methods easily, you could use the Red Gate's PInvoke.NET Visual Studio add-in.
Also you should visit the interop wiki.

After we create our PInvoke method we can call it. But after that we need to create the transaction and get its handle to fill the arguments of the method.

To get a transaction handle we need to dive into COM and call a method to get the handle. This method is wrapped to .NET using COM Interop.

[ComImport]
[Guid("79427A2B-F895-40e0-BE79-B57DC82ED231")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IKernelTransaction
{
void GetHandle(out IntPtr ktmHandle);
}


Don't forget adding a using System.Runtime.InteropServices statement.

Now we can call the method:


private const uint FILE_ATTRIBUTE_NORMAL = 0x80;
private const uint GENERIC_WRITE = 0x40000000;
private const uint CREATE_ALWAYS = 0x2;
private const uint FILE_SHARE_NONE = 0x0;

static void Main()
{
using (TransactionScope scope = new TransactionScope())
using (FileStream stream = CreateTransactedFile("D:\\SampleFile.txt"))
using (StreamWriter writer = new StreamWriter(stream))
{
writer.WriteLine("Hello, World!");

// To commit the transaction use the followind line
scope.Complete();
}
}

public static FileStream CreateTransactedFile(string fileName)
{
IKernelTransaction ktx =
(IKernelTransaction)TransactionInterop.GetDtcTransaction(Transaction.Current);

IntPtr txHandle;
ktx.GetHandle(out txHandle);

IntPtr fileHandle = CreateFileTransacted(fileName,
GENERIC_WRITE, FILE_SHARE_NONE, IntPtr.Zero,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL,
IntPtr.Zero, txHandle, IntPtr.Zero, IntPtr.Zero);

return new FileStream(fileHandle, FileAccess.Write);
}

Don't forget adding a reference to System.Transactions and a using statement.

Code explanation:

Constants define the input for the function arguments.

In method CreateTransactedFile() we get the handle of the ambient transaction created in method Main(). Then we call the CreateFileTransacted() function which returns a pointer to the create file. Lastly we created the FileStream object with the created the file handle and used it to create a StreamWriter object to write textual data to the file.


Calling Commit() of the transaction object ensures that all operations completed successfully and now we can apply it. Otherwise, nothing would be done. That's the main characteristic of transactions. All or nothing.

You don't need to close the transaction handle manually because when disposing the TransactionScope object it will close its handle automatically.

If you are interested in closing it manually you can use the .NET wrapper System.Runtime.InteropServices.Marshal.Release() method. Or try the difficult way using the CloseHandle unmanaged handle.

Transactional support not limited to creating a file only. Most file system operation now support transactions, some examples are CopyFileTransacted, CreateDirectoryTransacted, CreateHardLinkTransacted, DeleteFileTransacted, MoveFileTransacted, RemoveDirectoryTransacted, and SetFileAttributesTransacted.

Code Sample

Code sample is attached with the article.


Similar Articles