Hosted service has a major update to its lifecycle events

Published: Tuesday 12 December 2023

A worker service project has seen a major update in .NET 8 with the ability to hook into more lifecycle events for a hosted service.

We'll have a look at how at what these lifecycle events are and how they can be implemented into an existing background service.

Creating a worker service project

When creating a worker service project, it typically creates a background service which is similar to this:

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)
		{
			if (_logger.IsEnabled(LogLevel.Information))
			{
				_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
			}
			await Task.Delay(5000, stoppingToken);
		}
	}
}

This service inherits the BackgroundService abstract class which includes an abstract ExecuteAsync method that we have to override.

The ExecuteAsync method requires a cancellation token in the parameter which we typically would use in a while loop whilst the cancellation has not been requested.

It goes ahead and invokes some logic before delaying the task by a certain amount of time before repeating the task.

This continues until the cancellation of the worker service has been requested.

The IHostedService interface

The BackgroundService abstract class implements the IHostedService interface which includes methods for when a hosted service starts and stops.

/// <summary>
/// Defines methods for objects that are managed by the host.
/// </summary>
public interface IHostedService
{
    /// <summary>
    /// Triggered when the application host is ready to start the service.
    /// </summary>
    /// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
    /// <returns>A <see cref="Task"/> that represents the asynchronous Start operation.</returns>
    Task StartAsync(CancellationToken cancellationToken);

    /// <summary>
    /// Triggered when the application host is performing a graceful shutdown.
    /// </summary>
    /// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param>
    /// <returns>A <see cref="Task"/> that represents the asynchronous Stop operation.</returns>
    Task StopAsync(CancellationToken cancellationToken);
}

This is better than nothing, but it doesn't give us many lifecycle events and therefore flexibility on what we can do.

Introducing the new IHostedLifecycleService

New for .NET 8, the IHostedLifecycleService interface makes its appearence. This implements the IHostedService interface so includes the StartAsync and StopAsync implementations.

However, there are some new lifecycle events included:

  • StartingAsync - Invoked before the StartAsync method
  • StartedAsync - Invoked after the StartAsync method
  • StoppingAsync - Invoked before the StopAsync method
  • StoppedAsync - Invoked after the StopAsync method

This is great as we can use these new methods for our worker service such as importing or exporting data and validation.

The problem with BackgroundService

The problem with the existing BackgroundService abstract class is that it implements the IHostedService interface, so only takes advantage of the StartAsync and StopAsync methods.

As far as we know, there isn't a separate background service abstract class in .NET 8 to accommodate the new lifecycle events, so how do we take advantage of the new lifecycle events?

Adding the IHostedLifecycleService to an existing background service

The answer is to implement the IHostedLifecycleService to the existing background service and implementing the new methods into it.

This means that we can take advantage of the new methods in our background service.

In this example, we are logging a message for each lifecycle event to confirm the order in which they are triggered.

public class Worker : BackgroundService, IHostedLifecycleService
{
    private readonly ILogger<Worker> _logger;

    public Worker(ILogger<Worker> logger)
    {
        _logger = logger;
    }

    public Task StartingAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("** Starting **");
        return Task.CompletedTask;
    }

    public override Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("** Start **");
        return base.StartAsync(cancellationToken);
    }

    public Task StartedAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("** Started **");
        return Task.CompletedTask;
    }

    public Task StoppingAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("** Stopping **");
        return Task.CompletedTask;
    }

    public override Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("** Stop **");
        return base.StopAsync(cancellationToken);
    }

    public Task StoppedAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("** Stopped **");
        return Task.CompletedTask;
    }



    protected override async Task ExecuteAsync(
        CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            if (_logger.IsEnabled(LogLevel.Information))
            {
                _logger.LogInformation(
                    "Worker running at: {time}", 
                    DateTimeOffset.Now
                );
            }
            await Task.Delay(5000, cancellationToken);
        }
    }
}

Watch the new lifecycle events in action

Watch our video where we talk through the changes to the hosted service in .NET 8 and demo them to ensure that we can successfully hook into the new lifecycle events and confirm the order in which the events are triggered.