Implement the IHostedService interface

When you need finite control beyond the provided BackgroundService, you can implement your own IHostedService. The IHostedService interface is the basis for all long running services in .NET. Custom implementations are registered with the AddHostedService<THostedService>(IServiceCollection) extension method.

In this tutorial, you learn how to:

  • Implement the IHostedService, and IAsyncDisposable interfaces.
  • Create a timer-based service.
  • Register the custom implementation with dependency injection and logging.

Tip

All of the "Workers in .NET" example source code is available in the Samples Browser for download. For more information, see Browse code samples: Workers in .NET.

Prerequisites

Create a new project

To create a new Worker Service project with Visual Studio, you'd select File > New > Project.... From the Create a new project dialog search for "Worker Service", and select Worker Service template. If you'd rather use the .NET CLI, open your favorite terminal in a working directory. Run the dotnet new command, and replace the <Project.Name> with your desired project name.

dotnet new worker --name <Project.Name>

For more information on the .NET CLI new worker service project command, see dotnet new worker.

Tip

If you're using Visual Studio Code, you can run .NET CLI commands from the integrated terminal. For more information, see Visual Studio Code: Integrated Terminal.

Create timer service

The timer-based background service makes use of the System.Threading.Timer class. The timer triggers the DoWork method. The timer is disabled on IHostLifetime.StopAsync(CancellationToken) and disposed when the service container is disposed on IAsyncDisposable.DisposeAsync():

Replace the contents of the Worker from the template with the following C# code, and rename the file to TimerService.cs:

namespace App.TimerHostedService;

public sealed class TimerService(ILogger<TimerService> logger) : IHostedService, IAsyncDisposable
{
    private readonly Task _completedTask = Task.CompletedTask;
    private int _executionCount = 0;
    private Timer? _timer;

    public Task StartAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation("{Service} is running.", nameof(TimerHostedService));
        _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));

        return _completedTask;
    }

    private void DoWork(object? state)
    {
        int count = Interlocked.Increment(ref _executionCount);

        logger.LogInformation(
            "{Service} is working, execution count: {Count:#,0}",
            nameof(TimerHostedService),
            count);
    }

    public Task StopAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation(
            "{Service} is stopping.", nameof(TimerHostedService));

        _timer?.Change(Timeout.Infinite, 0);

        return _completedTask;
    }

    public async ValueTask DisposeAsync()
    {
        if (_timer is IAsyncDisposable timer)
        {
            await timer.DisposeAsync();
        }

        _timer = null;
    }
}

Important

The Worker was a subclass of BackgroundService. Now, the TimerService implements both the IHostedService, and IAsyncDisposable interfaces.

The TimerService is sealed, and cascades the DisposeAsync call from its _timer instance. For more information on the "cascading dispose pattern", see Implement a DisposeAsync method.

When StartAsync is called, the timer is instantiated, thus starting the timer.

Tip

The Timer doesn't wait for previous executions of DoWork to finish, so the approach shown might not be suitable for every scenario. Interlocked.Increment is used to increment the execution counter as an atomic operation, which ensures that multiple threads don't update _executionCount concurrently.

Replace the existing Program contents with the following C# code:

using App.TimerHostedService;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<TimerService>();

IHost host = builder.Build();
host.Run();

The service is registered in (Program.cs) with the AddHostedService extension method. This is the same extension method you use when registering BackgroundService subclasses, as they both implement the IHostedService interface.

For more information on registering services, see Dependency injection in .NET.

Verify service functionality

To run the application from Visual Studio, select F5 or select the Debug > Start Debugging menu option. If you're using the .NET CLI, run the dotnet run command from the working directory:

dotnet run

For more information on the .NET CLI run command, see dotnet run.

Let the application run for a bit to generate several execution count increments. You will see output similar to the following:

info: App.TimerHostedService.TimerService[0]
      TimerHostedService is running.
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: .\timer-service
info: App.TimerHostedService.TimerService[0]
      TimerHostedService is working, execution count: 1
info: App.TimerHostedService.TimerService[0]
      TimerHostedService is working, execution count: 2
info: App.TimerHostedService.TimerService[0]
      TimerHostedService is working, execution count: 3
info: App.TimerHostedService.TimerService[0]
      TimerHostedService is working, execution count: 4
info: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...
info: App.TimerHostedService.TimerService[0]
      TimerHostedService is stopping.

If running the application from within Visual Studio, select Debug > Stop Debugging.... Alternatively, select Ctrl + C from the console window to signal cancellation.

See also

There are several related tutorials to consider: