Installing a .NET Windows Service Multiple Times

Overview

I had a Windows service that was developed (in C# in Visual Studio 2013) to run against a database. Eventually, I needed to deploy this to our application server, but hit a wrinkle. We had only one application server to host the service, but multiple databases the service needed to run against. The solution was to install multiple instances of the service, each in its own folder and with its own configuration file. The key to getting this to work was to know how to install a .NET Windows service via installutil.exe and change the name of the service during installation. You do this by coding the service project installer to handle a custom command line argument. This article documents how to do this.

This article was adapted from http://www.damirscorner.com/InstallNETWindowsServiceWithADifferentName.aspx

Procedure

1. Create a Project Installer for the service. (This is done by right-clicking the design surface for the service and selecting "Add Installer" from the context menu.)

2. Change the code as follows.

This code implements the custom command line option "/ServiceName", which will be used later when building a batch file to install the service.

[RunInstaller(true)]
public partial class ProjectInstaller : System.Configuration.Install.Installer
{
    private string _dividerBar;

    public ProjectInstaller()
    {
        InitializeComponent();
        _dividerBar = "=".PadRight(100, '=');
    }
    private void SetServiceName()
    {
        WriteLog("Setting Service Name");
        var items = Context.Parameters;

        WriteLog(string.Format("Found the following {0} parameter(s)", items.Count));

        foreach (DictionaryEntry item in items)
        {
            WriteLog(string.Format("  {0} = [{1}]", item.Key, item.Value));
        }

        if (items.ContainsKey("ServiceName"))
        {
            var s = items["ServiceName"];
            WriteLog("ServiceName = [" + s + "]");
            // TODO: Change ACME_Jobs_Emailer to whatever your service is
            this.ACME_Jobs_Emailer.ServiceName = s;
            this.ACME_Jobs_Emailer.DisplayName = s;
        }

    }
    protected override void OnBeforeInstall(IDictionary savedState)
    {
        WriteLog(_dividerBar);
        WriteLog("Installing");
        SetServiceName();
        base.OnBeforeInstall(savedState);
    }
    protected override void OnBeforeUninstall(IDictionary savedState)
    {
        WriteLog(_dividerBar);
        WriteLog("Uninstalling");
        SetServiceName();
        base.OnBeforeUninstall(savedState);
    }
    private void WriteLog(string msg)
    {
        msg = DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss tt") + "> " + msg;
        this.Context.LogMessage(msg);
    }
}

3. Create app.config transforms

Using Slow Cheetah, you can create app.config transformations within your service project. Each transformation can contain a connection string and other settings specific to the environment where it will be deployed.

4. Write a batch file to install the services

Create the following batch file in the root folder of your service project, and be sure to include it in your Visual Studio project. You will need to change instances of "ACME.Jobs.Emailer" to the name of your service. You can also repeat the pattern of code in the batch file for as many environments as needed.

rem TODO: Replace all instances of "ACME.Jobs.Emailer" with the name of your service
rem ==============================================================================================================
rem ===== INSTALL UAT ============================================================================================
rem ==============================================================================================================
%windir%\Microsoft.NET\Framework64\v4.0.30319\installutil.exe /u /ServiceName="ACME.Jobs.Emailer - UAT" .\UAT\ACME.Jobs.Emailer.exe
%windir%\Microsoft.NET\Framework64\v4.0.30319\installutil.exe /ServiceName="ACME.Jobs.Emailer - UAT" .\UAT\ACME.Jobs.Emailer.exe
rem ==============================================================================================================
rem ===== INSTALL DEMO ===========================================================================================
rem ==============================================================================================================
%windir%\Microsoft.NET\Framework64\v4.0.30319\installutil.exe /u /ServiceName="ACME.Jobs.Emailer - DEMO" .\DEMO\ACME.Jobs.Emailer.exe
%windir%\Microsoft.NET\Framework64\v4.0.30319\installutil.exe /ServiceName="ACME.Jobs.Emailer - DEMO" .\DEMO\ACME.Jobs.Emailer.exe
rem ==============================================================================================================
rem ===== INSTALL TRAINING =======================================================================================
rem ==============================================================================================================
%windir%\Microsoft.NET\Framework64\v4.0.30319\installutil.exe /u /ServiceName="ACME.Jobs.Emailer - TRAINING" .\TRAINING\ACME.Jobs.Emailer.exe
%windir%\Microsoft.NET\Framework64\v4.0.30319\installutil.exe /ServiceName="ACME.Jobs.Emailer - TRAINING" .\TRAINING\ACME.Jobs.Emailer.exe
rem ==============================================================================================================
rem ===== INSTALL PRODUCTION =====================================================================================
rem ==============================================================================================================
%windir%\Microsoft.NET\Framework64\v4.0.30319\installutil.exe /u /ServiceName="ACME.Jobs.Emailer - PRODUCTION" .\Release\ACME.Jobs.Emailer.exe
%windir%\Microsoft.NET\Framework64\v4.0.30319\installutil.exe /ServiceName="ACME.Jobs.Emailer - PRODUCTION" .\Release\ACME.Jobs.Emailer.exe

5. Create Post-Build Event Code (optional)

As a convenience, I created the following post-build event command line to copy the batch file created above (which, in my case, I called install-amazon.bat) to the bin folder

copy /y "$(ProjectDir)install-amazon.bat" "$(TargetDir)..\install-amazon.bat"

6. Build the project for each Build Configuration of interest.

In my case, I would repeatedly select the Build Configuration, then build the project, then repeat this for each Build Configuration of interest.

7. Create a ZIP Deployment Package

Within Windows explorer, navigate to the bin folder for your project. ZIP up the batch file (in my case install-amazon.bat) along with all the folders for the environments of interest.

8. Deploy the ZIP Deployment package

Upload the ZIP deployment package to the server, extract it there, then run the batch file (in my case install-amazon.bat).