Friday, June 13, 2008 8:28 PM bart

LINQ to MSI - Part 1 - Interop

PattThis should really be the least interesting part of this series. But still, without any physical data access layer, there's no way to build abstractions on top of it. So, in this post of this series we'll take a look at some very simple MSI interop, giving us a basic data provider for MSI databases, mirrored after the typical structure of .NET data providers like System.Data.SqlClient.

 

Interop signatures

As all of you know, interop of native components in the world of the managed code CLR isn't that hard: System.Runtime.InteropServices is your friend. More specifically, we'll be importing stuff from msi.dll using DllImports. To see what's available there, you can either open up msi.h from the Windows SDK or run dumpbin /exports on the dll, or in a less geeky way you can simple go to MSDN to get some information. Ultimately you'll come up with the following signatures that are relevant to data access, just focusing on read operations for now:

internal static class MsiInterop
{
     [DllImport("msi.dll", EntryPoint="MsiOpenDatabaseW", CharSet=CharSet.Unicode, ExactSpelling=true)]
     public static extern uint MsiOpenDatabase(string databasePath, int persist, out IntPtr database);

     [DllImport("msi.dll", EntryPoint="MsiDatabaseOpenViewW", CharSet=CharSet.Unicode, ExactSpelling=true)]
     public static extern uint MsiDatabaseOpenView(IntPtr database, string query, out IntPtr view);

     [DllImport("msi.dll", EntryPoint="MsiViewExecute", CharSet=CharSet.Unicode, ExactSpelling=true)]
     public static extern uint MsiViewExecute(IntPtr view, IntPtr parameters);

     [DllImport("msi.dll", EntryPoint="MsiViewFetch", CharSet=CharSet.Unicode, ExactSpelling=true)]
     public static extern uint MsiViewFetch(IntPtr view, out IntPtr record);

     [DllImport("msi.dll", EntryPoint="MsiRecordGetStringW", CharSet=CharSet.Unicode, ExactSpelling=true)]
     public static extern uint MsiRecordGetString(IntPtr record, uint field, StringBuilder value, ref int bufferSize);

     [DllImport("msi.dll", EntryPoint="MsiRecordGetInteger", CharSet=CharSet.Unicode, ExactSpelling=true)]
     public static extern int MsiRecordGetInteger(IntPtr record, uint field);

     [DllImport("msi.dll", EntryPoint="MsiCloseHandle", CharSet=CharSet.Unicode, ExactSpelling=true)]
     public static extern uint MsiCloseHandle(IntPtr handle);
}

These functions are pretty simple to understand: using MsiOpenDatabase one opens a database, which can be used to define a query view using MsiDatabaseOpenView that subsequently can be executed using MsiViewExecute to yield results using MsiViewFetch, each of which can be accessed on a per-column basis using MsiRecordGetString and MsiRecordGetInteger. Finally, MsiCloseHandle is used to close any obtained handles. We're just doing raw interop here without more fancy SafeHandle stuff or so, but feel free to fine-tune the interop layer.

 

Respecting well-known patterns

Patterns deserve respect. Most of the time it are random designs that haven proven successful and received a career lifetime achievement award because of that, giving them the right to use the title "Pattern" (with capital P). I'd like to promote the design of data providers in .NET to this status. Essentially there are three players in the core pattern:

  • Connection - maintains the connection-oriented access to an underlying data store
  • Command - used to execute an arbitrary possibly parameterized command on the data store
  • DataReader - fetches results from a command in a sequential forward-only fashion

If we'd be really respectful, we'd go all the way implementing the System.Data.IDb* interfaces but since this is a sample only, we reserve ourselves the right to take some shortcuts and ignore concepts like transactions (although MsiOpenDatabase has such notions!). Obviously, the reader is free to extend this data provider at will to embrace those interfaces end-to-end providing meaningful implementations where applicable.

 

The connection

Nothing fancy here: just a call to MsiOpenDatabase, a way to obtain the handle for use in the command class and some clean-up using IDisposable and a finalizer. Ignore the fact I'm not subclass System.Exception for the purpose of this sample.

