Creating a Windows Service with .NET 6

Windows services are programs that are automatically started when the system starts up, or when the user logs in. They run in the background and can run with a different account than the logged-in user. .NET makes it easy to create Windows services or Linux daemons as shown in this article.

Services for Windows

Intro

Instead of creating Windows services and Linux daemons, nowadays you might think about using Docker instead. Docker orchestrators can be used to monitor and scale containers. Before creating a Windows service, you might think about this alternative. However, there are still many scenarios where Windows services are of great use.

What are some of the scenarios where Windows services are used? Looking at the services running with Windows, (start the Services app), services to update applications and the operating system (e.g. Windows Update, Microsoft Edge Update Service, Mozilla Maintenance Service, Google Update Service, AdobeUpdateService), services to store credentials for users and applications (Credential Manager), services to offer geolocation information for applications (Geolocation Service), malware checkers (Microsoft Defender Antivirus Service), services to deliver sensor data (Sensor Service), and many more. Not every of these services is implemented as a separate application – some of the services use the same executable. Looking at the properties of a service you can check the path for the executable including its command-line arguments. Many services that are part of Windows make use of the svchost.exe, the Geolocation service invokes this with the -k netsvcs -p options.

Windows Services

Services can be configured to automatically start when the operating system starts up (startup type Automatic). The Automatic Delayed start option allows the user to login before the service is started. Delayed started services are started after the Automatic started service. With the Manual configuration, the service starts up based on an event – e.g. a domain is joined, a firewall port is opened, a group policy is changed, or a custom event based on Event Tracing for Windows (ETW) is fired.

Create a Worker

Let’s start creating a Windows service by creating a background worker. With .NET 6, a background worker can be created using Visual Studio or the dotnet CLI command dotnet new worker.

The top-level statements created with this application use the Host class. The method CreateDefaultBuilder contains functionality to setup the dependency injection container, configuration, and logging. The dependency injection container managed by the Host class is configured by invoking the method ConfigureServices. In the generated code, the extension method AddHostedService is used to register a background class that implements the interface IHostedService. This interface is indirectly implemented by the Worker class by deriving from the base class BackgroundService. The interface IHostedService defines the methods StartAsync and StopAsync. Adding a hosted service, invoking the Run method of the host starts the host and in turn invokes the startup of the IHostedService.

Worker Service - Host configuration

The Worker class derives from the class BackgroundService. BackgroundService implements the interface IHostedService and defines the abstract method ExecuteAsync. This abstract method is called by the StartAsync method in the BackgroundService. StartAsync is defined by the IHostedService interface. With the implementation of the Worker class, ExecuteAsync uses an endless loop (until cancellation is requested) and writes a log message once a second.

Worker

The main functionality for the Host class is creating the dependency injection container, configuration, and logging. Using CreateDefaultBuilder, configuration is read from the configuration files appsettings.json, appsettings.{env.EnvironmentName}.json, environmental variables, and the command line.

Logging configuration is read from the section Logging within the configuration settings. Using the worker template, the configuration file appsettings.json defines logging based on the log level. The default configuration is set to Information:

Logging configuration

If the application runs on a Windows system, the method CreateDefaultBuilder also adds logging to the Windows event log and sets a filter provider to only log warnings and more critical issues to this provider.

Running the application, log information is written to the console. The worker writes a message every second.

info: SimpleWorkerService.Worker[0]
      Worker running at: 03/17/2022 10:45:55 +01:00
info: SimpleWorkerService.Worker[0]
      Worker running at: 03/17/2022 10:45:56 +01:00
info: SimpleWorkerService.Worker[0]
      Worker running at: 03/17/2022 10:45:57 +01:00
info: SimpleWorkerService.Worker[0]
      Worker running at: 03/17/2022 10:45:58 +01:00
info: SimpleWorkerService.Worker[0]
      Worker running at: 03/17/2022 10:45:59 +01:00

Convert to a Windows Service

To build a Windows Service, you just need to add the NuGet package Microsoft.Extensions.Hosting.WindowsServices, and add the method invocation UseWindowsService to the IHostBuilder fluent API:

DI Config

To see information level logging in the Windows event log, the filter is explicitly applied with the ConfigureLogging method used with the host builder. The UseWindowsService method configures the source name the same as the application name. This information is overwritten configuring the EventLogSettings. In addition to setting the SourceName property of the EventLogSettings, the LogName is set which creates a separate category for logging shown in the Event Viewer.

Because the EventLogLoggerProvider and the EventLogSettings classes are only available on Windows, the OperatingSystem class is used to check if the application is running on Windows before this API is invoked. In case your application is not used to run on Linux, you can use the SupportedOSPlatform attribute instead. You can also specify <TargetPlatform> and specify a Windows Target Framework Moniker.

Installing and Managing the Windows Service

After building the application, the new Windows Service can be published using dotnet publish (or by using Visual Studio):

dotnet publish -c Release -o c:\sampleservice

