Wix Toolset – custom action to preserve connection string on upgrade

By Mirek on (tags: Installer, WiX, categories: code)

In my previous post I showed you a standard template of WiX installer. Today you will see how to add a custom action to the installer so you can perform more customized operations during the application installation process.

The custom action, which I am going to present in this post, will expose a method that extracts the value of specific node of provided xml file. The action itself will require the path to the xml file and xpath of the xml node which the value is going to be read. In particular we will use it to extract the connection string from existing .config file of our application. Thanks to this we will be able to preserve the connection string when the application is upgraded. The connection string which user already set in the config file will be read before new application is installed. The value is then kept in the installer session and after new version is installed and config file is replaced with new one, the connection string will be put into proper place. Thanks to that user will not need to reconfigure the database connection after new application version is installed.

Assume you already have an installer project in your application solution. To implement a custom action you must add additional project which will contain your action logic. Got to Add New Project and select C# Custom Action Project from Windows Installer XML section of available projects templates. Let name this new project CustomActions. In the class file inside the project put following code

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Text;
   4: using Microsoft.Deployment.WindowsInstaller;
   5: using System.Xml.XPath;
   6: using System.IO;
   7:  
   8: namespace CustomActions
   9: {
  10:     public class CustomActions
  11:     {
  12:         [CustomAction]
  13:         public static ActionResult ReadXMLNodeValueAction(Session session)
  14:         {
  15:             try
  16:             {
  17:                 session.Log("Begin ReadXMLNodeValueAction");
  18:                 var existingfilepath = session["CONFIGFILE"];
  19:                 var connstringpath = session["CONFIG_NODE_TO_UPDATE_PATH"];
  20:                 if (File.Exists(existingfilepath))
  21:                 {
  22:                     session.Log("Reading connection string ({0}) from config file {1}", connstringpath, existingfilepath);
  23:                     var doc = new XPathDocument(existingfilepath);
  24:                     var navigator = doc.CreateNavigator();
  25:  
  26:                     var node = navigator.SelectSingleNode(connstringpath);
  27:                     if (node != null)
  28:                     {
  29:                         session["ORIGINAL_CONFIG_VALUE"] = node.Value;
  30:                         session.Log("Connection string found.");
  31:                     }
  32:                 }
  33:                 session.Log("End ReadXMLNodeValueAction");
  34:             }
  35:             catch (Exception ex)
  36:             {
  37:                 session.Log("ERROR in custom action ReadXMLNodeValueAction {0}", ex.ToString());
  38:                 return ActionResult.Failure;
  39:             }
  40:             return ActionResult.Success;
  41:         }
  42:     }
  43: }

The session object is a dictionary which contains all variables used during the installer process. This is the way that our custom action communicates with the installer. In lines 18 and 19 we query the installer session for  path of the xml file and xpath of the node which value is to be read. Those variables must be set before the installer invokes the action. You will see later how to do that in the wxs file. When we successfully read the node value we then store it in the installer session so it is available in installation process. This is done in line 29. The variable ORIGINAL_CONFIG_VALUE must also be defined before the execution enters the action.

Now go to your main setup project and add a reference to custom action library project. What we now need to do is to prepare the custom action parameters and executions. Open main wxs file which contains the definition of your installer and add following lines inside the Product section node

   1: <Property Id='ORIGINAL_CONFIG_VALUE' Hidden='yes' />
   2:  
   3: <Binary Id='CustomActionDLL'
   4:         SourceFile='$(var.CustomActions.TargetDir)CustomActions.CA.dll'/>
   5:  
   6: <CustomAction Id="CU_ReadXMLNodeValueAction"
   7:             Return="check"
   8:             Execute="immediate"
   9:             BinaryKey="CustomActionDLL"
  10:             DllEntry="ReadXMLNodeValueAction" />
  11:  
  12: <CustomAction Id='AssignConfigFile'
  13:                Property='CONFIGFILE'
  14:                Value='[INSTALLDIR]MyWpfApp.exe.config' />
  15:  
  16: <CustomAction Id='AssignUpdatePath' Return='check'
  17:                Property='CONFIG_NODE_TO_UPDATE_PATH'
  18:                Value='/configuration/connectionStrings/add[@name="AppContext"]/@connectionString' />
  19:  
  20: <InstallExecuteSequence>
  21:   <Custom Action='AssignConfigFile' After='CostFinalize' />
  22:   <Custom Action='AssignUpdatePath' After='AssignConfigFile'/>
  23:   <Custom Action='CU_ReadXMLNodeValueAction' After='AssignUpdatePath' />
  24: </InstallExecuteSequence>

Line 1: We define the variable which hold the result value of custom action execution
Line 3: Here we define the external library definition. Note that we point to CustomActions.CA.dll not CustomActions.dll. This is the special kind of library which is supported by WiX.
Lines 6-10: The actual custom action is declared as a entry point on CustomActionDLL. The ReadXMLNodeValueAction is the name of actual custom action method.
Lines 12-14: This is a standard action which simply assigns a value to the variable. In this case the path to the config file is set on the CONFIGFILE variable. This is then read in our custom action.
Lines 16-18: Then we set the node path we want to read. Variable CONFIG_NODE_TO_UPDATE_PATH is read in custom action then.
Lines 20-24: Finally we inject the execution of three custom actions into the execution of our installer process. First of all we define the config file path variable, then we define the xml node path and after those two we invoke our custom action.

Ok, so we have read the original connection string so far. But now we need to wait until all installation files are copied so we can restore the connection string in the newly copied config file. To achieve that we can use the XMLElement of Wix utils. Simply go to the configuration file component definition and add few lines like this

   1: <Component Id="cMainExecutableConfig" Guid="{DD3F8D08-E8C4-4170-A5C6-5EEE67C5EB1E}" Directory="INSTALLDIR">
   2:   <File DiskId="1" Source="$(var.AppTargetDir)$(var.AppProjectName).exe.config" KeyPath="yes" />
   3:   <util:XmlFile Id="SetConnectionString"
   4:                 Action="setValue"
   5:                 ElementPath="[CONFIG_NODE_TO_UPDATE_PATH]"
   6:                 File="[INSTALLDIR]MyWpfApp.exe.config"
   7:                 Value="[ORIGINAL_CONFIG_VALUE]" />
   8: </Component>

And also a required namespace definition at the beginning of wxs file

   1: <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
   2:      xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">

This simply puts value of ORIGINAL_CONFIG_VALUE variable into the MyWpfApp.exe.config under CONFIG_NODE_TO_UPDATE_PATH node.

That’s it. From now our application user never looses their connection string after application upgrade.

Cheers