Creating WiX Custom Actions in C# and Passing Parameters

I’m currently working on a project where I’m creating a custom HTTP module for a customer.  The module is to be used with an existing commercial product.  Since the custom HTTP module is working with an existing product, the process of installing it would need to make significant changes to the web.config file of the existing product, plus be able to remove those changes during a subsequent uninstall.

I hadn’t worked with Windows Installer XML (WiX) projects before so this was a perfect opportunity to get up to speed. 

Creating a Setup Project and C# Custom Action Project

Once you install the WiX toolset you’ll be able to create a new Setup Project from Visual Studio as shown below.


After creating the setup project you’ll have a Product.wxs file that defines your setup.  I won’t go into details about the general process of authoring this file as this is covered in detail in the online WiX tutorial.  Once we’ve created our setup project, we’ll need to add a C# Custom Action Project to our solution (you can see this project template option in the above screenshot as well).  Within the custom action project you’ll have a new CustomActions class that looks like the following:

  1: public class CustomActions
  2: {
  3:     [CustomAction]
  4:     public static ActionResult CustomAction1(Session session)
  5:     {
  6:         session.Log("Begin CustomAction1");
  8:         return ActionResult.Success;
  9:     }
  10: }

This is where you’ll write the code that you want to be executed when the custom action method is called.  Any method that you want to expose to the setup project must be a public static method that is annotated with the CustomAction attribute and has the following method signature.

  1: public static ActionResult ActionMethod(Session session)

The Session parameter passed to the method is the object that controls the installation process.  Among other things, the Session object allows you to write output to the installation log file and gives you access to global properties for passing data (we’ll use this later).

So in my particular instance I needed to write a custom action that would perform various updates to the existing product’s web.config file as part of the install.  WiX provides what’s referred to as a Standard Custom Action library for manipulating XML files as part of an installation.  However, after reviewing the on-line content and some examples this proved to be far too cumbersome for my needs.

So my custom action method ultimately looked similar to the following:

  1: [CustomAction]
  2: public static ActionResult ConfigurEwsFilter(Session session)
  3: {
  4:     try
  5:     {
  6:         session.Log("Begin Configure EWS Filter Custom Action");
  8:         // TODO: Make changes to config file
  10:         session.Log("End Configure EWS Filter Custom Action");
  11:     }
  12:     catch (Exception ex)
  13:     {
  14:         session.Log("ERROR in custom action ConfigureEwsFilter {0}", 
  15:                     ex.ToString());
  16:         return ActionResult.Failure;
  17:     }
  19:     return ActionResult.Success;
  20: }

The code snippet shows what could be argued as a standard boilerplate approach for custom actions.  Logging the entry and exit of the custom action, performing the configuration step necessary and handling an unhandled exceptions to make sure they are logged to the output correctly.

Compiling the custom action project will generate two DLLs.  The additional DLL, $(TargetName).CA.dll, provides a format that is callable from the MSI engine.  Therefore, this is the DLL that we want to reference from the *.wxs file in the setup project.  To do that, we add a Binary element within the Product element that was generated in the *.wxs when the setup project was created.

  1: <Product Id="*" Name="SetupProject1" 
  2:          Language="1033" Version="" 
  3:          Manufacturer="" 
  4:          UpgradeCode="d206ba77-ddbe-41a3-893f-324f55242bef">
  6:   <Binary Id="EwsAction.CA.dll"
  7:           src="..\EwsAction\bin\$(var.Configuration)\EwsAction.CA.dll" />
  9: </Product>

In the snippet above, I’m referencing the *.CA.dll relative to the setup project (which is in the same solution).  I’m also indicating that I want to base the version of the *.CA.dll on the current build configuration of the setup project using the $(var.Configuration) syntax.  That way, if I build a debug version of the setup project I get a debug version of the custom action and a similar result for a release build.

Along with the Binary element, we’ll also need to add a CustomAction element to reference the Binary element’s Id and indicate the entry point that we want to execute.  This should look similar to the following:

  1: <CustomAction Id="ConfigurEwsFilter" 
  2:               Return="check" 
  3:               Execute="immediate" 
  4:               BinaryKey="EwsAction.CA.dll" 
  5:               DllEntry="ConfigurEwsFilter" />

In the above example, notice that the BinaryKey attribute matches the Id attribute of the Binary element we previously added.  Also note that the DllEntry attribute references the method name that was decorated with the CustomAction attribute in the C# code.

Adding the Binary and CustomAction elements by themselves however still does not result in the custom action actually being executed.  To include the custom action in the installation process we need to add an entry to the InstallExecuteSequence as shown.

  1: <InstallExecuteSequence>
  2:   <Custom Action="ConfigurEwsFilter" Before="InstallFinalize"  />
  3: </InstallExecuteSequence>

Note here that we’re referencing the Id attribute of the CustomAction element while indicating that we want this action to execute prior to the standard InstallFinalize action of the installation.

Passing Parameters

The last issue we need to resolve is retrieving the location of the existing product’s web.config file and passing it to the custom action.  In my case I can use a standard RegistrySearch action to read the install path of the existing product from the registry.  The web.config should exist within a well known folder path within the install path.  Since the RegistrySearch action will store the root install path to a property, I need a separate CustomAction to build out the full path to the web.config file.  As a consequence, I’ll also need another entry in the InstallExecuteSequence to execute this new custom action.  These 3 additions are shown below.

  2:   <RegistrySearch Id='ProductInstallDir' 
  3:                   Type='raw' 
  4:                   Root='HKLM' 
  5:                   Key='SOFTWARE\Manufacturer\ExistingProduct\Setup' 
  6:                   Name='MsiInstallPath' 
  7:                   Win64='yes'/>
  8: </Property>
  9: <CustomAction Id='AssignConfigFile' 
  10:               Property='CONFIGFILE' 
  11:               Value='[PRODUCTINSTALLFOLDER]pathtoconfigfile\web.config' />
  12: <InstallExecuteSequence>
  13:   <Custom Action="AssignConfigFile" After="CostFinalize" />
  14:   <Custom Action="ConfigurEwsFilter" Before="InstallFinalize"  />
  15: </InstallExecuteSequence>

The result of the above is that the web.config file’s path should now be in the global property named CONFIGFILE.  With the Session object discussed earlier, I can now access this path from within my custom action code using syntax similar to the following:

  1: session.Log("Session value for CONFIGFILE = '{0}'", 
  2:             session["CONFIGFILE"]);

Now within my custom action method I can open the web.config file and make the necessary edits for my custom HTTP module.

Since the web.config edits are performed by a custom action, they won’t automatically be removed during an uninstall.  In my next blog post I’ll discuss creating a separate custom action to remove the web.config edits and have it run only during the uninstall of the module.