Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make a Windows Service from .NET Core 2.1/2.2

Recently I had a need to convert a .NET Core 2.1 or 2.2 console application into a Windows Service.

As I didn't have a requirement to port this process to Linux, I could dispense with the multiple platform solutions that I had seen on Stack Overflow that dealt with any combination of .NET Framework, .NET Standard and .NET Core.

like image 751
Mark McWhirter Avatar asked Dec 18 '18 16:12

Mark McWhirter


2 Answers

In this post I will describe the steps required to set up a .NET Core 2.1 or 2.2 process as a Windows Service.

As I have no requirement for Linux, I could look for a solution that was Windows-specific.

A bit of digging turned up some posts from Steve Gordon (thanks!), in particular where he presents the Microsoft.Extensions.Hosting package and Windows hosting (click here for post and here for his GitHub sample).

Here are the steps required:

  • First create a .NET Core console application.
  • Set the language version to at least 7.1 to support async Task for the Main method.  (Access the language version from the project settings->Build->Advanced->Language Settings.
  • Add the Microsoft.Extensions.Hosting and the System.ServiceProcess.ServiceController packages.
  • Edit the project .csproj file and include in the PropertyGroup: <RuntimeIdentifier>win7-x64</RuntimeIdentifier>
  • Ensure you have in the PropertyGroup  <OutputType>Exe</OutputType>

Now go to Program.cs and copy the following:

using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace AdvancedHost
{
    internal class Program
    {
        private static async Task Main(string[] args)
        {
            var isService = !(Debugger.IsAttached || args.Contains("--console"));

            var builder = new HostBuilder()
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHostedService<LoggingService>();
                });

            if (isService)
            {
                await builder.RunAsServiceAsync();
            }
            else
            {
                await builder.RunConsoleAsync();
            }
        }
    }
}

This code will support interactive debugging and production execution, and runs the example class LoggingService.

Here is a skeleton example of the service itself:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;

namespace AdvancedHost
{
    public class LoggingService : IHostedService, IDisposable
    {

        public Task StartAsync(CancellationToken cancellationToken)
        {
            // Startup code

            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            // Stop timers, services
            return Task.CompletedTask;
        }

        public void Dispose()
        {
            // Dispose of non-managed resources
        }
    }
}

The final two files necessary to complete the project:

File ServiceBaseLifetime.cs:

using Microsoft.Extensions.Hosting;
using System;
using System.ServiceProcess;
using System.Threading;
using System.Threading.Tasks;

namespace AdvancedHost
{

    public class ServiceBaseLifetime : ServiceBase, IHostLifetime
    {
        private readonly TaskCompletionSource<object> _delayStart = new TaskCompletionSource<object>();

        public ServiceBaseLifetime(IApplicationLifetime applicationLifetime)
        {
            ApplicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime));
        }

        private IApplicationLifetime ApplicationLifetime { get; }

        public Task WaitForStartAsync(CancellationToken cancellationToken)
        {
            cancellationToken.Register(() => _delayStart.TrySetCanceled());
            ApplicationLifetime.ApplicationStopping.Register(Stop);

            new Thread(Run).Start(); // Otherwise this would block and prevent IHost.StartAsync from finishing.
            return _delayStart.Task;
        }

        private void Run()
        {
            try
            {
                Run(this); // This blocks until the service is stopped.
                _delayStart.TrySetException(new InvalidOperationException("Stopped without starting"));
            }
            catch (Exception ex)
            {
                _delayStart.TrySetException(ex);
            }
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            Stop();
            return Task.CompletedTask;
        }

        // Called by base.Run when the service is ready to start.
        protected override void OnStart(string[] args)
        {
            _delayStart.TrySetResult(null);
            base.OnStart(args);
        }

        // Called by base.Stop. This may be called multiple times by service Stop, ApplicationStopping, and StopAsync.
        // That's OK because StopApplication uses a CancellationTokenSource and prevents any recursion.
        protected override void OnStop()
        {
            ApplicationLifetime.StopApplication();
            base.OnStop();
        }
    }
}

File ServiceBaseLifetimeHostExtensions.cs:

using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace AdvancedHost
{

    public static class ServiceBaseLifetimeHostExtensions
    {
        public static IHostBuilder UseServiceBaseLifetime(this IHostBuilder hostBuilder)
        {
            return hostBuilder.ConfigureServices((hostContext, services) => services.AddSingleton<IHostLifetime, ServiceBaseLifetime>());
        }

        public static Task RunAsServiceAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default)
        {
            return hostBuilder.UseServiceBaseLifetime().Build().RunAsync(cancellationToken);
        }
    }
}

In order to install, run or delete the service I use the 'sc' utility:

sc create AdvancedHost binPath="C:\temp\AdvancedHost\AdvancedHost.exe"

where AdvancedHost is the service name and the value for binPath is the compiled executable.

Once the service is created, to start:

sc start AdvancedHost

To stop:

sc stop AdvancedHost

And finally to delete (once stopped):

sc delete AdvancedHost

There are many more features contained in sc; just type 'sc' alone on the command line.

The results of sc can be seen in the services Windows control panel.

like image 147
Mark McWhirter Avatar answered Oct 23 '22 21:10

Mark McWhirter


You no longer need to copy-paste a lot of code to do it. All you need is to install the package Microsoft.Extensions.Hosting.WindowsServices

Then:

  • Append UseWindowsService() to the HostBuilder. This will also configure your application to use the EventLog logger.
  • Change the SDK in your project to Microsoft.NET.Sdk.Worker (<Project Sdk="Microsoft.NET.Sdk.Worker">).
  • Make sure that the output project type is EXE file (<OutputType>Exe</OutputType>)
  • Append <RuntimeIdentifier>win7-x64</RuntimeIdentifier> to the project file.

Debug your service like a regular console application, and then run dotnet publish, sc create ..., etc.

That's it. This also works for .NET Core 3.0/3.1. Read more here.

The minimal code example is shown below.

.csproj file:

<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <RuntimeIdentifier>win7-x64</RuntimeIdentifier>
    <TargetFramework>netcoreapp2.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="3.1.3" />
  </ItemGroup>

</Project>

File Program.cs:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace NetcoreWindowsService
{
    class Program
    {
        static void Main()
        {
            new HostBuilder()
                .ConfigureServices(services => services.AddHostedService<MyService>())
                .UseWindowsService()
                .Build()
                .Run();
        }
    }
}

File MyService.cs:

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;

namespace NetcoreWindowsService
{
    internal class MyService : IHostedService
    {
        private readonly ILogger<MyService> _logger;

        public MyService(ILogger<MyService> logger) => _logger = logger;

        public Task StartAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("The service has been started");
            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("The service has been stopped");
            return Task.CompletedTask;
        }
    }
}
like image 4
Vladimir Shiyanov Avatar answered Oct 23 '22 22:10

Vladimir Shiyanov