public class MsiConnection : IDisposable
{
     private string _fileName;
     private IntPtr _database;
     private bool _disposed;

     public MsiConnection(string fileName)
     {
          if (!File.Exists(fileName))
          {
               throw new FileNotFoundException(fileName + " not found.");
          }

          _fileName = fileName;
     }

     public void Open()
     {
          CheckDisposed();

          if (_database != IntPtr.Zero)
          {
               throw new InvalidOperationException("Database already opened.");
          }

          uint res = MsiInterop.MsiOpenDatabase(_fileName, 0, out _database);
          if (res != 0)
          {
               throw new Exception("Failed to open database. Error code = " + res);
          }
     }

     public void Close()
     {
          CheckDisposed();

          if (_database != IntPtr.Zero)
          {
               MsiInterop.MsiCloseHandle(_database);
               _database = IntPtr.Zero;
          }
     }

     internal IntPtr Handle
     {
          get { CheckDisposed(); return _database; }
     }

     public void Dispose()
     {
          Dispose(true);
          GC.SuppressFinalize(this);
     }

     private void Dispose(bool disposing)
     {
          if (!_disposed && _database != IntPtr.Zero)
          {
               MsiInterop.MsiCloseHandle(_database);
               _database = IntPtr.Zero;
               _disposed = true;
          }
     } 

     private void CheckDisposed()
     {
          if (_disposed)
               throw new ObjectDisposedException();
     }

     ~MsiConnection()
     {
          Dispose(false);
     }
}

 

The command

Just like the connection maps to the concept of an MSI connection, the command maps to the concept of an MSI view. Again we implement IDisposable but for the rest, everything should be self-expanatory:

public class MsiCommand : IDisposable
{
     private IntPtr _view;
     private string _command;
     private MsiConnection _conn;
     private bool _disposed;

     public MsiCommand(string command, MsiConnection conn)
     {
          _command = command;
          _conn = conn;
     }

     public MsiDataReader ExecuteReader()
     {
          CheckDisposed();

          if (_conn.Handle == IntPtr.Zero)
          {
               throw new Exception("Database connection not opened.");
          }

          uint res = MsiInterop.MsiDatabaseOpenView(_conn.Handle, _command, out _view);
          if (res != 0)
          {
               throw new Exception("Failed to create command. Error code = " + res);
          }

          return new MsiDataReader(this);
     }

     internal IntPtr Handle
     {
          get { CheckDisposed(); return _view; }
     }

     public void Dispose()
     {
          Dispose(true);
          GC.SuppressFinalize(this);
     }

     private void Dispose(bool disposing)
     {
          if (!_disposed && _view != IntPtr.Zero)
          {
               MsiInterop.MsiCloseHandle(_view);
               _view = IntPtr.Zero;
               _disposed = true;
          }
     } 

     private void CheckDisposed()
     {
          if (_disposed)
               throw new ObjectDisposedException();
     }

     ~MsiCommand()
     {
          Dispose(false);
     }
}

 

The data reader

Last but not least, the reader. Again IDisposable, but this time the core functionality is a little more involved. Just a little though. We'll execute the command "view" and fetch results which we keep around on a per-record basis for subsequent use in GetString and GetInteger to access the field values in the MSI table's current row.

public class MsiDataReader : IDisposable
{
     private MsiCommand _command;
     private IntPtr _record;
     private bool _disposed;

     internal MsiDataReader(MsiCommand command)
     {
          _command = command;
     }

     public bool Read()
     {
          CheckDisposed();

          if (_command.Handle == IntPtr.Zero)
          {
               throw new Exception("Command not ready.");
          }

          uint res;

          if (_record == IntPtr.Zero)
          {
               res = MsiInterop.MsiViewExecute(_command.Handle, IntPtr.Zero);
               if (res != 0)
               {
                    throw new Exception("Failed to run command. Error code = " + res);
               }
          }
          else
          {
               MsiInterop.MsiCloseHandle(_record);
          }

          res = MsiInterop.MsiViewFetch(_command.Handle, out _record);
          if (res == 259)
          {
               return false;
          }

          if (res != 0)
          {
               throw new Exception("Failed to get record. Error code = " + res);
          }

          return true;
     }

