How to install MSI packages using msiexec and C#

MSI packages remain a popular means of distributing applications for installation on Windows devices.

On occasion, you may find the need to install or uninstall an MSI package programmatically, for example, whenever you need to automate a software installation or update.

In this article, I’m going to show you how you can silently install and uninstall applications by leveraging the msiexec command-line program using C#.

MSI background

Let’s start with a little bit of background on MSI packages.

MSI is an installer package file format for Windows. The name ‘MSI’ comes from the original name for the technology; Microsoft Installer. Microsoft has since changed the official name to Windows Installer.

Here are some abstracts from the Microsoft Docs on what Windows Installer is.

Microsoft Windows Installer is an installation and configuration service provided with Windows…

Windows Installer enables the efficient installation and configuration of your products and applications running on Windows. The installer provides new capabilities to advertise features without installing them, to install products on demand, and to add user customizations.

MSI packages are essentially database files that hold all of the binaries, dialogs and settings which are required to install a Windows program. As per the name, Windows Installer, it only works on Windows.

The Windows Installer page on the Microsoft Docs website provides very detailed documentation regarding how Windows Installer works and how you can make use of its many advanced features to accomplish your goals.

Working with MSI packages programmatically

Whenever you want to install MSI packages programmatically the most common approach is to interact with the msiexec program directly from your code by running it as a hidden process and specifying the appropriate set of arguments. This is the approach that we will take in the sections that follow.

I’ll be using C# to launch the msiexec program. The full source code shown in this article can be found within the accompanying GitHub repository.

Abstraction

The solution I am presenting centres around the abstraction of an MSI package into the MsiPackage class.

Below is the constructor for MsiPackage which features three separate parameters.

/// <summary>
/// Constructor.
/// </summary>
/// <param name="pathToInstallerFile">Path to the installer file</param>
/// <param name="pathToInstallLogFile">Path to the install log file (optional)</param>
/// <param name="pathToUninstallLogFile">Path to the uninstall log file (optional)</param>
public MsiPackage(
    string pathToInstallerFile,
    string pathToInstallLogFile   = null,
    string pathToUninstallLogFile = null)
{
    _pathToInstallerFile    = pathToInstallerFile;
    _pathToInstallLogFile   = pathToInstallLogFile;
    _pathToUninstallLogFile = pathToUninstallLogFile;
}

The MsiPackage constructor accepts pathToInstallerFile as the first argument. This represents the path to the MSI package which we are wrapping. The second and third parameters are optional and can be used to enable msiexec logging whenever the MSI package is installed or uninstalled.

Installation

Now let’s look at how to install a program by launching the msiexec program from our C# code.

We’ll create a method in the MsiPackage class that implements this behaviour.

Install method

Below is the code for the Install method which installs a program that is packaged within an MSI file via msiexec.

Note that I am using Serilog as the logging framework.

/// <summary>
/// Installs the program.
/// </summary>
/// <returns>True if the installation succeeded, otherwise false</returns>
public bool Install()
{
    try
    {
        Log.Information("Beginning MSI package installation");
 
        string arguments = $"/i \"{_pathToInstallerFile}\" /quiet";
 
        if (!string.IsNullOrEmpty(_pathToInstallLogFile))
        {
            // Create the install log directory if it doesn't already exist.
            string pathToInstallLogDirectory = Path.GetDirectoryName(_pathToInstallLogFile);
 
            if (!Directory.Exists(pathToInstallLogDirectory))
            {
                Directory.CreateDirectory(pathToInstallLogDirectory);
            }
 
            // /l = Logging enabled.
            //  * = Log everything.
            //  v = Verbose output.
            //  x = Extra debugging information.
            arguments += $" /l*vx \"{_pathToInstallLogFile}\"";
        }
 
        using (Process p = ProcessHelper.CreateHiddenProcess(WindowsInstallerProgramName, arguments))
        {
            Log.Information("Starting process: {0}", p.StartInfo.FileName);
            p.Start();
            p.WaitForExit();
 
            string installResultDescription = ((MsiExitCode)p.ExitCode).GetEnumDescription();
            Log.Information("MSI package install result: ({0}) {1}", p.ExitCode, installResultDescription);
 
            if (p.ExitCode != 0) throw new InstallerException(installResultDescription);
        }
 
        Log.Information("Installation completed");
    }
    catch (Exception ex)
    {
        Log.Error(ex, "An exception occurred.");
        throw;
    }
 
    return true;
}

