4

How do I quickly obtain the ProductVersion of an msi database through pinvoke using the msi.dll? Mostly what I found involved utilizing the WindowsInstaller COM wrapper, while this got the job done, I want to achieve the same results through pinvoke using the msi.dll.

Cœur
  • 37,241
  • 25
  • 195
  • 267
user529570
  • 139
  • 1
  • 7
  • 2
    @user529570 Welcome to Stack Overflow - it doesn't matter that you have already solved this, it's still useful. You might want edit it (use the 'edit' link below the question) to phrase this as a question and put your solution(s) in the form of an answer. This allows people to offer alternative answers and vote on your answer. – Colin Pickard Dec 03 '10 at 16:08
  • Usefull. Codeproject seems to be the place to post things like this. A site where people could post solutions would be very usefull. – Nick Dec 03 '10 at 16:09
  • As indicated in one of the answers, you should use **DTF** for this when coding in C# - it does all the pinvoke stuff for you once and for all. [**Here is a sample of DTF used to deal with MSI**](https://stackoverflow.com/a/1061606/129130). DTF is installed with the [WiX toolset](https://wixtoolset.org/). Here are some [quick start tips for WiX](https://stackoverflow.com/a/25005864/129130). Most important dll: **`Microsoft.Deployment.WindowsInstaller.dll`**. Find it. Add a reference. Go time. Done. – Stein Åsmul Nov 16 '19 at 11:13

3 Answers3

9

Here's what I've come up with.

C# Windows Installer COM library:

            // Get the type of the Windows Installer object 
            Type installerType = Type.GetTypeFromProgID("WindowsInstaller.Installer");

            // Create the Windows Installer object 
            Installer installer = (Installer)Activator.CreateInstance(installerType);

            // Open the MSI database in the input file 
            Database database = installer.OpenDatabase(od.FileName, MsiOpenDatabaseMode.msiOpenDatabaseModeReadOnly);

            // Open a view on the Property table for the version property 
            WindowsInstaller.View view = database.OpenView("SELECT * FROM Property WHERE Property = 'ProductVersion'");

            // Execute the view query 
            view.Execute(null);

            // Get the record from the view 
            Record record = view.Fetch();

            // Get the version from the data 
            string version = record.get_StringData(2); 

C# Pinvoke:

    [DllImport("msi.dll", SetLastError = true)]
    static extern uint MsiOpenDatabase(string szDatabasePath, IntPtr phPersist, out IntPtr phDatabase);

    [DllImport("msi.dll", CharSet = CharSet.Unicode)]
    static extern int MsiDatabaseOpenViewW(IntPtr hDatabase, [MarshalAs(UnmanagedType.LPWStr)] string szQuery, out IntPtr phView);

    [DllImport("msi.dll", CharSet = CharSet.Unicode)]
    static extern int MsiViewExecute(IntPtr hView, IntPtr hRecord);

    [DllImport("msi.dll", CharSet = CharSet.Unicode)]
    static extern uint MsiViewFetch(IntPtr hView, out IntPtr hRecord);

    [DllImport("msi.dll", CharSet = CharSet.Unicode)]
    static extern int MsiRecordGetString(IntPtr hRecord, int iField,
       [Out] StringBuilder szValueBuf, ref int pcchValueBuf);

    [DllImport("msi.dll", ExactSpelling = true)]
    static extern IntPtr MsiCreateRecord(uint cParams);

    [DllImport("msi.dll", ExactSpelling = true)]
    static extern uint MsiCloseHandle(IntPtr hAny);

    public string GetVersionInfo(string fileName)
    {
        string sqlStatement = "SELECT * FROM Property WHERE Property = 'ProductVersion'";
        IntPtr phDatabase = IntPtr.Zero;
        IntPtr phView = IntPtr.Zero;
        IntPtr hRecord = IntPtr.Zero;

        StringBuilder szValueBuf = new StringBuilder();
        int pcchValueBuf = 255;

        // Open the MSI database in the input file 
        uint val = MsiOpenDatabase(fileName, IntPtr.Zero, out phDatabase);

        hRecord = MsiCreateRecord(1);

        // Open a view on the Property table for the version property 
        int viewVal = MsiDatabaseOpenViewW(phDatabase, sqlStatement, out phView);

        // Execute the view query 
        int exeVal = MsiViewExecute(phView, hRecord);

        // Get the record from the view 
        uint fetchVal = MsiViewFetch(phView, out hRecord);

        // Get the version from the data 
        int retVal = MsiRecordGetString(hRecord, 2, szValueBuf, ref pcchValueBuf);

        uRetCode = MsiCloseHandle(phDatabase);
        uRetCode = MsiCloseHandle(phView);
        uRetCode = MsiCloseHandle(hRecord);

        return szValueBuf.ToString();
    }

