As applications grow and evolve, the need for background tasks and scheduled jobs becomes more of a necessity. This is especially true with microservice architectures and in dealing with eventual consistency and event-driven communication. .Net Core 2.0 has a new interface, IHostedService, which makes executing background jobs easier. When using the IHostedService interface you can register multiple hosted services that will run in the background of your web application.

The IHostedService interface looks like:

namespace Microsoft.Extensions.Hosting
{
//
// Summary:
// Defines methods for objects that are managed by the host.
public interface IHostedService
{
// Summary:
// Triggered when the application host is ready to start the service.
Task StartAsync(CancellationToken cancellationToken);

// Summary:
// Triggered when the application host is performing a graceful shutdown.
Task StopAsync(CancellationToken cancellationToken);
}
}

You may be thinking, “What’s the difference between a hosted service implementing IHostedService vs starting a background thread to run a task?" The difference is the hosted service will be started and stopped with the application. When starting a background thread, the thread cannot gracefully handle clean-up actions and will be killed when the application is stopped. The HostedServiceExecutor will handle the starting and stopping of the hosted services. This will allow for the hosted services to be cleaned up gracefully when the application is stopped.

When creating a hosted service, you could implement the IHostedService, but .NET Core 2.1 will include an abstract base class called BackgroundService. The BackgroundService will provide the common operations needed for background tasks such as cancellation tokens. If you are still on .NET 2.0, you can copy this base class into your repository. The code for the BackgroundService class is below.

// Copyright (c) .NET Foundation. Licensed under the Apache License, Version 2.0.
/// <summary>
/// Base class for implementing a long running <see cref="IHostedService"/>.
/// </summary>
public abstract class BackgroundService : IHostedService, IDisposable
{
private Task _executingTask;
private readonly CancellationTokenSource _stoppingCts =
new CancellationTokenSource();

protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

public virtual Task StartAsync(CancellationToken cancellationToken)
{
// Store the task we're executing
_executingTask = ExecuteAsync(_stoppingCts.Token);

// If the task is completed then return it,
// this will bubble cancellation and failure to the caller
if (_executingTask.IsCompleted)
{
return _executingTask;
}

// Otherwise it's running
return Task.CompletedTask;
}

public virtual async Task StopAsync(CancellationToken cancellationToken)
{
// Stop called without start
if (_executingTask == null)
{
return;
}

try
{
// Signal cancellation to the executing method
_stoppingCts.Cancel();
}
finally
{
// Wait until the task completes or the stop token triggers
await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite,
cancellationToken));
}
}

public virtual void Dispose()
{
_stoppingCts.Cancel();
}
}

Creating a Hosted Service:

A use case for a background task is having a task that “polls” a database to check for updates. When dealing with microservices, you might need to have local caches for some services. Local caches will contain copies of data from other services. This helps avoid chained API calls and dependencies on other microservices. One approach to keep the cache up to date is having clients subscribe to events that an application publishes (pub/sub). Another approach is having the applications journal the events (write the events to a database), and then the clients poll that journal database for new events. At AppRiver, the latter approach has proven to be a good fit when dealing with local cache synchronization. Below is an example of a hosted service polling a journaling database:

public class JournalingManagerService : BackgroundService
{
private readonly IJournalingUpdateService _journalingUpdateService;

public JournalingManagerService(IJournalingUpdateService journalingUpdateService)
{
_journalingUpdateService = journalingUpdateService;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await _journalingUpdateService.Update(stoppingToken);
await Task.Delay(3000, stoppingToken);
}

await Task.CompletedTask;
}
}

To keep this hosted service example simple, all of the logic around polling and updating the database is abstracted away in the JournalingUpdateService.

Once the application starts, the HostedServiceExecutor will kick off all the IHostedServices registered in the dependency injection in the Startup class (more on DI later). The JournalingManagerService’s ExecuteAsync method will be called. This will call the Update method on the JournalingUpdateService and then it will wait for 3 seconds before repeating. The service will continue to run until the token is canceled / application is stopped. The BackgroundService handles a lot of the work around service lives and if you are interested in more information on that, check out this blog post about the HostedService class.

The last part involves wiring up the hosted services. Each IHostedService will need registering (using dependency injection) in the ConfigureServices method in the Startup class (Note: multiple hosted services can be registered). This will look something like this:

public void ConfigureServices(IServiceCollection services)
{
// other DI registration
services.AddSingleton<IHostedService, JournalingManagerService >();
services.AddSingleton<IHostedService, FooHostedService >();
}

Summary

The IHostedService interface is an easy way to run background tasks with a web application or host. Its main benefit is how gracefully it handles the clean up when the application is stopping. I would warn against overusing this feature and running a large number of tasks.

I’d recommend trying out the IHostedService interface and thinking about where it can be used in your architecture.

I’ve put together a sample repository to try out a hosted service. This repository uses an asynchronous messaging framework to journal integration events to a database. There are two microservices, Billing.API and Customer.API. The Billing.API keeps a local cache of the customer’s name. When the Customer.API updates the customer name it will publish an event to the journaling database. The Billing.API has a hosted service called JournalingManagerService which will poll the journaling database every three seconds to check for updated events.

Additional Resources

Comments

Subscribe Here!