Creating Windows service and Linux daemon with the same code base in .NET

Cross-platform service with C# and .NET Framework

Few months back I wrote an article on how to create Linux daemon in .NET Core using generic host class introduced in 2.1 version of .NET Core. This code works just great on Linux OS when configured as a daemon in systemd and provides more less same concept you would have with Windows Service written in .NET Framework 4.x.

The code works just fine on both Windows and Linux hosts, but because it is compiled as a Console Application, you do not really get to run it on your Windows machine as a Service. You can do workarounds but you can then completely forget about using installutil.exe which is shipped with .NET Framework. Hooking up to the Service Start and Service Stop event also might get tricky.

So if you end up having to build a Linux daemon which also works as a Windows Service, you might want to add a middle layer library which is common service functionality and you just host it in a different project depending on your platform. This way your service logic is shared and you get to have platform specific executable depending on where you want to spin up the service.

Of course, there are some things you need to take care of in order to have working solution which will compile and eventually work on a live host.

Compatibility issue

In order to have a common library which will be referenced and consumed byt both .NET Core project (Linux host) and .NET Framework (Windows host), you need to create a .NET Standard class library project. Introducing .NET Standard project template made a small confusion for pretty much everyone who is building .NET applications.

The whole idea of .NET Standard projects is to resolve compatibly issue between .NET Core and .NET Framework. For example, you have some common stuff which are platform agnostic, like your business logic and this logic should be shared among components, but some of them are written in .net Core and some are .NET Framework projects. You cannon reference one to another, so your library has to be something that both .NET Core and .NET Framework can reference.

Dotnet Tomorrow

This is where .NET Standard gets into the game. All your common logic should be compiled as a .NET Standard class library and then referenced by your other projects. This will ensure cross-platform re-usability of your code. A really nice explanation of this can be found in Microsoft documentation at this URL https://blogs.msdn.microsoft.com/dotnet/2016/09/26/introducing-net-standard/

The common library component

As the whole point of having application for specific host is re-usability, we are going to develop first our common library which will be created as .NET Standard Class Library Project and then implement the common functionality for both Linux daemon and Windows service.

So essentially services on both platforms have something in common we want to be aware of and that is Start and Stop of the daemon/service. Since these are the common functionalities, we'll force them to be implemented in the concrete service implementation using the interface.

namespace Sample.Service.Standard
{
    public interface ICommonService
    {
        void OnStart();
        void OnStop();
    }
}
    

Some other things that will be definitely common for both of the applications will be configuration and logging. The best way is to inject concrete implementations of these two functionalities through the constructor of the class that implements ICommonService interface. Since we cannot do that purely using the interface, we'll add an abstract class which will force the concrete implementation class to have constructor with logging and configuration as parameters.

Before we write this abstract class, make sure you add the following NuGet packages:

Both of these NuGet packages are .NET Standard libraries and they can be used both for .NET Core and .NET Framework projects. Concrete implementations of the interfaces contaied in these two NuGet packages will be injected by the projects that will host common service concrete implementation class, so no logging or configuration logic is going to be implemented in this common library. 

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace Sample.Service.Standard
{
    public abstract class CommonServiceBase : ICommonService
    {
        private IConfiguration configuration;
        ILogger<CommonServiceBase> logger;

        public IConfiguration Configuration => this.configuration;
        public ILogger<CommonServiceBase> Logger => this.logger;


        public CommonServiceBase(
            IConfiguration configuration,
            ILogger<CommonServiceBase> logger)
        {
            this.configuration = configuration;
            this.logger = logger;
        }

        public abstract void OnStart();

        public abstract void OnStop();
    }
}
    

Now we are ready to write our shared service logic and use it in both .NET Core and .NET Framework. To keep things simple, I only write something to log once class is instatiated and on Start and Stop events.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace Sample.Service.Standard.Implementation
{
    public class CommonSampleService : CommonServiceBase
    {
        public CommonSampleService(IConfiguration configuration, ILogger<CommonSampleService> logger) : base(configuration, logger)
        {
            logger.LogInformation("Class instatiated");
        }

        public override void OnStart()
        {
           this.Logger.LogInformation("CommonSampleService OnStart");
        }

        public override void OnStop()
        {
            this.Logger.LogInformation("CommonSampleService OnStop");
        }
    }
}
    

We are now ready reference our .NET Standard class library in both .NET Core which will run as a daemon on Linux host and .NET Framework service application which will be installed and run on Windows host. Eventually our solution structure should look something like this

Crossplatform Service

Linux daemon project