This could easily be extrapolated into obtaining any property or field from the msi database by changing the SQL statement. I hope this helps someone out.

user529570
  • 139
  • 1
  • 7
  • 2
    This P/Invoke code has two main flaws (which are unlikely to bite you until you forgot writing this code): MSIHANDLEs are Int32 not IntPtr (yes, this is unlike most HANDLE types, and relevant only on x64 environments), and your property fetch code can't handle property values longer than 255. – Michael Urman Dec 06 '10 at 14:02
  • I didn't realize that about the MSIHANDLEs, thanks! That would have been a huge rabbit hole to go down to find that bug. The property values length can be easily changed. I had just set it to 255 arbitrarily. – user529570 Dec 07 '10 at 20:58
  • Error 19 Cannot convert to static type 'Microsoft.Deployment.WindowsInstaller.Installer' – Jader Dias Jul 05 '13 at 19:45
6

Anyone needing to do .NET interop with MSI should be using Microsoft.Deployment.WindowsInstaller found in WiX's DTF SDK. It's a very clean library and way better then trying to write your own.

Christopher Painter
  • 54,556
  • 6
  • 63
  • 100
  • Is there any way to embed the interop library into the main executeable? I was looking for a way to interop without having to tag along an additional external DLL to my app. – user529570 Dec 07 '10 at 20:55
  • 1
    BTW, if you really want to consume 1 or 2 functions in the API and have a lightweight .CS file to do it, I suggest you google for MsiInterop.cs and then strip it down to what you really need. Better then using COM IMO. For general purpose MSI Interop I still stand by that WiX DTF is the best solution. – Christopher Painter Dec 08 '10 at 18:11
  • 1
    +1. WiX SDK generally results in about two thirds less code for me compared to the other options in the accepted answer. – Tharwen May 29 '12 at 07:48
  • Except that nowadays Wix is MS-RL so it is not suitable for some commercial software – zaitsman Jan 05 '16 at 05:01
  • @zaitsman I'm not a lawyer, and neither is Rob Mensching, but according to what he says here http://windows-installer-xml-wix-toolset.687559.n2.nabble.com/Outcurve-License-question-td7581298.html you don't have to do anything special if you use DTF unmodified. – RenniePet Aug 12 '17 at 22:24
0

I was unable to get @user529570's answer working for me, this however worked in C#

    using System;
    using System.Runtime.InteropServices;
    using System.Text;

    public static class Msi
    {
        public static string GetProductVersion(string fileName)
        {
            IntPtr hInstall = IntPtr.Zero;
            try
            {
                uint num = MsiOpenPackage(fileName, ref hInstall);
                if ((ulong)num != 0)
                {
                    throw new Exception("Cannot open database: " + num);
                }
                int pcchValueBuf = 255;
                StringBuilder szValueBuf = new StringBuilder(pcchValueBuf);
                num = MsiGetProperty(hInstall, "ProductVersion", szValueBuf, ref pcchValueBuf);
                if ((ulong)num != 0)
                {
                    throw new Exception("Failed to Get Property ProductVersion: " + num);
                }
                return szValueBuf.ToString();
            }
            finally
            {
                if(hInstall != IntPtr.Zero)
                {
                    MsiCloseHandle(hInstall);
                }
            }
        }

        [DllImport("msi.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
        private static extern int MsiCloseHandle(IntPtr hAny);

        [DllImport("msi.dll", CharSet = CharSet.Unicode, EntryPoint = "MsiOpenPackageW", ExactSpelling = true, SetLastError = true)]
        private static extern uint MsiOpenPackage(string szDatabasePath, ref IntPtr hProduct);

        [DllImport("msi.dll", CharSet = CharSet.Unicode, EntryPoint = "MsiGetPropertyW", ExactSpelling = true, SetLastError = true)]
        private static extern uint MsiGetProperty(IntPtr hInstall, string szName, [Out] StringBuilder szValueBuf, ref int pchValueBuf);
    }
aolszowka
  • 1,300
  • 12
  • 36
  • This works, but please note that opening a large file will take a long time as you are opening the complete package and not just the database. – Mekroebo Mar 15 '23 at 15:20