How to create and install a .NET Core Windows Service

Windows Services offer a robust mechanism for hosting long-running background applications on Windows devices. These services are usually configured to start with the operating system and can be stopped and started on demand. They are ideal for processing background tasks as they do not show a user interface and therefore do not interfere with what the user is doing.

In the past, Windows Services could be developed using the .NET Framework by creating a class that inherits from the ServiceBase class. These services could then be installed via the InstallUtil.exe (Installer Tool).

Since the advent of .NET Core, Windows Services are now developed by creating a ‘Worker Service’ class that inherits from the BackgroundService class. The project containing the Worker Service is then configured to run as a Windows Service via the Microsoft.Extensions.Hosting.WindowsServices NuGet package. The sc command-line program can be used to install services that are developed in this new way.

In this article, I will walk you through the process of creating a Worker Service project and show you how to wire this up to run as a Windows Service.

Creating a Worker Service project

We need to create a Worker Service project before we can get into wiring an application up to run as a Windows Service.

Prerequisites

Before we begin, make sure you have installed Visual Studio. I will be using Visual Studio 2022 and .NET 6.0.

You will need to have selected the ‘.NET desktop development’ workload within the Visual Studio Installer interface to set up the necessary project templates.

This article assumes that you are running a modern Windows operating system such as Windows 10/11.

Project creation

To get started, open Visual Studio and wait for the launch window to load. From the launch window, you will have the option to open an existing project or create a new one, as shown below.

Visual Studio – Launch window
Visual Studio – Launch window

From the launch window, press the ‘Create a new project’ button to load the available project templates.

Visual Studio – Create a new project
Visual Studio – Create a new project

On the ‘Create a new project’ page, type the text ‘worker’ into the search control and then select the C# ‘Worker Service’ item from the list. Press the ‘Next’ button to proceed to the next step.

Visual Studio – Configure your new project
Visual Studio – Configure your new project

On the ‘Configure your new project’ page, enter a sensible name for the project. I’ve chosen the name ‘TodosService’ for the purposes of this walkthrough. Press the ‘Next’ button to proceed to the next step.

Visual Studio - Additional information
Visual Studio – Additional information

On the ‘Additional information’ page, I recommend that you leave the default values and press the ‘Create’ button to complete the setup of the new project.

Default Worker Service template

Now that the project has been created from the Worker Service template, let’s explore the main files from the default project that has been generated.

Program file

Aside from ‘appsettings.json‘ and ‘appsettings.Development.json’, there are two C# files contained within the project; namely ‘Program.cs’ and ‘Worker.cs’.

The content of the generated ‘Program.cs’ file is listed below for reference.

using TodosService;
 
IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddHostedService<Worker>();
    })
    .Build();
 
await host.RunAsync();

As this is a .NET 6.0 project, it can avail of top-level statements, meaning that there is no visible class definition and no Main method.

The static Host.CreateDefaultBuilder method call is used to set up an application host with sensible defaults for things like configuration and logging.

The ConfigureServices method is used to add services to the built-in dependency injection container. The AddHostedService method is an extension method on the IServiceCollection interface that adds the specified IHostedService to the container as a singleton.

The Build method initialises and returns a Host object.

Finally, the RunAsync method is called to run the application.

Worker class

So what happens after the RunAsync method is called?

This is where the Worker class comes in. Since the Worker class has been registered as a hosted service, its ExecuteAsync method will be called automatically after the host starts running. When debugging, the project will run as a Console application.

The Worker class code is listed below for reference.

namespace TodosService
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;
 
        public Worker(ILogger<Worker> logger)
        {
            _logger = logger;
        }
 
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
                await Task.Delay(1000, stoppingToken);
            }
        }
    }
}

The Worker class inherits from the BackgroundService class, which in turn implements the IHostedService interface.

The Worker class constructor accepts an instance of ILogger which will be automatically injected from the dependency injection container after the host starts running.

As stated previously, the ExecuteAsync method will be called automatically after the host starts. The method body contains a while loop that will continue executing until cancellation is requested via the CancellationToken which is automatically passed into the method as an argument.

The code in the default project code simply logs an informational message and then calls Task.Delay with a one-second delay value before proceeding to the next iteration of the loop.

Windows Service hosting

Now that we’ve reviewed the default project template code, let’s look at how we can configure the project to be hosted as a Windows Service. To do this, we’ll need to update and install some NuGet packages.

The default project comes with a specific version of the Microsoft.Extensions.Hosting NuGet package. You’ll need to make sure that this package is up to date before proceeding further.

You’ll then need to install the latest stable version of the Microsoft.Extensions.Hosting.WindowsServices NuGet package.

Next, update the contents of the ‘Program.cs’ file to include a call to the UseWindowsService extension method as shown in the following code listing.

using TodosService;
 
// Initialise the hosting environment.
IHost host = Host.CreateDefaultBuilder(args)
    .UseWindowsService(options =>
    {
        // Configure the Windows Service Name.
        options.ServiceName = "TodosService";
    })
    .ConfigureServices(services =>
    {
        // Register the primary worker service.
        services.AddHostedService<Worker>();
 
        // Register other services here...
    })
    .Build();
 
// Run the application.
await host.RunAsync();

The UseWindowsService method wires up the code that is necessary for the application to run as a Windows Service. Within the body of the action that is used to configure the WindowsServiceLifetimeOptions, the name that should be assigned to the Windows Service when it is installed is set.

Aside from installation, that’s all that is required to run the application as a Windows Service!

BackgroundService methods

Although we’ve already done everything that is needed to run our application as a Windows Service, let’s update the default ExecuteAsync method implementation and look at some of the other lifetime methods from the BackgroundService class that we can override.

