We have seen how to create and to deploy a windows service in C#. However this post was explaining how to use to the designer functionality to deploy our service. This is not really a problem (who wants to do himself what has already been done by another one ?) but it has a "huge" drawback. This installer needs to be next to our service. And who on earth really want to find in a same place the production code (ie the service) and the deployment code (ie the installer) ? The next step would probably to add some test code, and then...
So we will see here how we can create the installer class in a separate DLL.
Creating the installer's action class manually
-
Add a new project of type "Empty Project" (this type is avaiable under Visual C# / Windows) and let's name it "MyWindowsServiceInstallerActions"
-
Right-clic on this project and select "Add / New Item" and then "General / Installer Class". Name it "MyInstaller"
-
Add a new reference to "System.ServiceProcess.dll"
-
Switch to code view and let's complete the constructor as follows
using System.Collections;
using System.ComponentModel;
using System.Configuration.Install;
using System.IO;
using System.ServiceProcess;
namespace MyWindowsServiceInstallerActions
{
[RunInstaller(true)]
public partial class MyInstaller : Installer
{
public MyInstaller()
{
InitializeComponent();
ServiceInstaller myServiceInstaller = new ServiceInstaller();
myServiceInstaller.ServiceName = "MyPersonalService";
myServiceInstaller.DisplayName = "My Personal Service";
myServiceInstaller.StartType = ServiceStartMode.Automatic;
this.Installers.Add(myServiceInstaller);
ServiceProcessInstaller myProcessInstaller = new ServiceProcessInstaller();
myProcessInstaller.Account = ServiceAccount.LocalService;
this.Installers.Add(myProcessInstaller);
}
}
}
We now have a functional installer class. Let's now update our setup project to use this one !
Update the setup project to use our installer
Let's check if this is functional
Let's rebuild and install our setup project. If we then go to the Service manager we can again see our service. Let's start it !
This time it does not work. We have a nice error saying something like "Error 193 0xc1". When we check in the event viewer we can have a little bit more explanation : "The My Personal Service service failed to start due to the following error : My Personal Service is not a valid Win32 application."
Yes that's right, a service must be an ".exe" file and not a ".dll" file but of course our service project generates a ".exe" and no problem here. So where ?
Let's check if our service registration is correct. All services are registered in the registry. So let's check there ! Check the key "HKEY_LOCAL_MACHINE \ SYSTEM \ CurrentControlSet \ Services" and you will find an entry for all services you have on your computer. So you will find an entry for the "My Personal Service" and you can check the value of the key "ImagePath".
Surprise ! For Windows, our service assembly is the MyWindowsServiceInstallerActions.dll file. It's so highly normal it does not work. Why this value ? Simply because the ServiceInstaller class just register the current assembly. So let's correct that !
Note that when you have tried to start your project, you will need to uninstall it (via Visual Studio) and the to log off so Windows can correctly uninstall it.
Let's correct the setup project
First we will need to know where the project is being installed
- Right-clic on the setup project and choose "View / Custom Actions"
- Select the "Primary Output" under the "Install" event and check the property
- Update the CustomActionData property to set /Target="[TARGETDIR]\"
Doing this, we declare a new parameter called "Target" that will have for value, the value of "TARGETDIR" which is a system value giving us the installation directory. Be careful not to forget the quotes (") and the trailing backslash (\). Indeed to set some parameters to system values you need to use of the two following syntax:
The second syntax must be used if the system value may contain spaces (which is typically the case of an installation directory).
Now we can update the code of our installer class as follows :
using System.Collections;
using System.ComponentModel;
using System.Configuration.Install;
using System.IO;
using System.ServiceProcess;
namespace MyWindowsServiceInstallerActions
{
[RunInstaller(true)]
public partial class MyInstaller : Installer
{
public MyInstaller()
{
}
protected override void OnBeforeInstall(IDictionary savedState)
{
base.OnBeforeInstall(savedState);
string targetPath = this.Context.Parameters["Target"];
this.Context.Parameters["assemblypath"] = Path.Combine(targetPath, "MyWindowsService.exe");
}
}
}
A few notes about this code :
-
You could have decided to override the Install method itself.
-
It's impossible to move this code to the constructor as the context parameters are unknown at that place
-
Note that the parameter "assemblyPath" is used internally by the ServiceInstaller to update the registry
Let's check again
We can now rebuild, reinstall and start the service again. Now this is working fine !
You now have a functional service with a correct separation of production code and deployment code. What would be the next step ? To update again the setup project to have our service start immediately after the installation. See you soon !