Quantcast
Channel: Andrew Lock | .NET Escapades
Viewing all articles
Browse latest Browse all 743

Running async tasks on app startup in ASP.NET Core (Part 1)

$
0
0
Running async tasks on app startup in ASP.NET Core (Part 1)

Sometimes you need to perform one-off initialisation logic before your app starts up properly. For example, you might want to validate your configuration is correct, populate a cache, or run database migrations. In this post, I look at the options available and show some simple methods and extension points that I think solve the problem well.

I start by describing the built-in solution to running synchronous tasks with IStartupFilter. I then walk through the various options for running asynchrnous tasks. You could (but possibly shouldn't) use IStartupFilter or IApplicationLifetime events to run asynchronous tasks. You could use the IHostedService interface to run one-off tasks without blocking app startup. However the only real solution is to run the tasks manually in program.cs. In my next post I'll show a suggested proposal that makes this process a little easier.

Why do we need to run tasks on app startup?

It's very common to need to run various sorts of initialisation code before an app is able to start up and begin serving requests. In an ASP.NET Core app there are clearly lots of things that need to happen, for example:

  • Determining the current hosting environment
  • Loading of configuration from appsettings.json and environment variables
  • Configuration of the dependency injection container
  • Building of the dependency injection container
  • Configuration of the middleware pipeline

All of these steps need to occur to bootstrap the application. However there are often one-off tasks you want to perform before the WebHost is run and starts listening for requests. For example:

Sometimes these tasks don't have to be run before your app starts serving requests. The cache priming example for instance - if its a well behaved cache, then it shouldn't matter if the cache is queried before it's primed. On the other hand, you almost certainly want your database to be migrated before your app starts serving requests!

There are some examples of where one-off initialisation tasks are required by the ASP.NET Core framework itself. A good example of this is the Data Protection subsystem, used for transient encryption (cookie values, anti-forgery tokens etc). This subsystem must be initialised before the app can start handling any requests. To handle this, they use a IStartupFilter.

Running tasks synchronously with IStartupFilter

I've written previously about IStartupFilter, as it's a really useful interface to have in your tool belt for customising your apps:

If you're new to the filter, I recommend reading my introductory post, but I'll provide a brief summary here.

IStartupFilters are executed as part of the process of configuring your middleware pipeline (typically done in Startup.Configure()). They allow you to customise the middleware pipeline that's actually created by the app, by inserting extra pieces of middleware, forking it, or doing any number of other things. For example, the AutoRequestServicesStartupFilter shown below inserts a new piece of middleware at the start of your pipeline:

public class AutoRequestServicesStartupFilter : IStartupFilter
{
    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        return builder =>
        {
            builder.UseMiddleware<RequestServicesContainerMiddleware>();
            next(builder);
        };
    }
}

This is useful, but what does it have to do with running one-off tasks on app startup?

The key feature IStartupFilter provides is a hook into the early app startup process, after the configuration is set and the dependency injection container is configured, but before the app is ready to start. That means you can use dependency injection with IStartupFilters and so can run pretty much any code. The DataProtectionStartupFilter for example is used to initialise the Data Protection system. I used a similar IStartupFilter approach to provide eager validation of strongly typed configuration.

The other very useful feature is it allows you to add tasks to be executed by registering a service with the DI container. That means as a library author, you could register a task to be run on app startup, without the app author having to invoke it explicitly.

So why can't we just use IStartupFilter to run asynchronous tasks on startup?

The problem is that the IStartupFilter is fundamentally synchronous. The Configure() method (which you can see in the code above) doesn't return a Task, so it's not a good idea to be trying to do sync over async. I'll discuss this a little later, but for now a quick detour.

Why not use health checks?

ASP.NET Core 2.2 introduces a health checks feature for ASP.NET Core applications, which allows you to query the "health" of an application, exposed via an HTTP endpoint. When deployed, orchestration engines like Kubernetes, or reverse proxies like HAProxy and NGINX can query this endpoint to check if your app is ready to start receiving requests.

You could use the health check feature to ensure your application doesn't start servicing requests (i.e. returning a "healthy" status from the health check endpoint) until all the required one-off tasks are complete. However this has a few downsides:

  • The WebHost and Kestrel itself would startup before the one-off tasks have been executed. While they wouldn't receive "real" requests (only health check requests) that might still be an issue.
  • It introduces extra complexity. As well as adding the code to run the one-task, you need to add a health check to test if the task completed, and synchronise the status of the task.
  • The start of the app would still be delayed until all the tasks have completed, so it's unlikely to reduce startup time.
  • If a task fails, the app would continue to run in a "dead" state, where the health check would never pass. That might be acceptable, but personally I prefer an app to fail immediately.
  • The health checks aspect still doesn't define how to actually run the task, just whether the task completed successfully. You still need to decide on a mechanism to run the tasks on startup.

For me, health checks doesn't seem like the right fit for the one-off tasks scenario. They may well be useful for some of the examples I've described, but I don't think they fit all cases. I really want to be able to run my one-off tasks on app startup, before the WebHost is run

Running asynchronous tasks

I've spent a long time discussing all the ways not to achieve my goal, how about some solutions! In this section I walk through some of the possibilties for running asynchronous tasks (i.e. tasks that return a Task and require await-ing). Some are better than others, and some you should avoid, but I wanted to cover the various options.

To give something concrete to discuss, I'll consider the database migration example. In EF Core, you can migrate a database at runtime by calling myDbContext.Database.MigrateAsync(), where myDbContext is an instance of your application's DbContext.