The Install method starts off by building up the arguments which need to be passed to msiexec.

/i is the key argument that instructs msiexec to install a program.

The path to the installer file is the second argument and it is enclosed in double-quote characters, in case the file path contains spaces.

The /quiet argument instructs msiexec to install the program ‘quietly’ i.e. without displaying any user interface.

Logging

The next part of the code checks if an install log file path has been configured.

If a logging path has been specified, the following arguments are added by the code.

/l

This enables logging.

*

This means ‘log everything’.

v

This stands for ‘verbose’ logging.

x

This adds extra debugging information.

The last argument passed to msiexec is the path to the install log file.

For your purposes, you may not want such extensive logging. However, this example demonstrates how you can configure the full array of possible logging, in order to get as much information as possible when troubleshooting an installation issue.

Running the process

After configuring the arguments, the code creates a hidden process for the msiexec program.

Note that WindowsInstallerProgramName is constant within the MsiPackage class with a value of ‘msiexec’.

After creating the Process object, the code starts the process and then waits for it to exit.

Please see my Working with processes using C# blog article for more information on how hidden processes are created programmatically.

After the process exits, a description of the install result is logged.

Each value of the MsiExitCode enum is decorated with a Description attribute. The GetEnumDescription extension method is used to retrieve the description of the exit code which is returned by msiexec.

If the exit code is non-zero, an exception is thrown to indicate that the installation was unsuccessful.

Uninstallation

Uninstalling programs with msiexec is similar to installing programs, however, there is an important difference.

According to the msiexec documentation, it should be possible to uninstall a program via the original installer file. However, I’ve found this to not be the case, so an alternative approach is required.

Product Codes

In order to uninstall a program, we need to know the ‘Product Code’ of the MSI package. Once we have this, we can pass it to msiexec so that the correct program can be removed.

Below is a private method of the MsiPackage class that can be used to retrieve the Product Code.

/// <summary>
/// Gets the product code of the MSI package.
/// </summary>
/// <returns>The product code as a string, or an empty string if the installer file does not exist</returns>
private string GetProductCode()
{
    Log.Information("Getting product code from MSI package: {0}", _pathToInstallerFile);
    
    try
    {
        if (File.Exists(_pathToInstallerFile))
        {
            using (var db = new Database(_pathToInstallerFile, DatabaseOpenMode.ReadOnly))
            {
                // Note: The 'grave accents' instead of normal single quotes in the query string are essential.
                string productCode = (string)db.ExecuteScalar("SELECT `Value` FROM `Property` WHERE `Property` = 'ProductCode'");
 
                Log.Information("Retrieved product code '{0}' from MSI package: {1}", productCode, _pathToInstallerFile);
 
                return productCode;
            }
        }
        else
        {
            Log.Warning("An MSI package does not exist at the specified location, returning an empty string");
            return string.Empty;
        }
    }
    catch (Exception ex)
    {
        Log.Error(ex, "An exception occurred.");
        throw;
    }
}

The GetProductCode method, first of all, checks if a file exists at the specified file path.

If a file does not exist, a warning is logged and an empty string is returned.

If a file does exist, the MSI package is queried to retrieve the Product Code.

The Wix Toolset contains a set of libraries for creating and interacting with Windows Installer packages. As part of this, the Microsoft.Deployment.WindowsInstaller assembly provides a convenient way to open an MSI package using the Database class. In the code the ExecuteScalar method is used to execute a query that retrieves the ‘ProductCode’ property from the MSI file. The Product Code is then returned to the caller.

Uninstall method

Below is the code for the Uninstall method which uninstalls a program using msiexec.

