woensdag 6 maart 2013

Building Windows Services with C# and Topshelf

The quickstart on the Topshelf site is only showing the real basics of getting a Windows Service up and running. There are a couple of importants details not touched, that I will write about in this blog.

Create a new Solution with the Console Application Template. Because Topshelf is doing the plumbing, we don't need to use the Windows Service template anymore.




If you're using Visual Studio 2010, don't forget to turn off Client Profile

When the solution is created it will look like this:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace XmplWindowsService
{
    class Program
    {
        static void Main(string[] args)
        {
        }
    }
}

The Main method is the entrypoint of the Windows Service. This is where we will setup Topshelf. But before we do that, we have to create a class that will do the actual work that will be done inside the Windows Service. So let's create a new class:

public class MyService
{
}

This class must have a Start() and a Stop() method:

public class MyService
{
   public void Start()
   {
   }

   public void Stop()
   {
   }
}


This is what we have so far:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace XmplWindowsService
{
    class Program
    {
        static void Main(string[] args)
        {
        }
    }

    public class MyService
    {
        public void Start()
        {
        }

        public void Stop()
        {
        }
    }
}


Now we can install Topshelf into the project:




Let's setup Topshelf inside the Main method of the Console Application:


static void Main(string[] args)
{
   HostFactory.Run(hostConfigurator =>
   {
      hostConfigurator.Service<MyService>(serviceConfigurator =>
      {
         serviceConfigurator.ConstructUsing(() => new MyService());
         serviceConfigurator.WhenStarted(myService => myService.Start());
         serviceConfigurator.WhenStopped(myService => myService.Stop());
      });

      hostConfigurator.RunAsLocalSystem();

      hostConfigurator.SetDisplayName("MyService");
      hostConfigurator.SetDescription("MyService using Topshelf");
      hostConfigurator.SetServiceName("MyService");
   });
}


When we run the application, we will see this:


Configuration Result:
[Success] Name MyService
[Success] Description MyService using Topshelf
[Success] ServiceName MyService
Topshelf v3.1.106.0, .NET Framework v4.0.30319.18033
The MyService service is now running, press Control+C to exit.

It shows us its name, description and servicename, just like we configured. We can see the version numbers, and the last line tells us how the stop the Windows Service, when debugging or when it's running as console application. Offcourse we can't press Control+C to stop it when it's running as a Windows Service.

Now the MyService class isn't doing anything just yet. So now it's time to give MyService some work to do. Let's for example write something to screen every second:


public class MyService
{
   public void Start()
   {
      while (true)
      {
         Console.WriteLine("I am working");
         System.Threading.Thread.Sleep(1000);
      }
   }

   public void Stop()
   {
   }
}

When we start the app we will see it write "I am working" to the screen every second. However, when we press Control+C the application will not stop. Topshelf will try to stop MyService, but won't succeed since MyService is in a never ending while-loop.

So we have to have a signal to let the loop know when to stop. Therefore we add a boolean named doWork, and set it to true when we start the loop. The doWork boolean will be set to false when we press Control+C and Topshelf calls the Stop() method.


public class MyService
{
   private bool _doWork;

   public void Start()
   {
      _doWork = true;

      while (_doWork)
      {
         Console.WriteLine("I am working");
         System.Threading.Thread.Sleep(1000);
      }
   }

   public void Stop()
   {
      _doWork = false;
   }
}


When we start the debugger and press Control+C, the application will quit as it should.

Unfortunately the code is still nog perfect. Remember when we first started the application where MyService did nothing and Topshelf wrote this to the console:

Configuration Result:
[Success] Name MyService
[Success] Description MyService using Topshelf
[Success] ServiceName MyService
Topshelf v3.1.106.0, .NET Framework v4.0.30319.18033
The MyService service is now running, press Control+C to exit.


Now that MyService is running in a while loop, the last line is not printed anymore. Well actually it is printed after we stopped the service.


Configuration Result:
[Success] Name MyService
[Success] Description MyService using Topshelf
[Success] ServiceName MyService
Topshelf v3.1.106.0, .NET Framework v4.0.30319.18033
I am working
I am working
I am working
Control+C detected, attempting to stop service.
The MyService service is now running, press Control+C to exit.
The MyService service has stopped.

The message that the service is running, is printed after the Start() method of MyService returns, but that only happens when we stop the service. If we would use a Timer to do something every second it wouldn't be a problem, because a Timer is doing it's work asynchronously. And when we read messages from a queue async it wouldn't be a problem either. But I think it's good to show how we can make the Start() method in this example return immediately too.


System.Threading.Tasks

We are going to use the System.Threading.Tasks namespace to make MyService execute asynchronous work.

First let's move the content of the Start() method, to another method called DoWork():

private void DoWork()
{
   _doWork = true;

   while (_doWork)
   {
      Console.WriteLine("I am working");
      System.Threading.Thread.Sleep(1000);
   }
}

we are going to call the DoWork() method with a Task. Let's create this task in the constructor of MyService:


public class MyService
{
   bool _doWork;
   readonly Task _task;

   public MyService()
   {
      _task = new Task(DoWork);
   }

   ...
}

Now we can start this Task from the Start() method of MyService:


public void Start()
{
   _task.Start();
}


This is what MyService looks like now:


public class MyService
{
   bool _doWork;
   readonly Task _task;

   public MyService()
   {
      _task = new Task(DoWork);
   }

   public void Start()
   {
      _task.Start();
   }

   public void Stop()
   {
      _doWork = false;
   }

   private void DoWork()
   {
      _doWork = true;

      while (_doWork)
      {
         Console.WriteLine("I am working");
         System.Threading.Thread.Sleep(1000);
      }
   }
}


