top of page

Introduction to Worker Services in ASP.NET Core

Updated: Feb 1

Worker Services in ASP.NET Core provide a robust foundation for building efficient, long-running background tasks. These specialized services offer a framework for executing recurring workloads, making them ideal for scenarios such as processing messages, monitoring file changes, aggregating data, and more. In this article, we embark on an exploration of the fundamental concepts and capabilities of Worker Services, shedding light on how they can elevate the capabilities of your ASP.NET Core applications.


Worker services in ASP.NET core


What are Worker Services in ASP.NET Core?

A Worker Service is a type of .NET project created using a specific template that enhances a standard console application with additional features, making it more robust and versatile. This type of service operates based on the concept of a host, responsible for managing the application's lifecycle. The host also provides access to essential functionalities such as dependency injection, logging, and configuration settings.


Worker services are designed to handle long-running tasks and are well-suited for performing recurring workloads. They are particularly useful in scenarios where continuous processing or background tasks are required.


Here are some examples of how worker services can be employed:

  1. Processing Messages/Events: Handling messages or events from a queue, service bus, or event stream consistently and efficiently.

  2. File System Monitoring: Reacting to changes in files stored in an object store or file system. This is especially beneficial for scenarios where real-time responses to file modifications are necessary.

  3. Data Aggregation: Aggregating data from a data store over time, allows for the continuous collection and processing of information.

  4. Data Enrichment: Enhancing data in data ingestion pipelines by adding additional information, validations, or transformations to improve its quality and usefulness.

  5. AI/ML Dataset Processing: Formatting and cleansing of datasets used in Artificial Intelligence (AI) or Machine Learning (ML) applications. This includes preparing and refining data for training models.


Working with Worker Service in ASP.NET

Working with Worker Services in ASP.NET Core involves implementing the IHostedService interface or extending the BackgroundService abstract class. These services leverage dependency injection capabilities through the IServiceProvider interface.


Here, we will create a PostNotificationService as an example, which sends predefined emails to subscribed users on a preconfigured schedule.


Getting Started

To start, create a .NET Core project specifically for hosting the worker service. The project type is different from a typical ASP.NET Core project, and the csproj file should include references to the necessary packages.


For example:

<Project Sdk="Microsoft.NET.Sdk.Worker">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="3.1.3" />
  </ItemGroup>
</Project>

Note the explicit reference to the Microsoft.Extensions.Hosting package, which is not present in other project types like WebApp.


Implementing PostNotificationService

Now, let's create the PostNotificationService class, our worker service. There are two options for implementing the service:

  1. IHostedService

  2. BackgroundService


Option 1: Implement IHostedService

In this option, the PostNotificationService class directly implements the IHostedService interface, which consists of two main methods: StartAsync and StopAsync. This approach provides more flexibility but requires manual management of the executing task and cancellation tokens.

public class PostNotificationService : IHostedService
{
    private Task _executingTask;
    private CancellationTokenSource _cts;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

        // Your PostNotificationService logic goes here

        return _executingTask?.IsCompleted ?? true
            ? _executingTask
            : Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        if (_executingTask == null)
            return;

        _cts.Cancel();

        await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken));
        cancellationToken.ThrowIfCancellationRequested();
    }
}

Advantages:

  • You have more control over the initialization and cleanup processes.

  • Fine-grained control over cancellation tokens.


Considerations:

  • Requires explicit management of the executing task and cancellation tokens.

  • Additional responsibility for handling asynchronous operations and error scenarios.


Option 2: Extend BackgroundService

This option involves creating a class that extends the BackgroundService abstract class provided by ASP.NET Core. The BackgroundService class already implements the IHostedService interface and includes a pre-implemented StartAsync method that calls an abstract ExecuteAsync method.

public class PostNotificationService : BackgroundService 
{ 
	protected override async Task ExecuteAsync(CancellationToken stoppingToken) 
	{ 
		// Your PostNotificationService logic goes here 
	} 
}

Advantages:

  • Abstracts away the complexities of managing the executing task and cancellation tokens.

  • Simplifies code by focusing on the core logic within the ExecuteAsync method.


Considerations:

  • Less control over the initialization and cleanup processes compared to implementing IHostedService directly.

  • Suitable for scenarios where the provided lifecycle management fits your requirements.


Choosing the Right Option

The choice between these options depends on the specific needs of your application. If you require granular control over the service's lifecycle and execution, implementing IHostedService directly may be more appropriate. On the other hand, if you prefer a more streamlined approach with less boilerplate code, extending BackgroundService might be the better choice.


In either case, both options allow you to create efficient and scalable worker services in ASP.NET Core, benefiting from the underlying infrastructure provided by the framework.


Registering a Hosted “Service”