/// <summary>
/// Uninstalls the program.
/// </summary>
/// <returns>True if the uninstallation succeeded, otherwise false</returns>
public bool Uninstall()
{
    bool uninstallResult = false;
 
    try
    {
        Log.Information("Beginning MSI package uninstallation");
 
        // We need to use the product code to uninstall the program.
        // Using the MSI package doesn't work, even though the documentation says that it should.
        string productCode = GetProductCode();
        string arguments   = $"/x {productCode} /quiet";
 
        if (!string.IsNullOrEmpty(_pathToUninstallLogFile))
        {
            // Create the uninstall log directory if it doesn't already exist.
            string pathToUninstallLogDirectory = Path.GetDirectoryName(_pathToUninstallLogFile);
 
            if (!Directory.Exists(pathToUninstallLogDirectory))
            {
                Directory.CreateDirectory(pathToUninstallLogDirectory);
            }
 
            // /l = Logging enabled
            //  * = Log everything
            //  v = Verbose output
            //  x = Extra debugging information
            arguments += $" /l*vx \"{_pathToUninstallLogFile}\"";
        }
 
        using (Process p = ProcessHelper.CreateHiddenProcess(WindowsInstallerProgramName, arguments))
        {
            Log.Information("Starting process '{0}'", p.StartInfo.FileName);
            p.Start();
            p.WaitForExit();
 
            string uninstallResultDescription = ((MsiExitCode)p.ExitCode).GetEnumDescription();
            Log.Information("MSI package uninstall result: ({0}) {1}", p.ExitCode, uninstallResultDescription);
 
            if (p.ExitCode != 0) throw new InstallerException(uninstallResultDescription);
        }
 
        Log.Information("Uninstallation completed");
    }
    catch (Exception ex)
    {
        Log.Error(ex, "An exception occurred.");
        throw;
    }
 
    return uninstallResult;
}

The Uninstall method starts off by getting the ‘Product Code’ from the MSI package using the GetProductCode method which we have just covered.

The code then builds up the arguments which need to be passed to msiexec in order to uninstall the program.

The /x argument instructs msiexec to uninstall the program.

The second argument is the Product Code of the program to uninstall.

As per the installation arguments, the /quiet argument hides the user interface.

The remainder of the Uninstall method is very similar to the Install method.

Logging is configured if an uninstall log file path has been specified. A separate log file path for uninstallation logs is used to avoid mixing uninstall logs with install logs.

A hidden process is then created and started. After the process exits the uninstall result is logged and the method returns. As per the Install method, an exception is thrown if the msiexec exit code is non-zero.

Demo

To finish up the code walkthrough below is a short demonstration of how to create an MsiPackage object and use it to install and uninstall a program.

// Configure the MSI package details.
string baseDirectory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
 
var msiPackage = new MsiPackage(
    pathToInstallerFile   : Path.Combine(baseDirectory, "Msi", "InstEd-1.5.15.26.msi"),
    pathToInstallLogFile  : Path.Combine(baseDirectory, "install-logs", "install.log"),
    pathToUninstallLogFile: Path.Combine(baseDirectory, "uninstall-logs", "unintall.log"));
 
// Install the program.
Log.Information("Installing program");
msiPackage.Install();
 
// Uninstall the program.
Log.Information("Uninstalling program");
msiPackage.Uninstall();

As you can see, it’s simply a matter of creating a new MsiPackage object instance and then calling the Install and Uninstall methods when and where required.

Parameters

We’ve covered a number of the available parameters/arguments for msiexec already in this article, however, there are many more.

If you open the ‘Run’ dialog (WIN + R) then type msiexec followed by the Enter/Return key, you’ll be shown a Windows Installer dialog which displays all of the possible parameters for the version of msiexec which is installed on your machine.

I have included the output from my machine below for reference.

Windows ® Installer. V 5.0.19041.1

msiexec /Option <Required Parameter> [Optional Parameter]

