ASP.NET Core background processing with IHostedService

Run background processes with the IHostedService and how to inject your services with dependency injection

Many services need background processing. The ASP.NET Core 2.X IHostedService interface gives you an easy implementation skeleton to implement background processes. The Hosted Services are registered in the dependency injection at startup and started automatically. You do not have to do the pluming to get them started at startup. On shutdown you can implement a graceful shutdown. When running background processes there a few pitfalls to avoid. In this blog I’ll introduce the IHostedService and how to avoid common memory leaks when implementing the hosted service.

Using IHostedService
When implementing the IHostedService interface, you have two methods to implement: StartAsync() and StopAsync(). The StartAsync is called at startup and the StopAsync is called at shutdown. The implementation of the class will inject the dependencies needed to run your business logic.

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);
}

An implementation of the IHostedService interface can be added in startup.cs service registration:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddHostedService(); 
    //ASP.NET 2.0 : //services.AddSingleton();
}

When the service is registered, it will be initialized when startup is finished. You implementation of the StartAsync is called where you can start processing.

Start repeating process in IHostedService
A common pattern for background tasks is:

  • Run your logic
  • Wait some time
  • Check if you have to stop or repeat the process

A simple implementation of a base class (inspired by a sample of David Fowler) which takes care of the plumbing can be:

public abstract class BackgroundService : IHostedService
{
    private Task _executingTask;
    private readonly CancellationTokenSource _stoppingCts =
                                                   new CancellationTokenSource();

    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));
        }
    }

    protected virtual async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        //stoppingToken.Register(() =>
        //        _logger.LogDebug($" GracePeriod background task is stopping."));

        do
        {
            await Process();

            await Task.Delay(5000, stoppingToken); //5 seconds delay
        }
        while (!stoppingToken.IsCancellationRequested);
    }

    protected abstract Task Process();
}

This implementation handles a graceful shutdown of the background task. You only have to implement the Process() method to get your work done.

Note that when you leave out the IServiceScopeFactory and only use the IService provider you will create a memory leak if you create any IDisposable objects. These objects are referenced in the Dependency injection scope and only released when your programs ends. Even when you dispose them, the Garbage Collector will not collect them.

Dependency injection into your IHostedServices

The IHostedService implementations are singletons in you application. When injecting a dependency that is scoped or transient they will have the same lifespan as IHostedService. This is probably not the intention of the scoped or transient object. Fortunately when running in development mode, the dependency injector will stop you from running. It checks if the dependency is scoped and then throws an exception.

System.InvalidOperationException: Cannot consume scoped service ‘MyDbContext’ from singleton ‘IMySingleton’.

The error prevents you from running a scoped object (for example a DbContext) in a singleton. To come around this you can use the IServiceScopeFactory create an own scope for each time you are running your process. The scope takes care of all scoped and IDisposable objects created for your processing. The code for this can look like:

    public abstract class ScopedProcessor : BackgroundService
    {
        private readonly IServiceScopeFactory _serviceScopeFactory;

        public ScopedProcessor(IServiceScopeFactory serviceScopeFactory) : base()
        {
            _serviceScopeFactory = serviceScopeFactory;
        }

        protected override async Task Process()
        {
            using (var scope = _serviceScopeFactory.CreateScope())
            {
                await ProcessInScope(scope.ServiceProvider);
            }
        }

        public abstract Task ProcessInScope(IServiceProvider serviceProvider);
    }

In the method ProcessInScope you can create you processing logic object with the serviceProvider. No need for injection and all logic objects are fresh on each processing cycle.

     var processor = serviceProvider.GetService();
     ...

Related posts
Schedule services
Headless services
Using scoped services
Using HttpClientFactory

Final thoughts
The IHostedServices gives you a simple way of implementing background services. The graceful shutdown is very useful. Keep in mind that you manage non singleton objects that are created by the dependency injector with a scope.

In the blog post Run scheduled background tasks in ASP.NET Core you can read more on how to do scheduling of background tasks.

A working demo of a background process/scheduled background process can be found in the following git repository Demo code background processing with IHostedService.

6 thoughts on “ASP.NET Core background processing with IHostedService”

  1. services.AddSingleton(); needs to be services.AddSingleton(); or services.AddSingleton(); right? How can I inject my DbContext into the derived class of ScopedProcessor?

    Like

  2. Just came across BackgroundService in .NET Core 2.1. I can’t understand when ExecuteAsync() is ever called. I create a class based off of BackgroundService, do an AddService(), and I can see StartAsync() and StopAsync() being called, but never ExecuteAsync(). I can inject MyService into a controller but still nothing. Am I misunderstanding something?

    Like

  3. Looks to me like the StartAsync method won’t return until we hit an await in the Process method. Is this right? I would have thought you’d just want to kick the loop off and then consider the BackgroundService started. How can this be written to acheive this?

    Ps. BackgroundService class is now available as part of .net core 2.1 in Microsoft.Extensions.Hosting

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.