.Net core console app as a Windows service
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.
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.
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.
Leave a comment