ASP.NET Core with Hosted Service & Lifecycle events

Merwan Chinta
CodeNx
Published in
5 min readJan 26, 2024

--

In .NET, a hosted service is a background service that runs within the same process as your web application or any application using the .NET Generic Host, Microsoft.Extensions.Hosting.Host.

It's not a Windows Service or a Linux Daemon by itself but can be thought of as a service that runs in the background of your application, handling tasks independently of user interaction.

It's perfect for long-running operations, background tasks, or any function that you want to keep running throughout the life of your application.

Knowing about Lifecycle

StartAsync and StopAsync are two fundamental methods in the IHostedService interface, which is the base interface for implementing a hosted service in .NET.

  1. StartAsync(CancellationToken cancellationToken): This method is called when the application starts. It’s where you would place your initialization code or start long-running tasks. The method receives a CancellationToken that can be used to stop the service when the application is shutting down.
  2. StopAsync(CancellationToken cancellationToken): This method is called when the application is shutting down. You should place your cleanup code here, ensuring that any resources are released, and any background tasks are gracefully stopped.

These methods are somewhat analogous to a Windows Service’s OnStart and OnStop methods, providing a structured way to initialize and tear down long-running or background tasks.

IHostedLifecycleService is an interface that extends the IHostedService. This interface provides more granular control over the application lifecycle, particularly beneficial for applications requiring precise resource management and initialization sequences, such as microservices or background services.

Before delving into the implementation, let’s understand the essence of IHostedLifecycleService. This interface, adds four critical lifecycle events to the existing StartAsync and StopAsync methods:

  1. StartingAsync: Invoked before the StartAsync method.
  2. StartedAsync: Called immediately after the StartAsync method completes.
  3. StoppingAsync: Triggered before the StopAsync method.
  4. StoppedAsync: Invoked right after the StopAsync method finishes.
IHostedLifecycleService hook events — Image source: Created by Author

Code Implementation

Imagine we have an ASP.NET Core web application responsible for handling HTTP requests. When a user posts information about a new bike, the web application saves this information to a database and publishes a message to a queue. The message contains information or a reference ID to the new bike entry.

Separately, a hosted service (background service) is running and listening to this queue. Whenever a new message appears, the hosted service picks it up and processes it, for instance, performing additional data enrichment, sending notifications, or integrating with other systems.

The idea is, to decouple your web application from long-running or resource-intensive tasks.

Project Structure

The solution consists of three main parts:

  1. ASP.NET Core Web Application: Handles HTTP requests and interacts with the database and message queue.
  2. Hosted Service (Background Service): Listens to the message queue and processes messages.
  3. Shared Library: Contains shared models, interfaces, and possibly shared service clients for database and message queue operations.

Setting Up the ASP.NET Core Web Application

Create a New Bike Endpoint

This endpoint accepts POST requests with new bike details, saves these details to the database, and publishes a message (including the bike’s ID or details) to the queue.

using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;

[Route("api/[controller]")]
[ApiController]
public class BikesController : ControllerBase
{
private readonly IBikeDbContext _dbContext;
private readonly IMessageQueueClient _messageQueueClient;

public BikesController(IBikeDbContext dbContext, IMessageQueueClient messageQueueClient)
{
_dbContext = dbContext;
_messageQueueClient = messageQueueClient;
}

[HttpPost]
public async Task<IActionResult> PostNewBike([FromBody] BikeModel bike)
{
await _dbContext.Bikes.AddAsync(bike);
await _dbContext.SaveChangesAsync();

await _messageQueueClient.PublishAsync(new QueueMessage { BikeId = bike.Id });

return Ok();
}
}

Set up any required services, like DB context and the message queue client, in your Startup.cs.

public void ConfigureServices(IServiceCollection services)
{
// other configurations ...
services.AddSingleton<IMessageQueueClient, YourMessageQueueClient>();
}

Creating the Hosted Service

