This is your brain. This is your brain on computers.

Introduction to .NET Core Worker Services

What are .NET Core Worker Services?

Starting with .NET Core SDK 3.0+, a new template has been introduced. It’s named, for better or worse, “.NET Core Worker Service.” This template gives you the ability to create cross-platform background services such as long-running Windows Services or daemons hosted in Linux using something like systemd. Since these services are basically console applications, you could also host a console application in a Worker Service container to be hosted as a microservice.

Until now, there hasn’t been a Microsoft-built approach to long-running background tasks other than using ancient Windows Services projects using the full .NET Framework. Those are notoriously difficult to debug, test, and execute since you’re required to use esoteric console commands for installation, start, stop, and uninstallation. Products like TopShelf have lessened the pain, but they are ultimately bandages on top of an annoying framework.

With .NET Core Worker services, you’re now able to use Microsoft’s approved and open sourced (GitHub hosted) approach to background tasks as a real replacement to Windows Services. It’s fully up to date with .NET Standard 2.0 and can be hosted in Linux with minimal changes to your project.

Here are just a few benefits you get by using this template:

Dependency injection. The template comes with a default container using Microsoft’s dependency injection container which debuted with ASP.NET Core. While you can certainly do this manually in normal console applications, the boilerplate is difficult to remember. Below is an example of what you’d get as of this article’s publication. WorkerService is the class that inherits from BackgroundService to perform the long-running activities. See more about this in the rest of the article.

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureServices((hostContext, services) =>
        {
            services.AddHostedService<WorkerService>();
        });

Configuration. The same configuration mechanisms that make ASP.NET Core flexible are available in Worker Services. That means you can access appsettings.json, user secrets, environment variables, and command line arguments using the IConfiguration interface. This is a lot better than the app.config files of the full .NET Framework in which you are required to take a dependency on System.Configuration which definitely wasn’t cross-platform.

Logging. Like the configuration section, logging is also inherited from the ASP.NET Core paradigm. By default, Microsoft’s .NET Standard version of its logging framework gives you access to Console, Debug, EventSource, and EventLog (on Windows). With dependency injection and third party extensions, you can expand or create your own logging providers. It’s simple to start logging to files, databases, generic streams, or even cloud providers like Splunk or Elastic.

Service lifetime. While not specific to the template, it does at least introduce you to the new BackgroundService class from which your Worker Service should inherit. Tying into this class gives you access to the service lifetime through Execute, Start, and Stop methods. The long running activity should be done in the Execute phase and should not block. The differences between these methods are discussed more below.

Note: This template uses a different approach to the already existing hosted services concepts that were introduced as part of the ASP.NET Core updates. Don’t get Worker Services confused with Hosted Services. Microsoft tends to use the terminology interchangeably in their documentation, but you will get confused if you don’t logically separate them. Worker Services apply to the “console application”-like project template which allows you to create background system services which have nothing to do with ASP.NET Core. Hosted Services apply to background tasks which can be hosted in a web server runtime as part of an ASP.NET Core application. Both use cases end up using the IHostedService interface, but differ regarding the applications under which they execute.

Set Up .NET Core SDK

Start by downloading the .NET Core SDK. As of this writing, the current version is 3.1.201. Microsoft regularly updates this, so make sure to check back and get the latest updates.

Microsoft website to download .NET Core SDK.

After download and installation, make sure it’s setup properly by opening your favorite command line tool (bash, mingw, powershell) and use the dotnet --version command to see which version is installed.

Create Project

The Worker Service template described in this article is not exclusive to Visual Studio 2019. You can either use the UI to create projects in Visual Studio 2019 or use the dotnet new worker command from the .NET Core command line to create a project if you prefer using CLI + Visual Studio Code.

Further Reading: Not sure how to use Visual Studio 2019 but want to learn? Check out our ultimate guide for people in exactly your position!

Visual Studio 2019. Make sure you’re using the latest version (16.5.4) as of this writing. In the new project dialog, search for “Worker Service” and choose that template to create the project.

Visual Studio Code. From the CLI, type dotnet new worker -o MyWorkerService to create a new Worker Service in the MyWorkerService folder.

Regardless of how you create the project, this is what you should see in the case of success.

  • appsettings.json
  • appsettings.Development.json
  • MyWorkerService.csproj
  • Program.cs
  • Worker.cs

In Program.cs, you’ll see what we introduced above where the dependency injection container is being created followed by adding the default Worker class as a Hosted Service. Remember that the “Hosted Service” nomenclature is used interchangeably by Microsoft in certain contexts. Don’t confuse it with ASP.NET Core Hosted Services.

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHostedService<Worker>();
            });
}

In Worker.cs, you’ll see a new class that inherits from BackgroundService that we mentioned before with an overridden ExecuteAsync method. It starts by doing nothing other than logging the current time every 1 second until a cancellation has been requested. Your long-running process should always end when a cancellation is requested and should always asynchronously await whatever is causing the process to be long-running. In this case, it’s a Task.Delay statement, but in your use case it might be a database update or an HTTP request. Whatever it is, it must return a task so that the service lifetime container can continue.

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);
        }
    }
}