Install Options
</package | /i> <Product.msi>
Installs or configures a product
/a <Product.msi>
Administrative install - Installs a product on the network
/j<u|m> <Product.msi> [/t <Transform List>] [/g <Language ID>]
Advertises a product - m to all users, u to current user
</uninstall | /x> <Product.msi | ProductCode>
Uninstalls the product
Display Options
/quiet
Quiet mode, no user interaction
/passive
Unattended mode - progress bar only
/q[n|b|r|f]
Sets user interface level
n - No UI
b - Basic UI
r - Reduced UI
f - Full UI (default)
/help
Help information
Restart Options
/norestart
Do not restart after the installation is complete
/promptrestart
Prompts the user for restart if necessary
/forcerestart
Always restart the computer after installation
Logging Options
/l[i|w|e|a|r|u|c|m|o|p|v|x|+|!|*] <LogFile>
i - Status messages
w - Nonfatal warnings
e - All error messages
a - Start-up of actions
r - Action-specific records
u - User requests
c - Initial UI parameters
m - Out-of-memory or fatal exit information
o - Out-of-disk-space messages
p - Terminal properties
v - Verbose output
x - Extra debugging information
+ - Append to existing log file
! - Flush each line to the log
* - Log all information, except for v and x options
/log <LogFile>
Equivalent of /l* <LogFile>
Update Options
/update <Update1.msp>[;Update2.msp]
Applies update(s)
/uninstall <PatchCodeGuid>[;Update2.msp] /package <Product.msi | ProductCode>
Remove update(s) for a product
Repair Options
/f[p|e|c|m|s|o|d|a|u|v] <Product.msi | ProductCode>
Repairs a product
p - only if file is missing
o - if file is missing or an older version is installed (default)
e - if file is missing or an equal or older version is installed
d - if file is missing or a different version is installed
c - if file is missing or checksum does not match the calculated value
a - forces all files to be reinstalled
u - all required user-specific registry entries (default)
m - all required computer-specific registry entries (default)
s - all existing shortcuts (default)
v - runs from source and recaches local package
Setting Public Properties
[PROPERTY=PropertyValue]

Consult the Windows ® Installer SDK for additional documentation on the
command line syntax.

Copyright © Microsoft Corporation. All rights reserved.
Portions of this software are based in part on the work of the Independent JPEG Group.

As you can see there are a lot of customisation options that can be useful for other scenarios.

I encourage you to try experimenting with msiexec from your terminal; just be careful not to uninstall something you didn’t intend to 🙂

Other tools

If you need to work with MSI files there are some tools that can help you.

Installer Projects

If you want to create your own MSI packages and you are using Visual Studio, check out the Microsoft Visual Studio Installer Projects extension which allows you to create setup projects.

After installing the extension you’ll have a new ‘Setup Project’ option within the list of possible projects you can create within Visual Studio.

After creating a Setup Project you can add the files you want to include in the project, configure the setup file properties, add custom actions and more.

Note that you can also use the Wix Toolset as an alternative approach for creating setup projects.

InstEd

If you are interested in exploring the internals of MSI files further, I recommend checking out InstEd.

IntEd is a very nice little tool that can be used to view and edit MSI files.

You can view package meta-data and edit almost any aspect of an MSI file.

InstEd user interface
InstEd user interface

If you are looking for an easy visual way to obtain details such as the Product Code of an MSI file, InSted can be very helpful in this regard.

Wrapping up

In this article, we have seen how we can leverage msiexec to install and uninstall programs from C#.

I’ve demonstrated how to retrieve properties such as ‘Product Code’ from an MSI file programmatically, and how we can configure extensive msiexec logging for troubleshooting purposes.

We’ve also had a quick look at some of the tools which can help us when working with MSI packages.

I hope you found this article helpful and don’t forget to check out the accompanying GitHub repository.


I hope you enjoyed this post! Comments are always welcome and I respond to all questions.

If you like my content and it helped you out, please check out the button below 🙂

Comments

Miles

So I am trying to install a network .msi file, however I keep getting errors when I run it. I was looking at your code to figure out where I went wrong, but I don’t even see where you called msiexec. The other stuff I understand but the install just makes no sense to me. Can you please explain a bit more?

February 28, 2022

Jonathan Crozier

Hi Miles,

I recommend looking at the source code for the accompanying GitHub repository.

The Install method within the MsiPackage class is where msiexec is called. The WindowsInstallerProgramName constant value is “msiexec”. This value is passed into the static CreateHiddenProcess method of the ProcessHelper class. The msiexec process is then kicked off by calling the Start method on the Process object instance.

I hope that helps!

March 7, 2022