15

I am using the C# wrapper for the Windows Installer API from the WIX Toolset. I use the ProductInstallation class to get information about the installed products such as the product code and product name.

For example

  • Product Name - "My Test Application"
  • Product Code - {F46BA620-C027-4E68-9069-5D5D4E1FF30A}
  • Product Version - 1.4.0

Internally this wrapper uses the MsiGetProductInfo function. Unfortunately this function does not return the product's upgrade code.

How can I retrieve the upgrade code for an installed application using C#?

Alex Wiese
  • 8,142
  • 6
  • 42
  • 71
  • 1
    Rather than using the registry as suggested below, **I propose that you use WMI as described in this answer**: [**How can I find the Upgrade Code for an installed MSI file?**](https://stackoverflow.com/questions/46637094/how-can-i-find-the-upgrade-code-for-an-installed-msi-file/46637095#46637095). This will ensure that you retrieve the correct upgrade code, and it will not require any conversion or interpretation. **You will get the real upgrade code back in its proper format**. – Stein Åsmul Oct 09 '17 at 00:07

6 Answers6

40

I have discovered the upgrade codes are stored in the following registry location.

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UpgradeCodes

The registry key name is the upgrade code and the registry key value name is the product code. I can easily extract these values however the codes are stored in a different format. The red circle shows the formatted upgrade code, the blue circle shows the formatted product code when viewing it in regedit.exe.

Red circle is the formatted upgrade code, the blue circle the formatted product code

The hyphens are stripped out of the Guid and then a series of string reversals are done. The first 8 characters are reversed, then the next 4, then the following 4 and then the rest of the string is reversed in sets of 2 characters. Normally when reversing a string we need to take care in making sure control and special characters are handled correctly (see Jon Skeet's aricle here) but as we are, in this case, dealing with a Guid string we can be confident the string will be reversed correctly.

Below is the complete code I used to extract the upgrade code for a known product code from the registry.

internal static class RegistryHelper
{
    private const string UpgradeCodeRegistryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UpgradeCodes";

    private static readonly int[] GuidRegistryFormatPattern = new[] { 8, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2 };

    public static Guid? GetUpgradeCode(Guid productCode)
    {
        // Convert the product code to the format found in the registry
        var productCodeSearchString = ConvertToRegistryFormat(productCode);

        // Open the upgrade code registry key
        var localMachine = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
        var upgradeCodeRegistryRoot = localMachine.OpenSubKey(UpgradeCodeRegistryKey);

        if (upgradeCodeRegistryRoot == null)
            return null;

        // Iterate over each sub-key
        foreach (var subKeyName in upgradeCodeRegistryRoot.GetSubKeyNames())
        {
            var subkey = upgradeCodeRegistryRoot.OpenSubKey(subKeyName);

            if (subkey == null)
                continue;

            // Check for a value containing the product code
            if (subkey.GetValueNames().Any(s => s.IndexOf(productCodeSearchString, StringComparison.OrdinalIgnoreCase) >= 0))
            {
                // Extract the name of the subkey from the qualified name
                var formattedUpgradeCode = subkey.Name.Split('\\').LastOrDefault();

                // Convert it back to a Guid
                return ConvertFromRegistryFormat(formattedUpgradeCode);
            }
        }

        return null;
    }

    private static string ConvertToRegistryFormat(Guid productCode)
    {
        return Reverse(productCode, GuidRegistryFormatPattern);
    }

    private static Guid ConvertFromRegistryFormat(string upgradeCode)
    {
        if (upgradeCode == null || upgradeCode.Length != 32)
            throw new FormatException("Product code was in an invalid format");

        upgradeCode = Reverse(upgradeCode, GuidRegistryFormatPattern);

        return Guid.Parse(upgradeCode);
    }

    private static string Reverse(object value, params int[] pattern)
    {
        // Strip the hyphens
        var inputString = value.ToString().Replace("-", "");

        var returnString = new StringBuilder();

        var index = 0;

        // Iterate over the reversal pattern
        foreach (var length in pattern)
        {
            // Reverse the sub-string and append it
            returnString.Append(inputString.Substring(index, length).Reverse().ToArray());

            // Increment our posistion in the string
            index += length;
        }

        return returnString.ToString();
    }
}
Alex Wiese
  • 8,142
  • 6
  • 42
  • 71
  • Alex above program is returning NULL for corresponding product code. Does every upgrade code makes its entry in it with product code? I have removed backets from my product code but not hyphens(-). Still it isn't working. – Keshav Jul 22 '15 at 06:06
  • After Debugging I found out registry is present on given location but upgradeCodeRegistryRoot is getting NULL value – Keshav Jul 22 '15 at 06:23
  • "The registry key name is the upgrade code" it's wrong! The registry keys are not equal to upgrade codes! – Amir Mahdi Nassiri Aug 09 '17 at 12:16
  • @AmirMahdiNassiri it is for me on Windows 10 x64. Did you read the part where I say it is in a different format and you need to convert it? When I refer to the registry key name I mean the actual key name directly under the UpgradeCodes key (in regedit it appears as a folder). Not the registry value name within that key (which is the product code). – Alex Wiese Aug 09 '17 at 23:23
  • @Keshav there was an issue if you ran this code in a 32-bit process on a 64-bit operating system. Please see my updated code that I have tested now works correctly on 64-bit Windows 10. – Alex Wiese Aug 09 '17 at 23:32
  • @AlexWiese Yeah I did convert the product code, and found my program, thank you for that :) But the problem is that the Registry Key (The folder name) is not the corresponding upgrade code! I Found the correct upgrade code using this method: [StackAnswer](https://stackoverflow.com/questions/5063129/how-to-find-the-upgrade-code-productcode-of-an-installed-application-in-win-7) I did repair my program and read the log to get the upgrade code, and suddenly my assumption was correct and the registry keys are NOT upgrade codes! Maybe they are formatted too! Yeah I'm using windows 10 x64 too. – Amir Mahdi Nassiri Aug 12 '17 at 05:09
7

This is the opposite method to get the ProductCode from an UpgradeCode. Could be useful for somebody.

using Microsoft.Win32;
using System;
using System.IO;
using System.Linq;
using System.Text;

internal static class RegistryHelper
{
    private const string UpgradeCodeRegistryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UpgradeCodes";

    private static readonly int[] GuidRegistryFormatPattern = new[] { 8, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2 };


    public static Guid? GetProductCode(Guid upgradeCode)
    {
        // Convert the product code to the format found in the registry
        var productCodeSearchString = ConvertToRegistryFormat(upgradeCode);

        // Open the upgrade code registry key
        var upgradeCodeRegistryRoot = GetRegistryKey(Path.Combine(UpgradeCodeRegistryKey, productCodeSearchString));

        if (upgradeCodeRegistryRoot == null)
            return null;

        var uninstallCode = upgradeCodeRegistryRoot.GetValueNames().FirstOrDefault();
        if (string.IsNullOrEmpty(uninstallCode))
        {
            return null;
        }

        // Convert it back to a Guid
        return ConvertFromRegistryFormat(uninstallCode);
    }





    private static string ConvertToRegistryFormat(Guid code)
    {
        return Reverse(code, GuidRegistryFormatPattern);
    }

    private static Guid ConvertFromRegistryFormat(string code)
    {
        if (code == null || code.Length != 32)
            throw new FormatException("Product code was in an invalid format");

        code = Reverse(code, GuidRegistryFormatPattern);

        return Guid.Parse(code);
    }

    private static string Reverse(object value, params int[] pattern)
    {
        // Strip the hyphens
        var inputString = value.ToString().Replace("-", "");

        var returnString = new StringBuilder();

        var index = 0;

        // Iterate over the reversal pattern
        foreach (var length in pattern)
        {
            // Reverse the sub-string and append it
            returnString.Append(inputString.Substring(index, length).Reverse().ToArray());

            // Increment our posistion in the string
            index += length;
        }

        return returnString.ToString();
    }

    static RegistryKey GetRegistryKey(string registryPath)
    {
        var hklm64 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
        var registryKey64 = hklm64.OpenSubKey(registryPath);
        if (((bool?)registryKey64?.GetValueNames()?.Any()).GetValueOrDefault())
        {
            return registryKey64;
        }

        var hklm32 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
        return hklm32.OpenSubKey(registryPath);
    }
}
Luis Sagasta
  • 101
  • 1
  • 6
  • 2
    @Alex Wiese code works fine for me. This code does not. It does, however, perform the opposite lookup by finding the ProductCode for a given UpgradeCode. – harlam357 Jul 17 '17 at 19:35
  • @harlam357 thank you for your comment. You are right. I have updated the answer to complement the accepted answer. – Luis Sagasta Oct 23 '17 at 11:21
5

The InstallPackage class has a property called LocalPackage. You can use this to query the MSI database that's cached in C:\Windows\Installer and get anything you could possibly want to know about it.

Christopher Painter
  • 54,556
  • 6
  • 63
  • 100
  • Yes that is correct but it is not as reliable. It is only helpful so long as the MSI package is still there. During a quick test on a few PCs I have found it is quite common for the MSI to not exist (27 missing from 593 packages tested or ~5%). In these tests all of the upgrade codes were still available from the registry. – Alex Wiese Jul 30 '13 at 03:11
  • 1
    Those MSI's should always be there otherwise you'll have problems with repair, uninstall, advertised shortcuts and so on. http://blogs.msdn.com/b/sqlserverfaq/archive/2013/04/30/do-not-delete-files-from-the-windows-installer-folder.aspx – Christopher Painter Jul 30 '13 at 12:24
  • I agree; they _should_ be there. Unfortunately sometimes they aren't. I should elaborate that this is for an inventory style service that will be installed on client PCs so I needed a more reliable method. – Alex Wiese Jul 30 '13 at 12:32
  • 1
    As an inventory tool, I imagine that's something you'll want to capture. Also, FWIW, UpgradeCode is not a required property for an MSI. Strongly recommended but not required. – Christopher Painter Jul 30 '13 at 13:07
  • 2
    Btw, if you want most reliability, you'd use *BOTH* of these approaches and take which ever one gives you the answer in that instance. If someone is dumb enough to delete an MSI file they can be dumb enough to delete a registry key also. – Christopher Painter Jul 30 '13 at 15:08
3

Just commenting in case this comes in handy in the future for anyone!

In the case that you only have the GUID or code available, the following site can be used to convert between the two:

https://daysoff.io/flipguid

Hopefully this can save some future headaches!

Jack Casey
  • 1,628
  • 11
  • 18
1

Here is a much more simple way to get the GUID formatted in registry format (which basically is just a raw byte representation)

First is to get raw bytes:

var guidBytes = Guid.Parse(productCode).ToByteArray();

Then just flipp the endianness of BitConverter.ToString() result

var convertedString = String.Concat(BitConverter.ToString(guidBytes).Split('-').SelectMany(s => s.Reverse()));
bnone
  • 11
  • 4
0

And here is your helper modified in such a way that it also works in .Net3.5 32 bit applications. They need special treatement because .net 3.5 has no awareness about registry being splitted between 32 and 64 bit entries. My solution is only using To64BitPath to browse 64 bit part of it. There is also a great tutorial that uses DllImports for that: https://www.rhyous.com/2011/01/24/how-read-the-64-bit-registry-from-a-32-bit-application-or-vice-versa/

class RegistryHelper
{
    private const string UpgradeCodeRegistryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UpgradeCodes";
    private const string UninstallRegistryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";

    private static readonly int[] GuidRegistryFormatPattern = new[] { 8, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2 };



    public static string To64BitPath(string path)
    { 
        return path.Replace("SOFTWARE\\Microsoft", "SOFTWARE\\WOW6432Node\\Microsoft");
    }

    private static RegistryKey GetLocalMachineRegistryKey(string path)
    {
        return RegistryKey.OpenRemoteBaseKey(RegistryHive.LocalMachine, string.Empty).OpenSubKey(path);
    }

    public static IEnumerable<Guid> GetUpgradeCodes()
    {
        var list = new List<Guid>();

        var key = GetRegistryKey(UpgradeCodeRegistryKey);
        if (key != null)
        {
            list.AddRange(key.GetSubKeyNames().Select(ConvertFromRegistryFormat));
        }

        return list;
    }

    public static Guid? GetProductCode(Guid upgradeCode)
    {
        // Convert the product upgradeCode to the format found in the registry
        var productCodeSearchString = ConvertToRegistryFormat(upgradeCode);

        // Open the upgradeCode upgradeCode registry key
        var upgradeCodeRegistryRoot = GetRegistryKey(Path.Combine(UpgradeCodeRegistryKey, productCodeSearchString));

        if (upgradeCodeRegistryRoot == null)
            return null;

        var uninstallCode = upgradeCodeRegistryRoot.GetValueNames().FirstOrDefault();
        if (string.IsNullOrEmpty(uninstallCode))
        {
            return null;
        }

        // Convert it back to a Guid
        return ConvertFromRegistryFormat(uninstallCode);
    }

    public static string ConvertToRegistryFormat(Guid code)
    {
        return Reverse(code, GuidRegistryFormatPattern);
    }

    private static Guid ConvertFromRegistryFormat(string code)
    {
        if (code == null || code.Length != 32)
            throw new FormatException("Product upgradeCode was in an invalid format");

        code = Reverse(code, GuidRegistryFormatPattern);

        return new Guid(code);
    }

    private static string Reverse(object value, params int[] pattern)
    {
        // Strip the hyphens
        var inputString = value.ToString().Replace("-", "");

        var returnString = new StringBuilder();

        var index = 0;

        // Iterate over the reversal pattern
        foreach (var length in pattern)
        {
            // Reverse the sub-string and append it
            returnString.Append(inputString.Substring(index, length).Reverse().ToArray());

            // Increment our posistion in the string
            index += length;
        }

        return returnString.ToString();
    }

    static RegistryKey GetRegistryKey(string registryPath)
    {
        var registryKey64 = GetLocalMachineRegistryKey(To64BitPath(registryPath));
        if (((bool?)registryKey64?.GetValueNames()?.Any()).GetValueOrDefault())
        {
            return registryKey64;
        }

        return GetLocalMachineRegistryKey(registryPath);
    }


    public static Guid? GetUpgradeCode(Guid productCode)
    {
        var productCodeSearchString = ConvertToRegistryFormat(productCode);
        var upgradeCodeRegistryRoot = GetRegistryKey(UpgradeCodeRegistryKey);

        if (upgradeCodeRegistryRoot == null)
        {
            return null;
        }

        // Iterate over each sub-key
        foreach (var subKeyName in upgradeCodeRegistryRoot.GetSubKeyNames())
        {
            var subkey = upgradeCodeRegistryRoot.OpenSubKey(subKeyName);

            if (subkey == null)
                continue;

            // Check for a value containing the product upgradeCode
            if (subkey.GetValueNames().Any(s => s.IndexOf(productCodeSearchString, StringComparison.OrdinalIgnoreCase) >= 0))
            {
                // Extract the name of the subkey from the qualified name
                var formattedUpgradeCode = subkey.Name.Split('\\').LastOrDefault();

                // Convert it back to a Guid
                return ConvertFromRegistryFormat(formattedUpgradeCode);
            }
        }

        return null;
    }
}
Paweł Zarzycki
  • 306
  • 3
  • 7