Start vs Stop vs Execute

The BackgroundService class comes with three methods that can be overridden: StartAsync, StopAsync, and ExecuteAsync. The differences between them can be quite confusing because the documentation isn’t exactly clear (nor are the comments on the classes themselves). Fortunately, the code is on GitHub so we can find out what it’s doing!

StartAsync. Virtual method which is called when the service is starting up. If you override this method, you’ll be responsible for bootstrapping any of your one-time resources. In addition, you’ll need to make sure you call base.StartAsync after setting up your resources so that the base class can call ExecuteAsync and finish starting up the services as expected. See the code below for how the base class starts execution of the service.

public virtual Task StartAsync(CancellationToken cancellationToken)
{
    // Store the task we're executing
    _executingTask = ExecuteAsync(_stoppingCts.Token);

    // If the task is completed then return it, this will bubble cancellation and failure to the caller
    if (_executingTask.IsCompleted)
    {
        return _executingTask;
    }

    // Otherwise it's running
    return Task.CompletedTask;
}

StopAsync. Virtual method which is called when the application is shutting down. If you override this method, you should dispose of any resources, objects, or still running tasks. In addition, you’ll need to call base.StopAsync to ensure that the cancellation token is signaled to stop as seen in the base class code below. The base class waits for the task to finish after signaling for it to end.

public virtual async Task StopAsync(CancellationToken cancellationToken)
{
    // Stop called without start
    if (_executingTask == null)
    {
        return;
    }

    try
    {
        // Signal cancellation to the executing method
        _stoppingCts.Cancel();
    }
    finally
    {
        // Wait until the task completes or the stop token triggers
        await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
    }
}

ExecuteAsync. This is the meat of the service. It’s an abstract class that you’ll be responsible for implementing to make sure the services does something useful. You can think of this as the entry point to your main application loop. Whatever you do in this method needs to return a Task asynchronously and respect the cancellation token given as a parameter. Don’t synchronously block on long-running operations. Additionally, you should always gracefully end the operations if the cancellation token is signaled to cancel.

The Worker Service template will create a basic implementation like so. Notice that the main loop will end when cancellation is requested, and the method awaits the long-running Task.Delay operation.

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
        await Task.Delay(1000, stoppingToken);
    }
}

Run as Windows Service in Azure

The first deployment method we’re going to discuss involves deploying a Windows Service to Azure. Windows Services are executables that run in the background on a host machine without interaction from a user. It’s common for home users to have many of these running in parallel for things like automatic updates, backups, scanners, and cleaners.

What are Azure WebJobs?

In Azure, you have a few choices to make with respect to how you’re going to host your Windows Services. If you already have a Windows virtual machine that you want to use, you can deploy to that instance and install using the traditional Windows Services installation techniques. But that’s quite a heavy setup for someone that just wants to push a small background process to the cloud.

Instead, you can use a feature of Azure App Services called WebJobs. In a nutshell, WebJobs are executables or scripts that run within the same context as a web, API, or mobile application. Azure App Services are typically reserved for developers that want to push complex ASP.NET services, so the WebJobs feature is often overlooked. There are some caveats to this approach, however (including pricing implications).

Of the two types of WebJobs, Continuous and Triggered, you’ll want to choose Continuous for your Worker Service. This causes your service to start immediately, always run, supports remote debugging, and runs on multiple instances of your App Service setup. The big issue here is that Continuous WebJobs require the “Always On” feature of the App Service to be enabled. This feature is only supported on Basic plans and up. As of this writing, the cheapest Basic plan in East US is $54.75 / month. That’s pricey for someone that just wants to push a small service. Sure, the Basic plan gives you the ability to deploy “unlimited” applications, but that only matters if you have many of these types of services to host simultaneously.

Modify Project to Host as Windows Service

The Worker Service template doesn’t make assumptions about how you want to host the service. By default, it’s just a console application that happens to use dependency injection and a class that inherits from BackgroundService.

To host in a Windows Service process, start by installing the Microsoft.Extensions.Hosting.WindowsServices NuGet package to your project.

Modify Program.cs to include the UseWindowsService() method on the host builder like seen below.

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseWindowsService()
        .ConfigureServices((hostContext, services) =>
        {
            services.AddHostedService<Worker>();
        });

Build and Publish to Azure

There are several ways in which you can publish to Azure, but we’re going to cover only the Visual Studio 2019 route.

Make sure your Visual Studio installation has the “Azure development” kit installed. You can get back to this screen by launching the Visual Studio installer application.

Next, right click the project in the Solution Explorer and select “Publish…”. If you have the Azure development kit installed, you’ll see the Azure WebJobs publish target. Click “Create Profile” to start the process of logging in to your Azure subscription. If you’re already logged in to Azure through your Visual Studio subscription, you won’t be prompted to login. Otherwise, you’ll have to login before you can continue.

