Worker Services in .NET

There are numerous reasons for creating long-running services such as:

  • Processing CPU intensive data.
  • Queuing work items in the background.
  • Performing a time-based operation on a schedule.

Background service processing usually doesn't involve a user interface (UI), but UIs can be built around them. In the early days with .NET Framework, Windows developers could create Windows Services for these reasons. Now with .NET, you can use the BackgroundService — which is an implementation of IHostedService, or implement your own.

With .NET, you're no longer restricted to Windows. You can develop background services that are cross-platform. Hosted services are logging, configuration, and dependency injection (DI) ready. They're a part of the extensions suite of libraries, meaning they're fundamental to all .NET workloads that work with the generic host.

Terminology

There are many terms that are mistakenly used synonymously. In this section, there are definitions for some of these terms to make their intent more apparent.

  • Background Service: Refers to the BackgroundService type.
  • Hosted Service: Implementations of IHostedService, or referring to the IHostedService itself.
  • Long-running Service: Any service that runs continuously.
  • Windows Service: The Windows Service infrastructure, originally .NET Framework centric but now accessible via .NET.
  • Worker Service: Refers to the Worker Service template.

Worker Service template

The Worker Service template is available to the .NET CLI, and Visual Studio. For more information, see .NET CLI, dotnet new worker - template. The template consists of a Program and Worker class.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace App.WorkerService
{
    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>();
                });
    }
}

The preceding Program class:

The Program.cs file from the template can be rewritten using top-level statements:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using App.WorkerService;

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

await host.RunAsync();

This is functionally equivalent to the original template. For more information on C# 9 features, see What's new in C# 9.0.

As for the Worker, the template provides a simple implementation.

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace App.WorkerService
{
    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);
                try
                {
                    await Task.Delay(1000, stoppingToken);
                }
                catch (OperationCanceledException)
                {
                    break;
                }
            }
        }
    }
}

The preceding Worker class is a subclass of BackgroundService, which implements IHostedService. The BackgroundService is an abstract class and requires the subclass to implement BackgroundService.ExecuteAsync(CancellationToken). In the template implementation the ExecuteAsync loops once per second, logging the current date and time until the process is signaled to cancel.

The project file

The Worker Service template relies on the following project file Sdk:

<Project Sdk="Microsoft.NET.Sdk.Worker">

For more information, see .NET project SDKs.

NuGet package

An app based on the Worker Service template uses the Microsoft.NET.Sdk.Worker SDK and has an explicit package reference to the Microsoft.Extensions.Hosting package.

Containers and cloud adoptability

With most modern .NET workloads, containers are a viable option. When creating a long-running service from the Worker Service template in Visual Studio, you can opt-in to Docker support. Doing so will create a Dockerfile that will containerize your .NET app. A Dockerfile is a set of instructions to build an image. For .NET apps, the Dockerfile usually sits in the root of the directory next to a solution file.

# See https://aka.ms/containerfastmode to understand how Visual Studio uses this
# Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/runtime:5.0 AS base
WORKDIR /app

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY ["background-service/App.WorkerService.csproj", "background-service/"]
RUN dotnet restore "background-service/App.WorkerService.csproj"
COPY . .
WORKDIR "/src/background-service"
RUN dotnet build "App.WorkerService.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "App.WorkerService.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "App.WorkerService.dll"]

The preceding Dockerfile steps include:

  • Setting the base image from mcr.microsoft.com/dotnet/runtime:5.0 as the alias base.
  • Changing the working directory to /app.
  • Setting the build alias from the mcr.microsoft.com/dotnet/sdk:5.0 image.
  • Changing the working directory to /src.
  • Copying the contents and publishing the .NET app:
  • Relayering the .NET SDK image from mcr.microsoft.com/dotnet/runtime:5.0 (the base alias).
  • Copying the published build output from the /publish.
  • Defining the entry point, which delegates to dotnet App.BackgroundService.dll.

Tip

The MCR in mcr.microsoft.com stands for "Microsoft Container Registry", and is Microsoft's syndicated container catalog from the official Docker hub. The Microsoft syndicates container catalog article contains additional details.

When targeting Docker as a deployment strategy for your .NET Worker Service, there are a few considerations in the project file:

<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <RootNamespace>App.WorkerService</RootNamespace>
    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
    <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.10.14" />
  </ItemGroup>
</Project>

In the preceding project file, the <DockerDefaultTargetOS> element specifies Linux as its target. To target Windows containers, use Windows instead. The Microsoft.VisualStudio.Azure.Containers.Tools.Targets NuGet package is automatically added as a package reference when Docker support is selected from the template.

For more information on Docker with .NET, see Tutorial: Containerize a .NET app. For more information on deploying to Azure, see Tutorial: Deploy a Worker Service to Azure.

Hosted Service extensibility

The IHostedService interface defines two methods:

These two methods serve as lifecycle methods - they're called during host start and stop events respectively.

Important

The interface serves as a generic-type parameter constraint on the AddHostedService<THostedService>(IServiceCollection) extension method, meaning only implementations are permitted. You're free to use the provided BackgroundService with a subclass, or implement your own entirely.

See also