.NET Tools

How to start using .NET Background Services

As developers, running critical tasks in the background can open up a world of problem-solving possibilities. Background services are ideal for many scenarios, especially if your users are willing to wait for results. Some use-case scenarios for background services include updating globally-used caches, processing queued work, and general health monitoring.

With .NET’s hosting model, background services have never been more straightforward, whether creating custom services or using many of the packages found on NuGet. 

This post will explore the BackgroundService class and the elements you need to know when writing your background services. You’ll also see some community work that uses the .NET hosting infrastructure to provide helpful enhancements.

What Is a BackgroundService?

BackgroundService is a base class for implementing long-running processes and implements the IHostedService interface. The interface offers an implementor two methods of StartAsync and StopAsync, each providing access to the lifecycle events of the host. The BackgroundService class contains one abstract method of ExecuteAsync, which helps reduce the implementation complexity and is often recommended as a starting point for many. Any BackgroundService implementation is registered to a host and is part of the application’s services collection, allowing the implementation to use dependency injection.

You’ll see more of what it takes to implement a BackgroundService in the following sections.

It all starts with the IHost

If you’re new to .NET or just recently migrated over from the full framework, you’ll immediately notice that all .NET applications are console applications in their most authentic nature. While you could certainly use the “Console Application” template as an empty starting point, the SDK provides multiple templates that give you the boilerplate code to initialize a host.

The IHost interface is a context for all your application’s services, whether integrated services like dependency injection, logging, configuration, or custom services developed by you. In practice, the host is the foundation of your application. 

With the host, you can predictably manage the starting and stopping of your application, allowing your application to behave more gracefully when those events occur. The host also tracks the lifetime of registered services, whether singleton, scoped, or transient, via an IServiceProvider instance. Finally, the host will also be responsible for starting any registered background services.

Registering your background services with an IHost instance lets you use services such as logging, dependency injection, and working with the ApplicationLifetime instance.

Creating a new project with the “Worker Service” template, you’ll see the start of a worker-based application in the Program.cs file.

using App.Sample;

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddHostedService<Worker>();
    })
    .Build();

await host.RunAsync();

The IHost is also available in any ASP.NET Core application, meaning you can run background services in the same process your web application or API runs.

In the next section, you’ll see what is considered a “Hosted Service” by exploring the boilerplate class of Worker.

Your first background service

You’ll notice a Worker.cs in the solution explorer if you’ve created a new project. Opening the file, you’ll see a starting class definition for Worker.

namespace App.Sample;

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

Let’s break down the crucial elements of this class and why they’re necessary. You might first notice the inherited base class of BackgroundService

As mentioned in a previous section, a BackgroundService is an abstract implementation of an IHostedService instance. A BackgroundService has a standard approach to handling the events of StartAsync and StopAsync for your service, allowing you to focus on implementing a single method of ExecuteAsync. Of course, you can still override those methods, but typically that’s unnecessary.

Next, you’ll notice the Worker class’ constructor, which has a parameter of ILogger<Worker>. The host creates and passes the type as an argument using the IServiceProvider and the registered services in the Program.cs file. Your worker service can define any number of parameters. You should be mindful of the scope lifetimes you’ll inject based on your hosting environment, or you might trigger scope validation. You can get around scope limitations by injecting an instance of IServiceProvider and creating a scope to retrieve “scoped” or “transient” instances of services.

Finally, we have the ExecuteAsync method, which has a while loop that continues running as long as your application executes. A “stop” event will trigger a cancellation request and allow the loop to end. You can place any processing logic within the loop and perform tasks such as processing items from a queue or executing timed tasks. While the loop is a good starting point, you can change the entire behavior of the ExecuteAsync method to match your use case.

Your host application is not limited to a single background service. You can register any number of services, but be aware that the more registered services there are in a host, the greater the likelihood that a single background service could cause issues for other services. You can certainly write your code to be more resilient, but the standard practice for catastrophic background service failures is to restart the process.

Now that you have a basic understanding of hosted background services, let’s look at some community-led efforts to use this model to deliver promising solutions.

Community projects using background services

Job libraries are the most common use for background services in the .NET space. These libraries allow you to create recurring jobs, queue jobs to be processed asynchronously, or perform fire-and-forget operations.

Currently, the two most popular libraries in .NET are Hangfire (85 million downloads) and Quartz (40 million downloads), with several other libraries providing similar functionality. Let’s look at how you might register a background service from one of these libraries.

For Hangfire, you first need to install the Hangfire package, which will give you access to IServiceCollection extension methods. Next, you’ll need to add the Hangfire services and the Hangfire server. 

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddHangfire(x => x.UseInMemoryStorage());
        services.AddHangfireServer();
    })
    .Build();

The call to AddHangfireServer registers a BackgroundJobServerHostedService, which will handle all jobs in the designated database. To run a job in the background service, call the method Enqueue on an IBackgroundJobClient.

var jobs = host.Services.GetRequiredService<IBackgroundJobClient>();
jobs.Enqueue(() => Console.WriteLine("Hello, World"));

Messaging and Actor frameworks sit in another category of libraries that can sometimes utilize background services, allowing you to send distributed messages. These distributed frameworks are trendy in the .NET space as they will enable you to scale your applications up, out, and in any combination. Current popular choices include Wolverine, MassTransit, Brighter, Akka.NET, and Proto.Actor.

Let’s look at how Wolverine uses background services to enable message-based programming.

using Wolverine;

IHost host = Host.CreateDefaultBuilder(args)
    .UseWolverine()
    .Build();

var lifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();

lifetime.ApplicationStarted.Register(async () =>
{
    // invoke the message bus after the host is running
    var bus = host.Services.GetRequiredService<IMessageBus>();
    await bus.InvokeAsync(new GreetingCommand("Khalid"));
});

host.Run();

public record GreetingCommand(string Name);

public class AssignUserHandler
{
    public void Handle(GreetingCommand greetingCommand)
    {
        Console.WriteLine($"Hello, {greetingCommand.Name}!");
    }
}

Wolverine uses a background service called WolverineRuntime, which pairs messages to their destination handlers. With the current code, Wolverine handles all messages in memory, but you can store messages in a variety of backing stores, including SQL Server, PostgreSQL, and RabbitMQ.

Note. If you’re interested in learning more about Wolverine, check out our recent webinar with guest Jeremy Miller and host Matt Ellis.

Conclusion

With the hosting model in the latest versions of .NET, writing background services has never been more straightforward. I recommend starting with the “Worker Service” template, as it has the right amount of boilerplate to make you productive and the necessary infrastructure you’ll likely want in your applications. 

Additionally, the ASP.NET Core hosting model supports background services, so you’re not limited to choosing when and where you use this feature of .NET, but you will need to be mindful of your production environment. 

The background service abstraction is powerful, and you’ve heard of some successful usages in the .NET open-source space. It’s a new tool library authors can use to bring new solutions to old problems.

I hope you enjoyed this post, and if you have any questions or comments, please feel free to leave them below.

Image Credit: Eduard Delputte

image description