If login was successful, you’ll see your existing Subscription, Resources groups, and Hosting plans listed on the left. Select which ones you want to use for your Worker Service (or create new ones). Rename the WebJob to something more descriptive since the default is just a placeholder. Note that this step will create a new App Service in your Azure subscription which may have pricing implications.

After the publication is complete, you can confirm success by going to the Azure portal, clicking the new App Service, and then clicking WebJobs on the left menu. The UI will let you manage your WebJob and view the logs in case things go wrong.

Further Reading: If you want to learn other great Azure-based technologies, you should check our our shortlist of great introductions to Azure technologies. In a matter of hours, you’ll understand how to use features such as Azure Pipelines and Azure Functions in your next project!

Run as Linux Systemd Service

A potentially cheaper option is to host the Worker Service as a daemon in systemd under a Linux virtual machine. In Azure, Linux VMs are usually less expensive than their Windows counterparts (although Microsoft has been pricing them competitively lately). Pricing aside, you may just have a fleet of Linux VMs already established of which you want to take advantage. Regardless of your reasoning, here’s how you do it.

Modify Project to Host in Systemd

Similar to the Windows Service route, you’ll need to install a NuGet package. For systemd, the package is named Microsoft.Extensions.Hosting.Systemd. Also like the Windows Service route, you’ll need to modify Program.cs to use the UseSystemd() method on the host builder.

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseSystemd()
        .ConfigureServices((hostContext, services) =>
        {
            services.AddHostedService<Worker>();
        });

Build and Publish to Local Folder

Publishing your Worker Service to a Linux VM is less convenient than the start-to-finish integration built into Visual Studio 2019 and Azure, but it’s not that much more difficult. First, instead of publishing directly to the host, you’ll publish to a local folder and then need to use something like scp or sftp to transfer the files securely to your VM.

If you want to use the .NET Core CLI, use the following command to indicate the target runtime will be a 64-bit Linux instance. Read more about runtime identifiers on Microsoft Docs.

dotnet publish -r linux-x64 -c Release

If you want to use Visual Studio 2019, you can follow similar steps as with the Windows Service route. Right click the project in the Solution Explorer and select “Publish…”. Instead of selecting Azure WebJobs from the resulting popup, select “Folder”. Choose the folder that you want the build output to be placed.

Select all the appropriate profile settings for your use case.

  • Configuration: like you’ll set this to “Release | Any CPU”, but you might have different build configurations for your project.
  • Target Framework: Worker Services are only supported in .NET Core 3.0+
  • Deployment Mode: select “Framework Dependent”
  • Target Runtime: see the Microsoft Docs article on runtime identifiers (we selected “linux-x64” for our scenario).

After you’ve published with this profile selected, you’ll see build output in the folder you selected during your profile setup. Zip the contents of your build output into a file so that you can scp or sftp it to your Linux VM.

Create Systemd Daemon

  1. After transferring your zipped project output to the VM of your choice, unzip the file to a temporary location
  2. Create a folder in /usr/sbin/ for your Worker Service such as /usr/sbin/MyWorkerService
  3. Copy the unzipped project output to that new folder
  4. Set permissions on the folder contents to something that makes sense for your scenario. We used chmod 700 to make sure the owner could execute.
  5. In the /etc/systemd/system folder, create a .service file for systemd to use for the service configuration (see contents of this file below).
  6. Start the service using sudo systemctl start <AppName>.service. Note that <AppName>.service is the file name you created in the previous step.
  7. Check the log files for your service using journalctl -xe regularly.

Here’s an example of the .service configuration file and a description of what each line means.

  • Type=notify indicates the service will callback to systemd when it has finished starting.
  • WorkingDirectory indicates the directory context in which the service will be executing.
  • ExecStart must point to the main dll output file that was generated as part of the publishing steps above.
[Unit]
Description=My first worker service

[Service]
Type=notify
WorkingDirectory=/usr/sbin/MyWorkerService/
ExecStart=/usr/sbin/MyWorkerService/MyWorkerService

[Install]
WantedBy=multi-user.target

When You’re Done: Don’t stop there! If your .NET Core Worker Service is a personal project, commit your code to something like Bitbucket or GitHub to build up your public portfolio. Having one will absolutely improve your chances during your next technical job interview.

Justin Skiles

Justin Skiles

Justin has been developing enterprise application software for over 10 years primarily using Microsoft stacks, Azure, and various open source tools. He has most recently been trying his best as a Manager and Director of Software Engineering in the health care industry.

Share the Knowledge

Share on facebook
Facebook
Share on twitter
Twitter
Share on linkedin
LinkedIn
Share on pinterest
Pinterest
Share on facebook
Share on twitter
Share on linkedin
Share on pinterest

Follow our updates

JOIN OUR SUBSCRIBERS

GET FREE UPDATES

Keep Exploring. Choose Your Path.

Be a Better, Smarter You

With our in depth guides, you’re bound to be setup for success.

Our experts have been collectively developing software for over 20 years.

We find the best tools and direct you to them so that you don’t have to.

JOIN THE CONVERSATION

Get the latest ultimate guides, tutorials, and advice to level up your skills.