Registering a hosted service in ASP.NET Core involves configuring it to be invoked by the host when building the application. This configuration typically takes place in the Main() method where the host is constructed.


Below is an example of registering the PostNotificationService as a hosted service in the host configuration:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(builder =>
        {
            builder
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseKestrel()
                .UseIISIntegration()
                .UseStartup<Startup>()
                .UseSetting("detailedErrors", "true");
        })
        .ConfigureServices(services =>
        {
            services.AddHostedService<PostNotificationService>();
        })
        .CaptureStartupErrors(true);

In this example:

  • CreateDefaultBuilder(args) sets up the default configurations for the host, including logging, configuration, and dependency injection.

  • .ConfigureWebHostDefaults(builder => { ... }) configures the web host settings, such as content root, server, and startup class.

  • .ConfigureServices(services => { ... }) is where additional services are configured for dependency injection. Here, AddHostedService<PostNotificationService>() registers the PostNotificationService as a hosted service.


Why Register the Hosted Service?

The AddHostedService method is crucial for ensuring the proper execution of the hosted service within the application lifecycle. If you neglect to configure the hosted service after CreateDefaultBuilder(), the default behavior is that the StartAsync() method of the hosted service is invoked even before the application pipeline is built. This can be problematic if your hosted service relies on services available in the application pipeline.


Importance of Configuration Order

By explicitly configuring the hosted service with AddHostedService<PostNotificationService>(), you ensure that the service starts only after the service pipeline is built and the application is started. This allows you to inject and utilize services from the application pipeline within your hosted service, providing a seamless integration of background processing with the rest of your application.


Dependency Injection with Hosted Service

Dependency injection in a hosted service within an ASP.NET Core application allows you to access and utilize services available in the dependency injection (DI) container. The IServiceProvider interface can be injected via the constructor of the hosted service, enabling the use of various services within the background processing logic.


Below is an example with a PostNotificationService that demonstrates dependency injection:

public class PostNotificationService : BackgroundService 
{ 
	private readonly IServiceProvider serviceProvider; 
	
	public PostNotificationService(IServiceProvider serviceProvider) 
	{ 
		_serviceProvider = serviceProvider; 
	} 

	protected override async Task ExecuteAsync(CancellationToken cancellationToken) 
	{ 
	while (!cancellationToken.IsCancellationRequested) 
	{ 
		await SendPostNotifications(cancellationToken); 
		await Task.Delay(TimeSpan.FromMinutes(5), cancellationToken); 
	} 
	} 
	
	private async Task SendPostNotifications(CancellationToken cancellationToken) 
	{ 
		using (var scope = _serviceProvider.CreateScope()) 
		{ 
			IMailSubscriptionService mailSubscriptionService = scope.ServiceProvider.GetRequiredService<IMailSubscriptionService>(); 

			IEmailManager emailManager = scope.ServiceProvider.GetRequiredService<IEmailManager>(); 

			IConfigurationManager configurationManager = scope.ServiceProvider.GetRequiredService<IConfigurationManager>(); 

			var emailContent = await mailSubscriptionService.GetDigestEmailRenderedView(); 
	
			foreach (var subscriber in mailSubscriptionService.GetSubscribersForDigestEmails()) 
			{ 
				await Task.Run(() => 
				{ 
					emailManager.SendMail( 
						subscriber.EmailAddress, 			configurationManager.ContributeMailContentConfiguration.Subject, 
						emailContent); 
				}); 
			} 
		} 
	} 
}

In this example:

  • The IServiceProvider is injected into the constructor of the PostNotificationService.

  • The SendPostNotifications method demonstrates how to use the injected services (IMailSubscriptionService, IEmailManager, and IConfigurationManager) to perform background processing.



How is the Service Scheduled?

The service is scheduled within the ExecuteAsync method using a while loop. The loop repeats the invocation of the SendPostNotifications method, which contains the business logic of the service.


A delay of five minutes (Task.Delay(TimeSpan.FromMinutes(5), cancellationToken)) is placed between consecutive function calls. This delay controls the frequency of the service execution.

protected override async Task ExecuteAsync(CancellationToken cancellationToken) 
{ 
	while (!cancellationToken.IsCancellationRequested) 
	{ 
		await SendPostNotifications(cancellationToken); 
		await Task.Delay(TimeSpan.FromMinutes(5), cancellationToken); 
	} 
}

This loop continues until the web application is stopped. The cancellation token is implicitly triggered via the StopAsync method of the implemented IHostedService interface, providing a graceful shutdown mechanism for the hosted service.


Conclusion

Worker Services stands as a powerful tool in the arsenal of ASP.NET Core developers, enabling the creation of resilient and scalable background processing solutions. Whether it's handling asynchronous tasks, reacting to events, or aggregating data over time, Worker Services provides a flexible and extensible platform.

bottom of page