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

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:

  • Manually executing tasks after the WebHost is built, but before it’s run.
  • Using a custom IServer implementation to run the tasks when the server is started, before it starts receiving requests. Unfortunately this approach can have issues.

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.

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(); //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(); } 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, but the change in 3.0 means IHostedService can be used to fulfil that role.