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 theApplicationStarted
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.
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 MigratorStartupFilter(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
// Create a new scope to retrieve scoped services
using(var scope = _seviceProvider.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.