To control Windows Services, the sc command can be used. Creating a new Windows Service is done using sc create passing the name of the service and the binPath parameter referencing the executable. This command requires administrator rights:

sc create "Sample Service" binPath= c:\sampleservice\SimpleWorkerService.exe

Using sc create, you can configure the account with which the service should run (the default is LocalSystem), services which are required to be started before this service (depend), and more.

The status of the service can be queried using the Services MMC, or with the command line sc query:

sc query "Sample Service"

After the service is created, it is stopped and need to be started:

sc start "Sample Service"

To stop and delete the service, the sc stop and sc delete commands can be used.

After starting the service, log information can be seen with the Windows Event Viewer. Because the LogName property was set, there’s a separate category with Application and Services Logs:

Windows Event Viewer Log

Passing Arguments

With a Windows Service, it’s possible to pass command-line arguments with the service configuration. To read the arguments, the Environment class can be used as shown in the following code snippet.

Reading Command-Line Arguments

When creating the service using sc create, pay attention to leave a blank after the binPath option. You can supply the parameters within the quotes:

sc create "Sample Service" binPath= "c:\sampleservice\SimpleWorkerService.exe --p1=one --p2=two"

Web Application as Windows Service

What about hosting Kestrel as a Windows Service? There’s not a lot difference using the package Microsoft.Extensions.Hosting.WindowsServices – in principle just the API UseWindowsService needs to be invoked. Let’s get into details using the .NET 6 WebApplicationBuilder class.

A Web API project can be created using dotnet new webapi. This template creates an API returning random weather information. With the option –use-minimal-apis, controllers are not used, and the complete functionality of the API can be defined with top-level statements. The parameter –no-https specifies to create an implementation with HTTP. Using HTTPS, it’s necessary to create and configure a certificate that’s used by the account running the service. Depending on the scenario how you use this Windows service, HTTP can be ok.

dotnet new webapi --use-minimal-apis --no-https -o ASPNETCoreWindowsService

The UseWindowsService method is an extension method for IHostBuilder. With .NET 6, WebApplication and WebApplicationBuilder are used instead of the Host and HostBuilder classes. Of course, you can also change the code to the old .NET 5 version. WebApplication offers an abstraction layer of the Host class and makes it easier to configure ASP.NET Core middleware. With .NET 5, the Startup class has been used. Instead of using the Startup class now everything can be done with top-level statements. The Minimal API makes use of C# 10 features and adds some APIs, e.g. a new overload of the MapGet method. Using the WebApplicationBuilder class, the functionality of the IHostBuilder can be accessed using the Host property. This property returns a ConfigureHostBuilder instance which implements the interface IHostBuilder. Here you can use the extension method UseWindowsService like before. The UseWindowsService extension method configures the content root path to AppContext.BaseDirectory for the Windows service. Because the CreateBuilder method already needs this directory, this directory needs to be specified with the WebApplicationOptions as shown. To specify a log category with the Windows event logs, the EventLogSettings are configured as before with the console application:

Windows Services with ASP.NET Core

The Kestrel server can be configured accessing the WebHost property of the WebApplicationBuilder, invoking the method ConfigureKestrel. With the sample application, the Kestrel server is configured using appsettings.json:

Kestrel Configuration

Now the service can be build, published, and configured as a Windows Service in the same way as mentioned before using the worker application. Opening a browser to reference the configured port with the controller route WeatherForecast returns JSON information from the API service:

http://localhost:9200/weatherforecast

Weather Forecast

Accessing the Windows Service from a different system, the Firewall needs to be configured to allow accessing this port from the outside.

Linux Daemons

What about running this application on Linux? The method UseWindowsService checks if it’s running on Windows as a Windows service, and returns if this is not the case. With this you can run the application on the Linux system as well. To create a Linux daemon, you can add the NuGet package Microsoft.Extensions.Hosting.Systemd, and invoke the method UseSystemd. What’s different is the configuration of systemd. To use systemd with WSL-2, you can use Distrod to run your Ubuntu environment. See a link below for Distrod.

Take away

Starting with .NET 3, the Host class was introduced which abstracts configuration for logging, dependency injecction, and configuration in one place. Extension methods make it easy to offer more features. With this, using the NuGet package Microsoft.Extensions.Hosting.WindowsServices just one API method is required to create a Windows Service. This way, background functionalty based on the worker template, but also hosting a Kestrel server for offering ASP.NET Core Web applications and services is an easy task.

If you like this article, it would be great if you buy a coffee:

Buy Me A Coffee

6 thoughts on “Creating a Windows Service with .NET 6

  1. Mr. Nagel, any idea if there is a way of getting the Start Parameters? That is, if you go to the Services control panel, double-click the service entry, stop the service, enter something into the Start Parameters field, and then restart the service?

    I would expect to be able to get them either from `args` directly or from `HostBuilderContext.Configuration`. Alas, they don’t show up anywhere I can find to look.

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.