When we run the application, we will see this:


Configuration Result:
[Success] Name MyService
[Success] Description MyService using Topshelf
[Success] ServiceName MyService
Topshelf v3.1.106.0, .NET Framework v4.0.30319.18033
The MyService service is now running, press Control+C to exit.
I am working
I am working

Exactly what we want. The message that MyService is running is coming first, and then the "I am working" messages are written.

But it's still not completely right. Let me tell you why.


Quit before work is finished

The problem that we have now is that the Windows Service will quit if you press Control+C, but it doesn't take into account any work in progress. This can be demoed quite easily.

Add some more work to the DoWork() method:


private void DoWork()
{
   _doWork = true;

   while (_doWork)
   {
      Console.WriteLine("I am working");

      Console.WriteLine("   Step 1");
      System.Threading.Thread.Sleep(1000);

      Console.WriteLine("   Step 2");
      System.Threading.Thread.Sleep(1000);

      Console.WriteLine("   Step 3");
      System.Threading.Thread.Sleep(1000);

      Console.WriteLine("   Step 4");
      System.Threading.Thread.Sleep(1000);

      Console.WriteLine("   Step 5");
      System.Threading.Thread.Sleep(1000);
   }
}


If we run the application, and then press Control+C after step one, we can get output like this:

Configuration Result:

[Success] Name MyService
[Success] Description MyService using Topshelf
[Success] ServiceName MyService
Topshelf v3.1.106.0, .NET Framework v4.0.30319.18033
The MyService service is now running, press Control+C to exit.
I am working
  Step 1
  Step 2
  Step 3
  Step 4
  Step 5
I am working
  Step 1
Control+C detected, attempting to stop service.
The MyService service has stopped.


The yellow marked lines are important. You can see that step 2, 3, 4 and 5 are not written, because the Service has stopped. However, I only want the service to be stopped when step 5 is finished. The best way to do this is remove the _doWork boolean, and use a Cancellationtoken.

Let's switch back to the constructor of MyService, and create a CancellationToken that we can use in the Task:

public class MyService
{
   readonly CancellationTokenSource _cancellationTokenSource;
   readonly CancellationToken _cancellationToken;
   readonly Task _task;

   public MyService()
   {
      _cancellationTokenSource = new CancellationTokenSource();
      _cancellationToken = _cancellationTokenSource.Token;

      _task = new Task(DoWork, _cancellationToken);
   }

   ...
}

Now we can use the CancellationTokenSource in the DoWork() method:

private void DoWork()
{
   while (!_cancellationTokenSource.IsCancellationRequested)
   {
      Console.WriteLine("I am working");

      Console.WriteLine("   Step 1");
      System.Threading.Thread.Sleep(1000);

      Console.WriteLine("   Step 2");
      System.Threading.Thread.Sleep(1000);

      Console.WriteLine("   Step 3");
      System.Threading.Thread.Sleep(1000);

      Console.WriteLine("   Step 4");
      System.Threading.Thread.Sleep(1000);

      Console.WriteLine("   Step 5");
      System.Threading.Thread.Sleep(1000);
   }
}

And we can cancel the Task in the Stop() method:

public void Stop()
{
   _cancellationTokenSource.Cancel();
   _task.Wait();
}

If we run the application, and then press Control+C after step one, we get exactly what we want:

Configuration Result:
[Success] Name MyService
[Success] Description MyService using Topshelf
[Success] ServiceName MyService
Topshelf v3.1.106.0, .NET Framework v4.0.30319.18033
The MyService service is now running, press Control+C to exit.
I am working
  Step 1
  Step 2
  Step 3
  Step 4
  Step 5
I am working
  Step 1
Control+C detected, attempting to stop service.
  Step 2
  Step 3
  Step 4
  Step 5
The MyService service has stopped.


Conclusion

In this blog I've shown how easy it is to create a Windows Service in C# and the Topshelf nuget package. I've also shown how you can use a Task to start an asynchronous proces to do some work, and how the task can be stopped in a clean way, without leaving partial executed work behind.

9 opmerkingen:

  1. Congratulations for your post! I was trying to convert a console application as a service and after read your article I could finish and It looked perfect! Thanks!

    BeantwoordenVerwijderen
  2. This is a fantastic and simple example of how to create the foundation for a console service. Thank you so much.

    BeantwoordenVerwijderen
  3. This is the Real thing!!! No more Use of Traditional Web Services

    BeantwoordenVerwijderen
  4. No More use of Template Windows Services Applications!!

    BeantwoordenVerwijderen
  5. Hi. Your Topshelf example is one that I find is very straightforward and functional. I have seen other examples, and can I ask you, how do I convert existing Console Application which is multi-threaded? I have seen that Topshelf don't handle muti-threading, but it is in the Console Application itself. If I follow correctly, this will be handled in the DoWork() section. Just not very sure. Please help. Thank you!

    BeantwoordenVerwijderen
    Reacties
    1. Hi Euthychus7,

      There are a few options for doing multi-threaded work:

      1. Install the service multiple times
      2. Writemulti-threaded code in MyService.

      Verwijderen
  6. Hello! Thank you for your reply. I have done it already. My console application already uses multi-threading, so I had to use up to the plain call to the worker thread (thread.Start()). If I make use of the cancellationToken code, the multi-threaded program has difficulty in stopping the service.

    All in all, your example is very good, and very straightforward. That is why I am able to follow and experiment on my own. So thank you very much!

    BeantwoordenVerwijderen
  7. Thank you! I knew a simple boolean flag felt wrong.

    BeantwoordenVerwijderen
  8. @Euthychus7

    Look at this fluent method for the service configuration.

    x.SetInstanceName("InstanceOne");

    BeantwoordenVerwijderen