     public string GetString(uint column)
     {
          CheckDisposed();

          if (_record == IntPtr.Zero)
          {
               throw new Exception("No record fetched.");
          }

          column++;

          StringBuilder sb = new StringBuilder("", 1);
          int len = 0;

          uint res = MsiInterop.MsiRecordGetString(_record, column, sb, ref len);
          if (res == 234)
          {
               sb.Capacity = len++;
               res = MsiInterop.MsiRecordGetString(_record, column, sb, ref len);
          }

          if (res != 0)
          {
               throw new Exception("Failed to read string. Error code = " + res);
          }

          return sb.ToString();
     }

     public int GetInteger(uint column)
     {
          CheckDisposed();

          if (_record == IntPtr.Zero)
          {
               throw new Exception("No record fetched.");
          }

          column++;

          int res = MsiInterop.MsiRecordGetInteger(_record, column);

          return res;
     } 

     public void Dispose()
     {
          Dispose(true);
          GC.SuppressFinalize(this);
     }

     private void Dispose(bool disposing)
     {
          if (!_disposed && _record != IntPtr.Zero)
          {
               MsiInterop.MsiCloseHandle(_record);
               _record = IntPtr.Zero;
               _disposed = true;
          }
     } 

     private void CheckDisposed()
     {
          if (_disposed)
               throw new ObjectDisposedException();
     }

     ~MsiDataReader()
     {
          Dispose(false);
     }
}

That's it. I told you it wasn't too bad and now all the plumbing should be done, ready for us to build the LINQ implementation on top of this.

 

Sample

To finish up this post, let's take a look at a simple sample:

class Program
{
     static void Main()
     {
          using (var conn = new MsiConnection(@"C:\temp\PowerShell_Setup_x86.msi"))
          {
               using (var cmd = new MsiCommand("SELECT Property, Value FROM Property", conn))
               {
                    conn.Open();

                    using (var reader = cmd.ExecuteReader())
                    {
                         while (reader.Read())
                         {
                              Console.WriteLine("{0} = {1}", reader.GetString(0), reader.GetString(1));
                         }
                    }
               }
          } 
     }
}

and here's the output:

image

I'll post the code of the entire provider by the end of this series. Enjoy!

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

Filed under:

Comments

# re: LINQ to MSI - Part 1 - Interop

Saturday, June 14, 2008 3:25 AM by Steven

Bart,

Can you explain why to chose to implement finalizer methods on your classes instead of using a from SafeHandle inherited class to close the handle?

# LINQ to MSI - Part 1 - Interop

Saturday, June 14, 2008 5:05 AM by LINQ to MSI - Part 1 - Interop

Pingback from  LINQ to MSI - Part 1 - Interop

# re: LINQ to MSI - Part 1 - Interop

Saturday, June 14, 2008 5:32 AM by ShayEr

Why not use SafeHandle for the MSI connection?

# All Else Failed » LINQ to MSI - Part 1 - Interop

Saturday, June 14, 2008 2:58 PM by All Else Failed » LINQ to MSI - Part 1 - Interop

Pingback from  All Else Failed » LINQ to MSI - Part 1 - Interop

# All Else Failed » All Else Failed ?? LINQ to MSI - Part 1 - Interop

Pingback from  All Else Failed » All Else Failed ?? LINQ to MSI - Part 1 - Interop

# All Else Failed » All Else Failed ?? All Else Failed ?? LINQ to MSI - Part 1 - Interop

Pingback from  All Else Failed » All Else Failed ?? All Else Failed ?? LINQ to MSI - Part 1 - Interop

# LINQ to MSI

Friday, July 25, 2008 10:51 AM by InstallSite Blog

LINQ stands for Language-Integrated Query and enables you to directly query databases in .NET programming

# Bart De Smet on Channel9: LINQ-to-Anything

Thursday, October 16, 2008 10:32 AM by Katrien's MSDN Blog

Don’t miss the 100th episode of Going Deep on Channel9, this episode stars Bart De Smet. In case you