This project will be created as .NET Core Console application and will be published targeting Linux platform. Since I mentioned I wrote on how to create and deploy Linux Daemon in .NET Core in this article I will not focus much on the approach and will just highlight simple implementation and configuration.

First we'll use generic host introduced in .NET Core 2.1 to instantiate our service host instance.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Sample.Service.Standard;
using System.Threading;
using System.Threading.Tasks;

namespace Sample.Service.Linux
{
    public class ServiceHost : IHostedService
    {
        IApplicationLifetime appLifetime;
        ILogger<ServiceHost> logger;
        IHostingEnvironment environment;
        IConfiguration configuration;
        ICommonService commonService;
        public ServiceHost(
            IConfiguration configuration,
            IHostingEnvironment environment,
            ILogger<ServiceHost> logger,
            IApplicationLifetime appLifetime,
            ICommonService commonService)
        {
            this.configuration = configuration;
            this.logger = logger;
            this.appLifetime = appLifetime;
            this.environment = environment;
            this.commonService = commonService;
        }


        public Task StartAsync(CancellationToken cancellationToken)
        {
            this.logger.LogInformation("StartAsync method called.");

            this.appLifetime.ApplicationStarted.Register(OnStarted);
            this.appLifetime.ApplicationStopping.Register(OnStopping);
            this.appLifetime.ApplicationStopped.Register(OnStopped);

            return Task.CompletedTask;
        }

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

        private void OnStarted()
        {           
            this.commonService.OnStart();
        }

        private void OnStopping()
        {
        }

        private void OnStopped()
        {
            this.commonService.OnStop();
        }
    }
}

    

You can notice that constructor of this class is not parameterless. These parameters will be injected from the entry point class using .NET Core build in dependency injection. Along with logger and configuration, we'll inject our common service interface ICommonService implementation which is our sample concrete class.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Sample.Service.Standard;
using Sample.Service.Standard.Implementation;
using Serilog;
using System.IO;

namespace Sample.Service.Linux
{
    class Program
    {
        static async System.Threading.Tasks.Task Main(string[] args)
        {
            IHost host = new HostBuilder()
                 .ConfigureHostConfiguration(configHost =>
                 {
                     configHost.SetBasePath(Directory.GetCurrentDirectory());
                     configHost.AddEnvironmentVariables(prefix: "ASPNETCORE_");
                     configHost.AddCommandLine(args);
                 })
                 .ConfigureAppConfiguration((hostContext, configApp) =>
                 {
                     configApp.SetBasePath(Directory.GetCurrentDirectory());
                     configApp.AddEnvironmentVariables(prefix: "ASPNETCORE_");
                     configApp.AddJsonFile($"appsettings.json", true);
                     configApp.AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", true);
                     configApp.AddCommandLine(args);
                 })
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddLogging();
                    services.AddHostedService<ServiceHost>();
                    services.AddSingleton(typeof(ICommonService), typeof(CommonSampleService));
                })
                .ConfigureLogging((hostContext, configLogging) =>
                {
                    configLogging.AddSerilog(new LoggerConfiguration()
                              .ReadFrom.Configuration(hostContext.Configuration)
                              .CreateLogger());
                    configLogging.AddConsole();
                    configLogging.AddDebug();
                })
                .Build();

            await host.RunAsync();
        }
    }
}

    

Now we just need to add some configuration for Serilog so we can actually spit out some files when we log messages from our sample code

{
  "Logging": {
    "PathFormat": "Logs/Sample.Service.Linux.{Date}.log",
    "LogLevel": {
      "Default": "Warning",
      "System": "Warning",
      "Microsoft": "Warning"
    }
  },
  "Serilog": {
    "MinimumLevel": "Debug",
    "WriteTo": [
      {
        "Name": "RollingFile",
        "Args": {
          "logDirectory": ".\\Logs",
          "fileSizeLimitBytes": 1024,
          "pathFormat": "Logs/Sample.Service.Linux.{Date}.log",
          "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}"
        }
      }
    ]
  }
}

    
Note

Make sure that you mark your appsettings.json file action as Copy Always or Copy if Newer as it is required fro Serilog to be configured and have log files written to the disk

Windows Service Project

Third part of the solution in Windows Service project which is using .NET Framework to compile and run. This makes it tightly coupled to Windows operating system and you can run this project only on a Windows machine.

For the simplicity I will use Service1 ServiceBase class implementation and call OnStart and OnStop methods of injected ICommonService interface implementation. This way our common library class will know when service start and service stop is invoked on the hosting Windows machine.

using System.ServiceProcess;
using Sample.Service.Standard;