namespace TodosService
{
    /// <summary>
    /// The main Worker Service.
    /// </summary>
    public class Worker : BackgroundService
    {
        #region Readonlys
 
        private readonly ILogger<Worker> _logger;
 
        #endregion
 
        #region Constructor
 
        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="logger"><see cref="ILogger"/></param>
        public Worker(ILogger<Worker> logger)
        {
            _logger = logger;
        }
 
        #endregion
 
        #region Methods
 
        /// <summary>
        /// Executes when the service has started.
        /// </summary>
        /// <param name="stoppingToken"><see cref="CancellationToken"/></param>
        /// <returns><see cref="Task"/></returns>
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            try
            {
                _logger.LogInformation("** SERVICE STARTED **");

var httpClient = new HttpClient();

int id = 1;                 while (!stoppingToken.IsCancellationRequested)                 {                     _logger.LogInformation("Fetching todo {id} of 200...", id);

                    var todosJson = await httpClient.GetStringAsync(
                       $"https://jsonplaceholder.typicode.com/todos/{id++}");

_logger.LogInformation(todosJson);

if (id > 200)
{
id = 1;
}
                    await Task.Delay(1000, stoppingToken);                 }             }             finally             {                 _logger.LogInformation("** SERVICE STOPPED **");             }         }         /// <summary>         /// Executes when the service is ready to start.         /// </summary>         /// <param name="cancellationToken"><see cref="CancellationToken"/></param>         /// <returns><see cref="Task"/></returns>         public override Task StartAsync(CancellationToken cancellationToken)         {             _logger.LogInformation("Starting service");             return base.StartAsync(cancellationToken);         }         /// <summary>         /// Executes when the service is performing a graceful shutdown.         /// </summary>         /// <param name="cancellationToken"><see cref="CancellationToken"/></param>         /// <returns><see cref="Task"/></returns>         public override Task StopAsync(CancellationToken cancellationToken)         {             _logger.LogInformation("Stopping service");             return base.StopAsync(cancellationToken);         }         #endregion     } }

In the above code, the Worker class has been updated with a new implementation of the ExecuteAsync method and the StartAsync and StopAsync methods have been overridden.

The StartAsync method is called when the application host is ready to start, whereas the StopAsync method is called when the application host is performing a graceful shutdown. It isn’t necessary to override either of these methods, but I find it helpful to log that the methods are being called.

Within the ExecuteAsync method, a HttpClient is used to fetch and display the JSON output of HTTP requests to the JSONPlaceholder API using the GetStringAsync method. The ‘todos’ endpoint returns up to 200 todo records, so there is logic to reset the id variable back to 1 after the limit of 200 has been reached.

Of course, you can implement whatever logic you like within the ExecuteAsync method, and you don’t necessarily need to have a while loop. For example, you could instead have code that opens a gRPC channel and awaits streamed messages from a gRPC server, or you could kick off a background timer that performs work on a separate thread.

Windows Service installation

For Windows Services that are developed using the .NET Framework, the InstallUtil.exe (Installer Tool) can be used for installation purposes.

However, for .NET Core Worker Services that are intended to run as Windows Services, we need to use the standard sc command-line program instead.

After you have published your Worker Service project to a folder, you can register it as a Windows Service using an sc command that is similar to the following.

sc create TodosService binPath= "<path_to_publish_directory>\TodosService.exe" DisplayName= "Todos Service" obj= LocalService start= auto

The above command will register a Windows Service named ‘TodosService’, with the ‘binPath’ pointing to the main program executable file of the published Worker Service project.

The ‘DisplayName’ parameter is used to set the Name that will be displayed within the Windows Services Manager.

The ‘obj’ parameter sets the user account that the service should run as. This could be set to ‘LocalService’, ‘NetworkService’, or ‘LocalSystem’ depending on the required permission level and security requirements.

The ‘start’ parameter is used to configure the Startup Type for the service.

Note that the spaces between the equals (=) and double-quote (“) characters is intentional as they allow the above command to be compatible with both modern Windows 10/11 operating systems as well as older versions such as Windows 7 which require the spaces.

If you want to configure the Description that appears within the Windows Services Manager, this needs to be configured using a separate command, as follows.

sc description TodosService "Fetches Todo items in a continuous loop."

The above command sets an appropriate Description for the Windows Service named ‘TodosService’.

Summary

In this article, I provided step-by-step instructions on how to create a .NET Core Worker Service project and I walked through the code that is included in the default Worker Service template project.

I then explained how to configure the Worker Service project to run as a Windows Service and updated the default implementation to provide you with some ideas on what you can use Windows Services for. I also made you aware of the additional BackgroundService lifetime methods that can be overridden.

Finally, I included examples of how to register your application as a Windows Service using the sc command-line program and how to configure common Windows Service attributes such as Display Name, Startup Type, and Description.


I hope you enjoyed this post! Comments are always welcome and I respond to all questions.

If you like my content and it helped you out, please check out the button below 🙂

Comments

Ed Holguin

Hello Jonathan! Thank you for the great and simple write up on this. I followed everything just fine but I’m stumped when installing. I’m getting an error “The account name is invalid or does not exist, or the password is invalid for the account name specified.” Is there something that needs to be done for “LocalService” to work correctly? Thanks in advance for any help you can provide.

July 25, 2023

Jonathan Crozier

Hi Ed,

Thanks for the comment!

What operating system are you trying to install the Windows Service on?

Try updating the ‘obj’ parameter as follows.

obj= "NT AUTHORITY\LocalService"

July 25, 2023