There's also a synchronous version of the method, Database.Migrate(), but just pretend there isn't for now!

1. Using IStartupFilter

I described earlier how you can use IStartupFilter to run synchronous tasks on app startup. Unfortunately the only way to run asynchronous tasks would be to use a "sync over async" approach, in which we call GetAwaiter().GetResult():

Warning: this code uses bad async practices.

public class MigratorStartupFilter: IStartupFilter
{
    // 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 Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        // 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 by blocking the async call
            myDbContext.Database.MigrateAsync()
                .GetAwaiter()   // Yuk!
                .GetResult();   // Yuk!
        }

        // don't modify the middleware pipeline
        return next;
    }
}

It's very possible that this won't cause any issues - this code is only running on app startup, before we're serving requests, and so deadlocks seem unlikely. But frankly, I couldn't say for sure and I avoid code like this wherever possible.

David Fowler is putting together some great (work in progress) guidance on doing asynchronous programming correctly. I highly recommend reading it!

2. Using IApplicationLifetime events

I haven't discussed this much before, but you can receive notifications when your app is starting up and shutting down with the IApplicationLifetime interface. I won't go into detail about it here, as it has a few problems for our purposes:

  • IApplicationLifetime uses CancellationTokens for registering callbacks, which means you can only execute callbacks synchronously. This essentially means your stuck with a sync over async pattern, whatever you do.
  • The ApplicationStarted event is only raised after the WebHost is started, so the tasks are run after the app starts accepting requests.

Given they don't solve the sync over async problem of IStartupFilters, and don't block app startup, we'll leave IApplicationLifetime and move on to the next possibility.

3. Using IHostedService to run asynchronous tasks

IHostedService allows ASP.NET Core apps to execute long-running tasks in the background, for the duration of the app's lifetime. They have many different uses - you could use them to run periodic tasks on a timer, to handle other messaging paradigms, such as RabbitMQ messages, or any number of other things. In ASP.NET Core 3.0, even the ASP.NET web host will likely be built on top of IHostedService.

The IHostedService is inherently asynchronous, with both a StartAsync and a StopAsync function. This is great for us, as it means no more sync over async! Implementing a database migrator as a hosted service might look like something like this:

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

    public Task StopAsync(CancellationToken cancellationToken)
    {
        // noop
        return Task.CompletedTask;
    }
}

Unfortunately, IHostedService isn't the panacea we might hope for. It allows us to write true async code, but it has a couple of problems:

  • The typical implementation for IHostedServices expects the StartAsync function to return relatively quickly. For background services, it's expected that you'll start the service asynchronously, but that the bulk of the work will occur outside of that startup code (see the docs for an example). Migrating the database "inline" isn't a problem as such, but it will block other IHostedServices from starting, which may or may not be expected.
  • IHostedService.StartAsync() is called after the WebHost is started, so you can't use this approach to run tasks before your app starts up.

The biggest problem is the second one - the app will start accepting requests before the IHostedService has run the database migrations, which isn't what we want. Back to the drawing board.

4. Manually running tasks in Program.cs

None of the solutions shown so far offer the complete solution. They either require using sync over async programming, (which while probably ok in the context of application startup, isn't great to encourage), or don't block app startup. There's a simple solution I've ignored so far, and that's to stop trying to use framework mechanisms, and just do the job ourselves.

The default Program.cs used in the ASP.NET Core templates builds and runs an IWebHost in one statement in the Main function:

public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

However, there's nothing stopping you from running code after Build() has created the WebHost, but before you call Run(). Add to that the C# 7.1 feature of allowing your Main function to be async, and we have a reasonable solution:

public class Program
{
    public static async Task Main(string[] args)
    {
        IWebHost webHost = CreateWebHostBuilder(args).Build();

        // Create a new scope
        using (var scope = webHost.Services.CreateScope())
        {
            // Get the DbContext instance
            var myDbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();

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

        // Run the WebHost, and start accepting requests
        // There's an async overload, so we may as well use it
        await webHost.RunAsync();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

This solution has a number of advantages:

  • We're now doing true async, no sync over async required
  • We can execute the task asynchronously
  • The app doesn't accept requests until after our tasks have executed
  • The DI container has been built at this point, so we can use it to create services.

It's not all good news unfortunately. We're still missing a couple of things:

  • Even though the DI container has been built, the middleware pipeline has not. That doesn't happen until you call Run() or RunAsync() on the IWebHost. At that point the middleware pipeline is built, the IStartupFilters are executed, and the app is started. If your async task requires configuration that happens within any of these steps, you're out of luck
  • We've lost the ability to automatically run tasks by adding a service to the DI container. We have to remember to manually run the task instead.

If those caveats aren't a problem, then I think this final option provides the best solution to the problem. In my next post I'll show a couple of ways we can take this basic example and build on it, to make something a little easier to use.

Summary

In this post I discussed the need to run tasks asynchronously on app startup. I described some of the challenges of doing this. For synchronous tasks, IStartupFilter provides a useful hook into the ASP.NET Core app startup process, but running asynchronous tasks requires doing sync over async programming which is generally a bad idea. I described a number of the possible options for running async tasks, the best of which I found is "manually" running the task in Program.cs, between building the IWebHost and running it. In the next post I'll present some code to make this pattern easier to use.


Viewing all articles
Browse latest Browse all 743

Trending Articles