Introduction
Recently I was trying to write a C# service using the .NET framework and ran into a lot of issues that were not covered in the documentation or if they were, the solution wasn't easily inferred. So I decided to write a series of articles describing in step-by-step detail, how I went about solving the problems I encountered.
This is a multi-article contribution which is broken down as follows:
- Creating the project, initial service and installation
- Adding additional services to the application
- Adding support for custom event logging
Using the code
Lets start off by creating a new C# project that is a console application. Visual Studio .NET does give you a wizard to create a Windows Service using C#, but I'll take it from scratch so that those who don't have VS.NET can still follow along. Figure 1 shows the initial project creation screen.
Figure 1
Now that we have a simple console application created, we need to change some of the defaults that are created by the wizard. Lets open up the Class1.cs file. This file contains the entry point for the application, but we're going to rename the class Class
to better reflect it's role in the application. Right click the Class1.cs node in the Solution Explorer and choose Rename. Rename the file Application.cs. Then right click and choose View Code so that we can edit the code directly. We're going to rename the class Class
to Application
.
We are also going to change the namespace to reflect where the class exists within our company's namespace. I have a domain that I use for my sample code, called CodeBlooded
and I'm going to use this as my company name. You are free to use any name that you want. Since this service is part of a fictional Spades game (that I may end up fully developing over time), we are going to name our namespace CodeBlooded.Spades.Services
. Edit the new Application.cs file and change the namespace from the default of SpadesServer
to CodeBlooded.Spades.Services
.
To keep us from having to make these changes for every class we add to the project, open up the project properties dialog by right clicking the SpadesServer project in our solution and choose Properties. In the project properties dialog, go to the Common Properties | General tab and change the Application | Default Namespace field to CodeBlooded.Spades.Services
. From now on, any class we create will automatically be assigned to this namespace. Figure 2 shows you what this tab should look like.
Figure 2
While we are in the project settings dialog, go to the Configuration Properties | Build tab and change the Outputs | XML Documentation File settings to SpadesServer.xml. This is an XML file that the compiler will create for us to help us build our documentation for the project. Look in the .NET framework SDK documentation on what documentation tags are supported. Figure 3 shows what this dialog should look like.
Figure 3
Now that we've taking care of some configuration details in time to get started on actually creating some code. The .NET framework has a base class ServiceBase
that provides the interface to implement a service. This class must be derived from, to add our code that actually has the logic for the services we want to create. We must override the default methods of the OnStart
and OnStop
methods. These methods are called by the Service Control Manager to actually control the service.
The problem is that the OnStart
and OnStop
methods must return control back to the Service Control Manager within 1 minute, for the Service Control Manager to recognize that the service is running or has stopped. So how do we actually get any work done, if we must return within one minute. We must create a background thread that will perform all the work that our service is going to do.
To keep from having to write the same code for each of the classes that we will create during this tutorial, I'm going to create a base class that is responsible for creating the worker thread as well as communicating with the SCM. Create a new class that we'll call SpadesServiceBase
. The following code is what the class should look like.
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.ServiceProcess;
namespace CodeBlooded.Spades.Services
{
public class SpadesServiceBase : System.ServiceProcess.ServiceBase
{
public SpadesServiceBase()
{
}
protected override void OnStart(string[] args)
{
}
protected override void OnStop()
{
}
}
}
We will eventually create 3 services to control the state of the game, with an administration service that will control and/or manage the other 3 services. To handle this, the administration service will derive from the SpadesServiceBase
class but we'll create another class that derives from SpadesServiceBase
class that the 3 child services will derive from. This helps us isolate the shared code better, by having the lowest level base class contain only the code that is applicable to all services, and the child services base class will contain the common code that is shared across all child services.
So lets create another class called SpadesChildServiceBase
and have it derive from SpadesServiceBase
. Create the file any way you want, but the code should end up looking like the following:
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.ServiceProcess;
namespace CodeBlooded.Spades.Services
{
public class SpadesChildServiceBase : SpadesServiceBase
{
public SpadesChildServiceBase()
{
}
protected override void OnStart(string[] args)
{
base.OnStart( args );
}
protected override void OnStop()
{
base.OnStop();
}
}
}
Now we're going to write the code to create the background thread and the synchronization objects to be used by the SCM to start and stop the background thread. The background thread will periodically wake up and perform some action. If you were going to write a service that was dependant on messages coming from a client, then you use whatever IPC mechanism you wanted to control the communication. For best performance, you should make this code event driven instead of writing a poling mechanism. In future articles I may describe the way to accomplish this by using asynchronous socket calls, but for now I'm going to simply create a poling mechanism to manage the workflow of the services.
To facilitate the base classes communicating with the more specific derived classes, the base class will provide a pure virtual method called Execute
that will need to be implemented by the service. The Execute
method is called periodically by the base classes as they periodically get signaled to run by our timer. The Execute
method should contain the specific logic that you would need to do periodically, to accomplish what each individual service is responsible for.
We need to add a reference to System.Threading
to our SpadesServiceBase
class and add a member variable of Thread
that we'll call m_thread
. We will also need a ManualResetEvent
from the Threading
namespace to be used to communicate our stop request from the SCM. In the OnStart
method of the SpadesServiceBase
, we'll create an instance of a Thread
object as well as the ManualResetEvent
. To run a thread, we need to define a thread start method that is the entry point for the thread. In our case we'll call our method ServiceMain
and use a ThreadStart
class from the framework to define our ServiceMain
as a delegate method. Modify your SpadesServiceBase
class to look like the following code block.
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.ServiceProcess;
using System.Threading;
namespace CodeBlooded.Spades.Services
{
public class SpadesServiceBase : System.ServiceProcess.ServiceBase
{
public SpadesServiceBase() {
m_delay = new TimeSpan(0, 0, 0, 10, 0 );
}
protected override void OnStart(string[] args) {
ThreadStart ts = new ThreadStart( this.ServiceMain );
m_shutdownEvent = new ManualResetEvent(false);
m_thread = new Thread( ts );
m_thread.Start();
base.OnStart( args );
}
protected override void OnStop() {
m_shutdownEvent.Set();
m_thread.Join(10000);
base.OnStop();
}
protected void ServiceMain() {
bool bSignaled = false;
int nReturnCode = 0;
while( true ) {
bSignaled = m_shutdownEvent.WaitOne( m_delay, true );
if( bSignaled == true )
break;
nReturnCode = Execute();
}
}
protected virtual int Execute() {
return -1;
}
protected Thread m_thread;
protected ManualResetEvent m_shutdownEvent;
protected TimeSpan m_delay;
}
}
Now that we have these details taken care of, lets go ahead and create the administrative service. Right now the service won't be responsible for a whole lot, but over the next few lessons we'll add more functionality to it. For now create a new class SpadesAdminService
and derive it from SpadesServiceBase
. The code below is the boilerplate code for the administrative service.
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.ServiceProcess;
namespace CodeBlooded.Spades.Services
{
public class SpadesAdminService : SpadesServiceBase
{
public SpadesAdminService()
{
this.ServiceName = "SpadesAdminSvc";
}
protected override void OnStart(string[] args)
{
base.OnStart( args );
}
protected override void OnStop()
{
base.OnStop();
}
protected override int Execute() {
System.Diagnostics.EventLog.WriteEntry("SpadesAdminSvc",
ServiceName + "::Execute()");
return 0;
}
}
}
Ok now that we have a service written, we need to do some more plumbing work to communicate to Windows that we have services in this executable. To do this is straightforward and all we need to do is create a collection of ServiceBase
objects and simply call the Run
method of the base class ServiceBase
. Add the following code to the Main
method in the Application
object. The following code fragment shows all that is necessary to start our services.
static void Main(string[] args)
{
ServiceBase[] servicesToRun;
servicesToRun = new ServiceBase[] { new SpadesAdminService() };
ServiceBase.Run( servicesToRun );
}
We're getting really close to wrapping this lesson up and all we need to do to be able to run the service, is add our Installers
. The installers can either be called through a setup program that I may document in some future article or they can be called by the InstallUtil program that comes with the framework SDK.
Add a new installer to the project and we'll call it SpadesInstaller
. After you run the Add Class wizard, we'll add some code to the class constructor. The installers use attributes that are applied to the class, where the installer will create an instance of each class that has the RunInstaller
attribute applied to the class.
All we'll need is an instance of the ServiceProcessInstaller
to control how the service process is started and then for each service that we will be installing, we'll create a ServiceInstaller
object. After setting some properties on these objects, we'll then add it to the Installers
collection. The following code shows all that we need to install our service.
using System;
using System.Collections;
using System.ComponentModel;
using System.Configuration.Install;
using System.ServiceProcess;
namespace CodeBlooded.Spades.Services
{
[RunInstaller(true)]
public class SpadesInstaller : System.Configuration.Install.Installer
{
public SpadesInstaller()
{
ServiceProcessInstaller process = new ServiceProcessInstaller();
process.Account = ServiceAccount.LocalSystem;
ServiceInstaller serviceAdmin = new ServiceInstaller();
serviceAdmin.StartType = ServiceStartMode.Manual;
serviceAdmin.ServiceName = "SpadesAdminSvc";
serviceAdmin.DisplayName = "Spades Administration Service";
Installers.Add( process );
Installers.Add( serviceAdmin );
}
}
}
After compiling to test our code, all we need to do is start a command prompt and move to our output directory. For now we'll just use the bin\Debug subdirectory of our project and run the following command to have our service installed.
installutil SpadesServer.exe
If you want to uninstall the service run the following command.
installutil SpadesServer.exe -U
Once this is complete, we should be able to start and stop the service from the Service Control Manager, to our hearts content.
Over the next few articles, we'll add more functionality to the framework that we've created here. We'll add some child services that when they start will start the admin service if it's not already started. The admin service will shutdown the child services if the admin service is stopped. This is similar to the way IISAdmin controls the different web services. In addition, we'll add a custom message log as well as a message log installer.
History