namespace Sample.Service.Windows
{
    public partial class Service1 : ServiceBase
    {
        ICommonService commonService;

        public Service1(ICommonService commonService)
        {
            this.commonService = commonService;

            InitializeComponent();
        }

        internal void StartService(string[] args)
        {
            this.commonService.OnStart();
        }

        protected override void OnStart(string[] args)
        {
            this.StartService(args);
        }

        protected override void OnStop()
        {
            this.commonService.OnStop();
        }
    }
}

    

You can notice that service start logic is moved to void method StartService. This is to make service debugging easier from the Visual Studio without having to actually deploy service on Windows using installutil tool. More on this approach you can read from article Debugging Windows Service application with console in C#

Now to add dependency injection in our entry point class

using Sample.Service.Standard;
using Sample.Service.Standard.Implementation;
using System;
using System.Diagnostics;
using System.ServiceProcess;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using System.IO;
using Serilog;

namespace Sample.Service.Windows
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        /// 

        static void Main(string[] args)
        {
            #region Dependecy injection setup
            ServiceCollection services = new ServiceCollection();

            //Create configuration builder
            var configurationBuilder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json");


            //Inject configuration
            services.AddSingleton<IConfiguration>(provider =>
            {
                return configurationBuilder.Build();
            });

            //Inject Serilog
            services.AddLogging(options =>
           {
               options.AddSerilog(
                   new LoggerConfiguration()
                              .ReadFrom.Configuration(configurationBuilder.Build())
                              .CreateLogger()
                   );           
           });           
            
            //Inject common service
            services.AddSingleton(typeof(ICommonService), typeof(CommonSampleService));

            //Inject concrete implementaion of the service
            services.AddSingleton(typeof(ServiceBase), typeof(Service1));

            //Build DI provider
            ServiceProvider serviceProvider = services.BuildServiceProvider();


            #endregion

            if (Debugger.IsAttached)
            {
                //Console Debug mode

                var svc = serviceProvider.GetService<ServiceBase>() as Service1;
                svc.StartService(args);

                Console.ReadLine();
            }
            else
            {
                //Start service
                
                ServiceBase[] ServicesToRun;
                ServicesToRun = new ServiceBase[]
                {
                    serviceProvider.GetService<ServiceBase>()
                };
                ServiceBase.Run(ServicesToRun);
            }

           
        }
    }
}

    

I did not want to use separate config format for Serilog, so I just injected the same implementation and added appsettings.json file from the .NET Core project to .NET Framework Windows Service project and eventually ended up with two configuration files, one .NET Framework App.config XML file and one appsettings.jos file for Serilog only.

Ideally you should have only App.config XML file, but since this is not the focus of this article I'll just leave it as it is. Now to switch to dependency injection from the entry point Program.cs class file

using Sample.Service.Standard;
using Sample.Service.Standard.Implementation;
using System;
using System.Diagnostics;
using System.ServiceProcess;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using System.IO;
using Serilog;
namespace Sample.Service.Windows
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
/// static void Main(string[] args)
{
#region Dependecy injection setup
ServiceCollection services = new ServiceCollection();
//Create configuration builder
var configurationBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
//Inject configuration
services.AddSingleton<IConfiguration>(provider =>
{
return configurationBuilder.Build();
});
//Inject Serilog
services.AddLogging(options =>
{
options.AddSerilog(
new LoggerConfiguration()
.ReadFrom.Configuration(configurationBuilder.Build())
.CreateLogger()
); }); //Inject common service
services.AddSingleton(typeof(ICommonService), typeof(CommonSampleService));
//Inject concrete implementaion of the service
services.AddSingleton(typeof(ServiceBase), typeof(Service1));
//Build DI provider
ServiceProvider serviceProvider = services.BuildServiceProvider();
#endregion
if (Debugger.IsAttached)
{
//Console Debug mode
var svc = serviceProvider.GetService<ServiceBase>() as Service1;
svc.StartService(args);
Console.ReadLine();
}
else
{
          //Start service
                
                ServiceBase[] ServicesToRun;
                ServicesToRun = new ServiceBase[]
                {
                    serviceProvider.GetService<ServiceBase>()
                };
                ServiceBase.Run(ServicesToRun);
            }

           
        }
    }
}

    

Our Windows Service is ready to be compiled and installed on Windows machine using installutil tool and we'll be able to manage it from Windows Administrative Tools/Services UI. The complete solution code can be found on Github in the following repository https://github.com/dejanstojanovic/dotnetcore-windows-linux-service

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform includion ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article