blog post image
Andrew Lock avatar

Andrew Lock

~4 min read

Running async tasks on app startup in ASP.NET Core 3.0

Exploring ASP.NET Core 3.0 - Part 4

In this post I describe how a small change in the ASP.NET Core 3.0 WebHost makes it easier to run asynchronous tasks on app startup using IHostedService.

Running asynchronous tasks on app startup.

In a previous series I showed various ways you could run asynchronous tasks on app startup. There are many reasons you might want to do this - running database migrations, validating strongly-typed configuration, or populating a cache, for example.

Unfortunately, in 2.x it wasn't possible to use any of the built-in ASP.NET Core primitives to achieve this:

  • IStartupFilter has a synchronous API, so would require doing sync over async.
  • IApplicationLifetime has a synchronous API and raises the ApplicationStarted event after the server starts handling requests.
  • IHostedService has an asynchronous API, but is executed after the server is started and starts handling requests.

Instead, I proposed two possible solutions:

With ASP.NET Core 3.0, a small change in the WebHost code makes a big difference - we no longer need these solutions, and can use IHostedService without the previous concerns!

A small change makes all the difference

In ASP.NET Core 2.x you can run background services by implementing IHostedService. These are started shortly after the app starts handing requests (i.e. after the Kestrel web server is started), and are stopped when the app shuts down.

In ASP.NET Core 3.0 IHostedService still serves the same purpose - running background tasks. But thanks to a small change in WebHost you can now also use it for automatically running async tasks on app startup.

The change in question is these lines from the WebHost in ASP.NET Core 2.x:

public class WebHost
{
    public virtual async Task StartAsync(CancellationToken cancellationToken = default)
    {
        // ... initial setup
        await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false);

        // Fire IApplicationLifetime.Started
        _applicationLifetime?.NotifyStarted();

        // Fire IHostedService.Start
        await _hostedServiceExecutor.StartAsync(cancellationToken).ConfigureAwait(false);

        // ...remaining setup
    }
}

In ASP.NET Core 3.0, these have been changed to this:

public class WebHost
{
    public virtual async Task StartAsync(CancellationToken cancellationToken = default)
    {
        // ... initial setup
        
        // Fire IHostedService.Start
        await _hostedServiceExecutor.StartAsync(cancellationToken).ConfigureAwait(false);

        // ... more setup
        await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false);

        // Fire IApplicationLifetime.Started
        _applicationLifetime?.NotifyStarted();

        // ...remaining setup
    }
}

As you can see, IHostedService.Start is now executed before Server.StartAsync. This change means you can now use IHostedService to run async tasks.

This assumes that you want to delay your app handling requests until after the async task has completed. If that's not the case, you may want to use the Health Check approach from the last post in my series.

Using an IHostedService to run async tasks on app startup

Implementing an IHostedService as an "app startup" task is not difficult. The interface consists of just two methods:

public interface IHostedService
{
    Task StartAsync(CancellationToken cancellationToken);
    Task StopAsync(CancellationToken cancellationToken);
}

Any code you want to be run just before receiving requests should be placed in the StartAsync method. The StopAsync method can be ignored for this use case.

For example, the following startup task runs EF Core migrations asynchronously on app startup:

public class MigratorHostedService: IHostedService
{
    // We need to inject the IServiceProvider so we can create 
    // the scoped service, MyDbContext
    private readonly IServiceProvider _serviceProvider;
    public MigratorHostedService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // Create a new scope to retrieve scoped services
        using(var scope = _serviceProvider.CreateScope())
        {
            // Get the DbContext instance
            var myDbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();

            //Do the migration asynchronously
            await myDbContext.Database.MigrateAsync();
        }
    }

    // noop
    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

To add the task to the dependency injection container, and have it run just before your app starts receiving requests, use the AddHostedService<> extension method:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // other DI configuration
        services.AddHostedService<MigratorHostedService>();
    }

    public void Configure(IApplicationBuilder)
    {
        // ...middleware configuration
    }
}

The services will be executed at startup in the same order they are added to the DI container, i.e. services added later in ConfigureServices will be executed later on startup.

Summary

In this post I described how a small change in the WebHost in ASP.NET Core 3.0 enables you to more easily run asynchronous tasks on app startup. In ASP.NET Core 2.x there wasn't an ideal option (I proposed various approaches in a previous series), but the change in 3.0 means IHostedService can be used to fulfil that role.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?