I am using the Visual Studio Installer Projects extension for Visual Studio 2019. I have created my setup project and want to add custom actions which I want to implement as C# methods (I want the methods to install/uninstall a certificate). What I am not clear on is the linkage between the custom action configuration in the Custom Actions panel (appearing in Visual Studio) and the C# code I want to invoke. How do I create the class/methods such that they will be visible to the installer and activated as required? Does the class have to have a certain inheritance, and if so, what assemblies are required?
-
[VS Installer Projects have many limitations](https://stackoverflow.com/questions/64431682/can-i-convert-a-sequence-of-wxi-files-to-a-visual-studio-wix-project#comment113935768_64431682) ([shortlist](https://stackoverflow.com/a/2637666/129130)). You should [avoid custom actions too](https://stackoverflow.com/a/46179779/129130), but if you need them WiX has all the features you need - unless you want to [try a commercial tool](https://stackoverflow.com/a/50229840/129130). Here is [a rudimentary sample of WiX and custom actions](https://github.com/glytzhkof/WiXCustomActionsTesting). – Stein Åsmul Mar 30 '22 at 21:05
-
I realize that vdproj is limited, but for my needs it gets the job done... outside of the certificate installation I want to do. I looked at WiX once, but it was lacking a UI which vdproj has; for a simple installation, I found far less friction. – Zenilogix Mar 30 '22 at 21:28
3 Answers
You can create them just like you would a normal class. You will need to import a couple of namespaces to do it.
Specifically
- Microsoft.Deployment.WindowsInstaller;
- Microsoft.Tools.WindowsInstallerXml;
Here is a shortened version of a custom action I did for an installer a little while back.
using Microsoft.Deployment.WindowsInstaller;
using Microsoft.Tools.WindowsInstallerXml;
public class CustomActions : WixExtension
{
[CustomAction]
public static ActionResult CreateAndPopulateStorageFolder(Session session)
{
ActionResult result = ActionResult.Success;
try
{
session.Log("In Custom Action: CreateAndPopulateStorageFolder");
if (!Directory.Exists("C:\\Path"))
{
session.Log("In Custom Action: CreateAndPopulateStorageFolder: Directory does not exist");
Directory.CreateDirectory("C:\\Path");
Directory.CreateDirectory("C:\\Path");
Directory.CreateDirectory("C:\\Path");
}
}
catch (Exception ex)
{
session.Log(ex.Message);
result = ActionResult.Failure;
}
return result;
}
}

- 58
- 6
-
I can't find Microsoft.Deployment.WindowsInstaller on my computer. A quick google suggests to me that I would need to install WiX; is that my only option here? – Zenilogix Mar 30 '22 at 19:42
-
I found the assembly... downloaded the WiX binaries zip. I couldn't get my action to invoke... not clear why. – Zenilogix Mar 30 '22 at 21:23
I found the answer in this article. Basically amounts to adding an Installer
class to my assembly (inherits from System.Configuration.Install.Installer
) and overriding the appropriate methods. Need to set the InstallerClass property to True for the Custom Action Properties.
using System.Collections;
using System.ComponentModel;
using System.Configuration.Install;
using System.IO;
namespace InstallerCustomAction
{
[RunInstaller(true)]
public partial class MyInstaller : Installer
{
public MyInstaller()
{
InitializeComponent();
}
public override void Install(IDictionary stateSaver)
{
File.WriteAllText(@"C:\InstallTest.txt", "This is a test of an install action");
base.Install(stateSaver);
}
}
}
(Note that the template creates a corresponding Designer.cs file)
This doesn't seem to support incoming parameters, but it's not something I need.

- 1,318
- 1
- 15
- 31
-
Installer classes are generally intended to be used during development for testing and debugging ([more here](https://stackoverflow.com/a/50144232/129130)). They are often used to test service components. MSI has its own features to fully support installation of services without any custom action code - you just set parameters in regular MSI tables. You should implement proper custom action code as indicated in the provided samples and answers. – Stein Åsmul Mar 30 '22 at 21:27
-
The last part means that if you can't do without custom actions, you should not use Installer classes, but proper custom action code as shown in [the rudimentary WiX CA sample](https://github.com/glytzhkof/WiXCustomActionsTesting). To me the Installer class concept is a little similar to COM self-registration, a feature which is [also not recommended for deployment](https://stackoverflow.com/a/6141538/129130) for a number of reasons (rollback problems, error handling and logging issues, etc...). A proper custom action can also be debugged with Visual Studio with breakpoints and step-through. – Stein Åsmul Mar 31 '22 at 07:45
-
@SteinÅsmul I am having this problem: https://stackoverflow.com/questions/30933856/visual-studio-installer-project-custom-action-entry-point-not-found when I attempt the `[CustomAction]` etc. approach. What am I missing? Bear in mind I am using vdproj, *not* WiX (other than the Microsoft.Deployment.WindowsInstaller assembly) – Zenilogix Mar 31 '22 at 15:09
-
@SteinÅsmul I think I now understand my problem. I incorrectly assumed MSI Custom Actions has built-in support for .NET/CLR (it appears that it doesn't); if I want to invoke C# I have to either implement as an .exe or wrap my .NET in C++ somehow (not clear how). – Zenilogix Mar 31 '22 at 15:30
-
WiX custom actions are wrapped in a native dll wrapper using a special tool called `MakeSfxCA.exe`. So you get 2 dlls, the latter suffixed with `.CA.dll` or something like that. This file will do the work of invoking the managed code. I wrote an answer - ages ago - on [how to try to inject a WiX custom action dll](https://stackoverflow.com/a/52320995/129130) in MSI files created via other means. Perhaps the best benefit of a proper MSI CA dll is the ability to [debug with step through](https://stackoverflow.com/a/52880033/129130). – Stein Åsmul Mar 31 '22 at 19:25
-
[Try that WiX CA sample I linked to above](https://github.com/glytzhkof/WiXCustomActionsTesting) - just compile and find the dlls I mention. Then compile the MSI and see how the files are included. – Stein Åsmul Mar 31 '22 at 19:30
Found the answer I was looking for in DllExport (https://github.com/3F/DllExport/wiki). This allowed me to create a .NET Framework assembly and export entry points to assembly methods as if they were conventional unmanaged C++ DLL exports, which can be invoked by msiexec as custom actions.
Basic steps:
- Create the assembly project and add a class with static methods to be exported.
- Add DllExport nuget reference to the project. The nuget includes a configuration tool; adding the nuget launches the tool. You need to identify your solution, the project within your solution, and (confusingly) the namespace containing the DllExport attribute (which should be
System.Runtime.InteropServices
). When you "Apply", it modifies the vcproj. - Add the
[DllExport]
attribute to each static method to be exported. On build, an assembly will be created which has C++ entry points to the methods. NOTE by default, there will be an "AnyCPU" version of the assembly which does not contain any exports, in addition to x86 and x64 versions of the assembly which do. You need to use one of those. - Add the x86 assembly to the installer project and reference the assembly and exported name in the custom action. You can also specify a value for CustomActionData.
Sample code:
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
namespace CustomActionFrameworkTest
{
public class CustomActions
{
/// <summary>
/// Import the MsiGetProperty method
/// </summary>
/// <param name="hInstall">Handle to the install instance</param>
/// <param name="szName">Property name we want the value of</param>
/// <param name="szValueBuf">Value</param>
/// <param name="pchValueBuf">Length of value (buffer size on input, actual length output)</param>
/// <returns></returns>
[DllImport("msi.dll", CharSet = CharSet.Unicode)]
static extern int MsiGetProperty(int hInstall, string szName,
[Out] StringBuilder szValueBuf, ref int pchValueBuf);
[DllExport]
public static int InstallTest(int handle)
{
// Debugger.Launch(); // If you need to debug
var sb = new StringBuilder(512); // Initialize to an arbitrary size
int size = 512; // Must give the function a buffer size
var status = MsiGetProperty(handle, "CustomActionData", sb, ref size);
if (status == 0)
{
var message = $"Got value '{sb}' from CustomActionData";
MessageBox.Show(message, "Test", 0);
}
else
{
MessageBox.Show($"MsiGetProperty failed with error code: {status}", "Error", 0);
}
return 0;// 0= success, 1602 = user exit, 1603 = failure
}
}
}

- 1,318
- 1
- 15
- 31