In this article, we’ll describe the IHostedLifecycleService interface. We’ll see how it extends the IHostedService interface and enables finer control over the hosted service lifecycle. Finally, we’ll show some practical use cases for the IHostedLifecycleService interface.

To download the source code for this article, you can visit our GitHub repository.

Let’s get started. 

What Is the IHostedService Interface

The IHostedService interface is a powerful feature in .NET that enables us to manage background tasks in our applications. The hosted service integrates into the host lifecycle before the application request processing pipeline. 

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

The IHostedService interface requires the implementation of two methods, StartAsync() and StopAsync()

public interface IHostedService
{
    Task StartAsync(CancellationToken cancellationToken);
    Task StopAsync(CancellationToken cancellationToken);
}

The StartAsync() method implements the logic to start a background task. The host calls it when the server is started and IHostApplicationLifetime.ApplicationStarted property is triggered. The StartAsync() method should be short-running, as no other services will start before all hosted services start. 

Conversely, when the application stops, the host calls the StopAsync() method. We use it to stop the background task and to dispose of unmanaged resources. 

To explore possible ways to use IHostedService, read our article Different Ways to Run Background Tasks in ASP.NET Core.

The IHostedLifecycleService Interface

The IHostedLifecycleService interface inherits from the IHostedService interface. It adds methods for more granular control of the starting and stopping hosted service.

Concretely, it introduces the new methods that execute before and after the service starts or stops: 

public interface IHostedLifecycleService : IHostedService
{
    Task StartingAsync(CancellationToken cancellationToken);
    Task StartedAsync(CancellationToken cancellationToken);
    Task StoppingAsync(CancellationToken cancellationToken);
    Task StoppedAsync(CancellationToken cancellationToken);
}

The StartingAsync() method triggers before the StartAsync() method, while StartedAsync() triggers after the StartAsync() method. Similarly, the StoppingAsync() triggers before and the StoppedAsync() triggers after the StopAsync() method.

Let’s see that in action, with two classes HostedServiceA and HostedServiceB. We implement the IHostedLifecycleService interface in both but will show the code only from the first class:

public class HostedServiceA(ILogger<HostedServiceA> logger) : IHostedLifecycleService
{
    public Task StartAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation($"Service {nameof(HostedServiceA)} start.");

        return Task.CompletedTask;
    }

    public Task StartedAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation($"Service {nameof(HostedServiceA)} started.");

        return Task.CompletedTask;
    }

    public Task StartingAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation($"Service {nameof(HostedServiceA)} starting.");

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation($"Service {nameof(HostedServiceA)} stop.");

        return Task.CompletedTask;
    }

    public Task StoppedAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation($"Service {nameof(HostedServiceA)} stopped.");

        return Task.CompletedTask;
    }

    public Task StoppingAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation($"Service {nameof(HostedServiceA)} stopping.");

        return Task.CompletedTask;
    }
}

In every method that implements the IHostedLifecycleService interface, we log the service and the method names and return Task.CompletedTask.

The HostedServiceB class implementation is the same as the HostedServiceA class implementation, that’s why we didn’t show that class here.

Let’s register our classes as hosted services and run the application:

var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHostedService<HostedServiceA>();
builder.Services.AddHostedService<HostedServiceB>();

var app = builder.Build();

app.Run();

We register hosted services in the DI container with the AddHostedService<THostedService>() extension method of the IServiceCollection interface. When we run the application, we will see the output of our services in the console: 

info: TheIHostedLifecycleServiceInterface.Services.HostedServiceA[0]
      Service HostedServiceA starting.
info: TheIHostedLifecycleServiceInterface.Services.HostedServiceB[0]
      Service HostedServiceB starting.
info: TheIHostedLifecycleServiceInterface.Services.HostedServiceA[0]
      Service HostedServiceA start.
info: TheIHostedLifecycleServiceInterface.Services.HostedServiceB[0]
      Service HostedServiceB start.
info: TheIHostedLifecycleServiceInterface.Services.HostedServiceA[0]
      Service HostedServiceA started.
info: TheIHostedLifecycleServiceInterface.Services.HostedServiceB[0]
      Service HostedServiceB started.
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
.....
info: Microsoft.Hosting.Lifetime[0]  
      Application is shutting down...
info: TheIHostedLifecycleServiceInterface.Services.HostedServiceB[0]
      Service HostedServiceB stopping.
info: TheIHostedLifecycleServiceInterface.Services.HostedServiceA[0]
      Service HostedServiceA stopping.
info: TheIHostedLifecycleServiceInterface.Services.HostedServiceB[0]
      Service HostedServiceB stop.
info: TheIHostedLifecycleServiceInterface.Services.HostedServiceA[0]
      Service HostedServiceA stop.
info: TheIHostedLifecycleServiceInterface.Services.HostedServiceB[0]
      Service HostedServiceB stopped.
info: TheIHostedLifecycleServiceInterface.Services.HostedServiceA[0]
      Service HostedServiceA stopped.

Firstly, we can see that the order of the method’s calls is as expected.

Secondly, we can see that the StartingAsync() method is called for all hosted services before the StartAsync() method executes. The StartAsync() method executes again for all services before the StartedAsync() executes. We observe similar behavior when stopping the service.

Finally, the execution of the service’s methods depends on the order in which the services are registered. When starting, the first executed methods are the HostedServiceA service and then the HostedServiceB service for every service lifecycle stage. When the application stops, the order is reversed, and the first call is to the last registered service. 

Concurrent Hosted Service Starting and Stopping

By default, the host executes the hosted service’s starting and stopping sequentially. In other words, the StartingAsync() method of the HostedServiceB executes only after the same method of the HostedServiceA returns.

From .NET 8, we can change this behavior by setting two of the host options, ServiceStartConcurrently and ServiceStopConcurrently: 

builder.Services.Configure<HostOptions>(options =>
{
    options.ServicesStartConcurrently = true;
    options.ServicesStopConcurrently = true;
});

All hosted services will start and stop parallel with these options set to true. Still, the lifecycle of any service won’t start before the current lifecycle of all the services returns. That means that all StartingAsync() methods first execute parallel; after that, StartAsync() methods, and so on. 

When our application has multiple hosted services, their concurrent start enables a shorter startup time. The same is true for application shutdown.  

The possibility of the concurrent start of all hosted services makes the IHostedLifecycleService interface even more valuable, as some of the hosted services may require certain conditions to be fulfilled. We can implement the fulfillment of these conditions earlier in the lifecycle of the other service. 

Use Cases for IHostedLifecycleService Interface

If our application has multiple hosted services, we can use the StartingAsync() method to perform any initialization task, such as seeding the database, clearing caches, and checking the existence of external resources and dependencies. If something is wrong, we can stop the application before it starts to accept incoming requests. 

Similarly, we can perform pre-loading and initialize the application’s state. The other hosted services can rely on specific data or states in their StartAsync() methods. We could use the StartedAsync() method to log the initial application state or to inform dependent services that this service is ready to accept the workload. 

On the other hand, the StoppingAsync() method helps persist the application state before it terminates. We can also use this method to dispose of unmanaged resources. Likewise, the StoppedAsync() method can inform other dependencies that the service is not available anymore. 

Conclusion

The IHostedLifecycleService interface enables more granular control of what happens in each service’s lifecycle stage when it starts and stops. We can execute code that initializes the required dependencies for other services. We can ensure that everything is ready for the application to work correctly, or we can stop the application when this is not the case. 

Introducing the host options to start and stop services concurrently makes such granularity almost necessary if our application implements multiple hosted services. 

Developers can now hook into more stages of the hosted service lifecycle, enabling more performant and resilient applications. 

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!