Implement the Hosted Service

When you inherit from BackgroundService, you're primarily overriding the ExecuteAsync method to perform your background processing. However, the BackgroundService class already implements IHostedService, and it provides the StartAsync and StopAsync methods. If you need more fine-grained control, you can override these methods or implement IHostedLifecycleService for an even more detailed lifecycle management.

Create a new class that implements IHostedLifecycleService.

I’ll assume the message queue client has InitializeAsync and CleanupAsync methods for setting up and tearing down the connection to the queue.

public class QueueProcessingService : IHostedLifecycleService
{
private readonly IMessageQueueClient _messageQueueClient;
private Task _executingTask;
private CancellationTokenSource _stoppingCts = new CancellationTokenSource();

public QueueProcessingService(IMessageQueueClient messageQueueClient)
{
_messageQueueClient = messageQueueClient;
}

public async Task StartingAsync(CancellationToken cancellationToken)
{
// Logic before StartAsync, e.g., initializing the queue
await _messageQueueClient.InitializeAsync(cancellationToken);
}

public async Task StartAsync(CancellationToken cancellationToken)
{
// Triggered when the application host is ready to start the service.
_executingTask = ExecuteAsync(_stoppingCts.Token);

// If the task is completed, return it, otherwise it's still running
await (_executingTask.IsCompleted ? _executingTask : Task.CompletedTask);
}

protected async Task ExecuteAsync(CancellationToken stoppingToken)
{
// The main background processing logic goes here
while (!stoppingToken.IsCancellationRequested)
{
var message = await _messageQueueClient.ReceiveAsync(stoppingToken);
if (message != null)
{
ProcessMessage(message);
}
}
}

public Task StartedAsync(CancellationToken cancellationToken)
{
// Logic after StartAsync, e.g., post-initialization logging or actions
// This is where you might log that the service has successfully started
return Task.CompletedTask;
}

public Task StoppingAsync(CancellationToken cancellationToken)
{
// Logic before StopAsync, e.g., pre-cleanup actions
// This is where you might log that the service is stopping
return Task.CompletedTask;
}

public async Task StopAsync(CancellationToken cancellationToken)
{
// Triggered when the application host is performing a graceful shutdown.
_stoppingCts.Cancel();

// Wait until the task completes or the stop token triggers
await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
}

public async Task StoppedAsync(CancellationToken cancellationToken)
{
// Logic after StopAsync, e.g., cleaning up the queue
await _messageQueueClient.CleanupAsync(cancellationToken);
}

private void ProcessMessage(QueueMessage message)
{
// Process the message, e.g., enriching data, sending notifications, etc.
}
}

In this implementation:

  1. StartingAsync: Initializes the message queue before the service starts processing messages.
  2. StartedAsync: Can be used for post-startup actions, such as logging that the service has successfully started.
  3. StoppingAsync: Can be used for pre-shutdown actions, such as logging that the service is about to stop.
  4. StoppedAsync: Cleans up the message queue after the service has stopped processing messages.

This more detailed implementation ensures that your message queue is properly initialized and cleaned up as part of the service’s lifecycle, providing a clean and maintainable structure for managing the service’s resources.

In the Startup.cs of your ASP.NET Core project (or a separate worker service project if you prefer), register your hosted service.

public void ConfigureServices(IServiceCollection services)
{
services.AddHostedService<QueueProcessingService>();
// ...
}

This architecture suits scenarios where tasks can be processed asynchronously and independently of the main user interaction flow, making your system more responsive and robust.

I trust this information has been valuable to you. 🌟 Wishing you an enjoyable and enriching learning journey!

📚 For more insights like these, feel free to follow 👉 Merwan Chinta

--

--

Merwan Chinta
CodeNx

🚧 Roadblock Eliminator & Learning Advocate 🖥️ Software Architect 🚀 Efficiency & Performance Guide 🌐 Cloud Tech Specialist