.Net core console app as a Windows service

Alfus Jaganathan
Cloud Solutions Architect
March 23, 2019
Rate this article
Views    22373

Ever wonder if we can run a .NET core console application as a windows service? This read can help you understand some techniques around achieving it.

Disclaimer: This solution is not tested in Production

Now, there may be questions popping up in our mind like… What are the benefits doing this? Why do we need a .NET core app to run as a windows service? /etc.

So I thought of putting together couple of cases where this solution could help us.

  • Build the application in .NET core (considering benefits of .NET core)
  • Use dependency injection across the application
  • Build the application to perform some scheduled operations (without using a scheduler)
  • Run the application as a console/task (without a top shelf) and run in cloud platforms and external schedulers such as PCF(PAS/W) and PCF Scheduler; for more information on PCF scheduler and running console apps as Tasks, please refer here (PCF Scheduler, Scheduling Jobs, PCF Tasks, .NET Core console Apps)
  • Achieve all the above without changing my code every time

When I started looking around, I came across this Microsoft documentation which unfortunately, tells me how to host an ASP.NET Core application in a windows service, but nothing on running a .NET core console as a windows service. But the doc really helped in understanding some basics around it. For e.g. modifications to .csproj file, Program.cs file, publishing, etc. We will look into this one by one here.

Basically we all know that windows service applications are console applications containing a top shelf (ServiceBase). In other words, if we remove the top shelf from a windows service application, it will be a normal console application. So, the hint we get from here is, if we build a .NET Core console application, we need to introduce a top shelf so that we can run as a windows service. With that in mind we shall see the step by step approach to build the application.

Pre-requisites

  • Visual Studio 2017 or higher OR Visual Studio Code
  • .NET Core SDK

Step1: Building a bare bone .NET Core console application Please see below (using the dotnet new command)

dotnet new console -o mycorewindowsservice

.NET Core SDK will take care of creating a bare bone console application under the folder mycorewindowsservice

Step2: Create your processor which is going to do the real job In this case, for sample purpose we can create a simple class which will simply create files with current datetime as filename in a specified folder. My class looks something like this.

public class FileCreateProcessor : IProcessor
    {
        private readonly IConfiguration configuration;
        private readonly ILogger<FileCreateProcessor> logger;

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

        public void Execute()
        {
            var outputPath = configuration["OutputFilePath"];
            Directory.CreateDirectory(outputPath);
            var currentDateTime = DateTime.Now.ToString("MM-dd-yyyy-hh-mm-ss-tt");
            var fileName = Path.Combine(outputPath, $"{currentDateTime}.txt");
            logger.LogInformation($"Creating file {fileName}");
            File.WriteAllText(fileName, currentDateTime);        }
    }

You can see here that I am using IConfiguration and ILogger, which are services I am going to inject later using DI

Step3: Bring in Generic HostBuilder so that we can get the benefits of configuring services, configuring logging, configuring app configuration all using .NET Core standard extension methods. Please see the code below as in our sample application.

public static void Main(string[] args)
        {
            var hostBuilder = new HostBuilder()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .ConfigureAppConfiguration((hostingContext, config) =>
                {
                    config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
                    config.AddEnvironmentVariables();
                    config.AddCommandLine(args);
                })
                .ConfigureServices((hostContext, services) =>
                {
                    //Inject additional services as needed
                })
                .ConfigureLogging((hostingContext, logging) =>
                {
                    logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
                    logging.AddConsole();
                    logging.AddDebug();
                    logging.AddEventLog();
                });
        }

For configuration, we have added 4 configuration sources (from appsettings.json, environment variables and command line args). For this, we have added the below packages

<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="2.2.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.2.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" />

For logging configuration, we are adding an additional logging provider called event log in addition to console and debug, as we are planning to run as a windows service. Here are the packages we are have to add.

<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.2.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="2.2.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.2.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.2.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.EventLog" Version="2.2.0" />

Step4: Now into the interesting part, we are going to add the top shelf part into this console application. For this, first we need to follow the doc from Microsoft, then had to copy some sample code from Microsoft GitHub.

Modify .csproj file as below, basically making the application self contained and targeting to windows runtime. If you want to know more about RID‘s, please refer here.

<PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <RuntimeIdentifier>win7-x64</RuntimeIdentifier>
    <IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
</PropertyGroup>

Add the below nuget package to support ServiceController

<PackageReference Include="System.ServiceProcess.ServiceController" Version="4.5.0" />

Copy ServiceLifetime.cs from Microsoft’s hosting sample and paste into the root folder and modify the namespace accordingly.

Now go back to the doc and refer to section Program.Main updates. Based on that, we are going to modify our sample program’s main method as below.

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

            if (isService)
            {
                var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
                var pathToContentRoot = Path.GetDirectoryName(pathToExe);
                Directory.SetCurrentDirectory(pathToContentRoot);
            }

            var hostBuilder = new HostBuilder()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .ConfigureAppConfiguration((hostingContext, config) =>
                {
                    …
                })
                .ConfigureServices((hostContext, services) =>
                {
                    …
                })
                .ConfigureLogging((hostingContext, logging) =>
                {
                    …
                });

            if (isService)
                await hostBuilder.RunAsServiceAsync();
            else
                await hostBuilder.RunConsoleAsync();
        }
    }

Here, we have added flag isService to determine whether the application is running as a console or a windows service. If it is a service, we have to set the current directory, as described in the doc. Next thing we do here is running the application as Console or Service based on the flag.

Note: RunAsServiceAsync is the extension method from the code we copied from the sample.

Step5: Now we are almost at the end state where we need to build our service and inject into ServiceCollection as a hosted service. For this create a class called MyServiceHost as below, which basically executes the processor based on the given processing interval. THe interesting part to be noted here is, all the dependencies (IProcessor, IConfiguration, ILogger) are injected in runtime via dependency injection.

public class MyServiceHost : IHostedService, IDisposable
    {
        private Timer paymentProcessorTimer;
        private readonly IProcessor processor;
        private readonly IConfiguration configuration;
        private readonly ILogger<MyServiceHost> logger;

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

        public Task StartAsync(CancellationToken cancellationToken)
        {
            logger.LogInformation("Service started");
            var processingIntervalInSeconds = Convert.ToDouble(configuration["BatchProcessingIntervalInSeconds"]);

            paymentProcessorTimer = new Timer((e) => { processor.Execute(); }, null, TimeSpan.Zero,
                TimeSpan.FromSeconds(processingIntervalInSeconds));

            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            logger.LogInformation("Service stopped");
            paymentProcessorTimer?.Change(Timeout.Infinite, 0);
            return Task.CompletedTask;
        }

        public void Dispose()
        {
            paymentProcessorTimer?.Dispose();
        }
    }

Now that we have created the ServiceHost, we have to inject it as a hosted service under ConfigureServices action method. We should also inject its custom dependencies here, for e.g. IProcessor in our sample, as below.

.UseContentRoot(Directory.GetCurrentDirectory())
                .ConfigureAppConfiguration((hostingContext, config) =>
                {
                    …
                })
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddScoped<IProcessor, FileCreateProcessor>();
                    services.AddHostedService<MyServiceHost>();
                })
                .ConfigureLogging((hostingContext, logging) =>
                {
                    …
                });

I hope we have made all the code changes necessary for the application, except creation of the configuration file appsettings.json. So we shall create the file under the project root and apply the below configuration settings.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information"
    }
  },
  "BatchProcessingIntervalInSeconds": "20",
  "OutputFilePath": "c:\mysamplecorewindowsservice_output"
}

Step6: Publish and running the application. Use the below dotnet publish command to build and publish the artifacts. Once we publish the application, we can either run the application as a console or install it as a Windows Service.

dotnet publish --configuration Release -o c:mycorewindowsservice

Here, option -o refers to the publish directory.

To install the windows service, you can use sc.exe. For more details, please refer here. Please find some sample commands below.

sc create corewinservice binPath= "c:mycorewinserviceapp"
sc start corewinservice
sc stop corewinservice
sc delete corewinservice

Hope you had some fun coding! You can also refer to the sample application here

Please comment and let me know if this article helps you. Also please feel free to let me know your thoughts or suggestions or any other alternative solutions you come across.

Subscribe To Our Newsletter
Loading

Leave a comment