In my previous post I described the need to run one-off asynchronous tasks on app start up. This post follows on directly from the last one, so if you haven't already, I suggest you read that one first.
In this post I show a proposed adaptation of the "run the task manually in program.cs" approach from my last post. The implementation uses a few simple interfaces and classes to encapsulate the logic of running tasks on app start up. I also show an alternative approach that uses service decoration of IServer
to execute tasks before Kestrel starts.
Running asynchronous tasks on app startup
As a recap, we're trying to find a solution that allows us to execute arbitrary asynchronous tasks on app start up. These tasks should be executed before the app starts accepting requests, but may require configuration and services, so should be executed after DI configuration is complete. Examples include things like database migrations, or populating a cache.
The solution I proposed at the end of my previous post involved running the task "manually" in Program.cs, between the calls to IWebHostBuilder.Build()
and IWebHost.Run()
:
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 listeningaccepting 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 approach works, but it's a bit messy. We're going to be bloating the Program.cs file with code that probably shouldn't be there, but we could easily extract that to another class. More of an issue is having to remember to invoke the task manually. If you're using the same pattern in multiple apps, then it would be nice to have this handled for you automatically.
Registering startup tasks with the DI container
The solution I'm proposing is based on the patterns used by IStartupFilter
and IHostedService
. These interfaces allow you to register classes with the dependency injection container which will be executed later.
First, we create a simple interface for the startup tasks:
public interface IStartupTask
{
Task ExecuteAsync(CancellationToken cancellationToken = default);
}
And a convenience method for registering startup tasks with the DI container:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddStartupTask<T>(this IServiceCollection services)
where T : class, IStartupTask
=> services.AddTransient<IStartupTask, T>();
}
Finally, we add an extension method that finds all the registered IStartupTask
s on app startup, runs them in order, and then starts the IWebHost
:
public static class StartupTaskWebHostExtensions
{
public static async Task RunWithTasksAsync(this IWebHost webHost, CancellationToken cancellationToken = default)
{
// Load all tasks from DI
var startupTasks = webHost.Services.GetServices<IStartupTask>();
// Execute all the tasks
foreach (var startupTask in startupTasks)
{
await startupTask.ExecuteAsync(cancellationToken);
}
// Start the tasks as normal
await webHost.RunAsync(cancellationToken);
}
}
That's all there is to it!
To see it in action, I'll use the EF Core database migration example from the previous post.
An example - async database migration
Implementing IStartupTask
is very similar to implementing IStartupFilter
. You can inject services from the DI container, but if you require access to Scoped services, you should inject an IServiceProvider
and create a new scope manually.
Side note - this seems like something a lot of people will get wrong, so I considered automatically creating a new scope for every task in the
RunWithTasksAsync
extension method. That would let you directly inject scoped services into theIStartupTask
. I decided against that to keep the behaviour consistent withIStartupFilter
andIHostedService
- I'd be interested in any thoughts people have in the comments.
The EF Core migration startup task would look something like the following:
public class MigratorStartupFilter: IStartupTask
{
// 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 Task ExecuteAsync(CancellationToken cancellationToken = default)
{
// 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
await myDbContext.Database.MigrateAsync();
}
}
}
And we'd add the startup task to the DI container in ConfigureServices()
:
public void ConfigureServices(IServiceCollection services)
{
services.MyDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
// Add migration task
services.AddStartupTask<MigrationStartupTask>();
}
Finally, we need to update Program.cs to call RunWithTasksAsync()
instead of Run()
public class Program
{
// Change return type from void to async Task
public static async Task Main(string[] args)
{
// CreateWebHostBuilder(args).Build().Run();
await CreateWebHostBuilder(args).Build().RunWithTasksAsync();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
This takes advantage of the async Task Main
feature of C# 7.1. Overall this code is functionally equivalent to the "manual" equivalent from my last post and above, but it has a few advantages.
- It keeps the task implementation code out of Program.cs
- I think it's easier to understand what's happening in Program.cs - we're running startup tasks and then running the application. Most of that is due to simply moving implementation code out of Program.cs
- It allows you to easily add extra tasks by adding them to the DI container.
- If you don't have any tasks, the behaviour is the same as calling
RunAsync()
For me, the biggest advantage is that once you have added RunWithTasksAsync()
, you can easily add additional tasks by adding them to the DI container, without having to make any other changes.
Thomas Levesque recently wrote a similar post tackling the same problem, and came to a similar solution. He has a NuGet package available for the approach.
It's not entirely sunshine and roses though…
The small print - we haven't quite finished building the app yet
Other than the fact this isn't baked into the framework (so people have to customize their Program.cs classes), there's one tiny caveat I see to the approach shown above. Even though the tasks run after the IConfiguration
and DI container configuration has completed, they run before the IStartupFilter
s have run and the middleware pipeline has been configured.
Personally, this hasn't been a problem for me, and I can't think of any cases where it would be. None of the tasks I've written have a dependency on the IStartupFilter
s having run. That doesn't mean it won't happen though.
Unfortunately, there's not an easy way round this with the current WebHost
code (though that may change in 3.0 when ASP.NET Core runs as an IHostedService
). The problem is that the application is bootstrapped (by configuring the middleware pipeline and running IStartupFilter
s) and started in the same function. When you call WebHost.Run()
in Program.Cs, this internally calls WebHost.StartAsync
which is shown below with logging and some other minor code removed for brevity:
public virtual async Task StartAsync(CancellationToken cancellationToken = default)
{
_logger = _applicationServices.GetRequiredService<ILogger<WebHost>>();
// Build the middleware pipeline and execute IStartupFilters
var application = BuildApplication();
_applicationLifetime = _applicationServices.GetRequiredService<IApplicationLifetime>() as ApplicationLifetime;
_hostedServiceExecutor = _applicationServices.GetRequiredService<HostedServiceExecutor>();
var diagnosticSource = _applicationServices.GetRequiredService<DiagnosticListener>();
var httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>();
var hostingApp = new HostingApplication(application, _logger, diagnosticSource, httpContextFactory);
// Start Kestrel (and start accepting connections)
await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false);
// Fire IApplicationLifetime.Started
_applicationLifetime?.NotifyStarted();
// Fire IHostedService.Start
await _hostedServiceExecutor.StartAsync(cancellationToken).ConfigureAwait(false);
}
The problem is that we want to insert code between the call to BuildApplication()
and the call to Server.StartAsync()
, but there's no mechanism for doing so.
I'm not sure if the solution I settled on feels hacky or elegant, but it works, and gives an even nicer experience for consumers, as they don't need to modify Program.cs…
An alternative approach by decorating IServer
The only way I could see to run async code between BuildApplication()
and Server.StartAsync()
is to replace the IServer
implementation (Kestrel) with our own! This isn't quite as horrendous as it sounds at first - we're not really going to replace the server, we're just going to decorate it:
public class TaskExecutingServer : IServer
{
// Inject the original IServer implementation (KestrelServer) and
// the list of IStartupTasks to execute
private readonly IServer _server;
private readonly IEnumerable<IStartupTask> _startupTasks;
public TaskExecutingServer(IServer server, IEnumerable<IStartupTask> startupTasks)
{
_server = server;
_startupTasks = startupTasks;
}
public async Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken)
{
// Run the tasks first
foreach (var startupTask in _startupTasks)
{
await startupTask.ExecuteAsync(cancellationToken);
}
// Now start the Kestrel server properly
await _server.StartAsync(application, cancellationToken);
}
// Delegate implementation to default IServer
public IFeatureCollection Features => _server.Features;
public void Dispose() => _server.Dispose();
public Task StopAsync(CancellationToken cancellationToken) => _server.StopAsync(cancellationToken);
}
The TaskExecutingServer
takes an instance of IServer
in its constructor - this the original KestrelServer
registered by ASP.NET Core. We delegate most of the IServer
implementation directly to Kestrel, we just intercept the call to StartAsync
and run the injected tasks first.
The difficult part of the implementation is getting the decoration working properly. As I discussed in a previous post, using decoration with the default ASP.NET Core container can be tricky. I typically use Scrutor to create decorators, but you could always do the decoration manually if you don't want to take a dependency on another library. Be sure to look at how Scrutor does it for guidance!
The extension method shown below for adding an IStartupTask
does two things - it registers an IStartupTask
with the DI container, and it decorates a previously-registered IServer
instance (I've left out the Decorate()
implementation for brevity) . If it finds that the IServer
is already decorated, it skips the second step. That way you can call AddStartupTask<T>()
safely any number of times:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddStartupTask<TStartupTask>(this IServiceCollection services)
where TStartupTask : class, IStartupTask
=> services
.AddTransient<IStartupTask, TStartupTask>()
.AddTaskExecutingServer();
private static IServiceCollection AddTaskExecutingServer(this IServiceCollection services)
{
var decoratorType = typeof(TaskExecutingServer);
if (services.Any(service => service.ImplementationType == decoratorType))
{
// We've already decorated the IServer
return services;
}
// Decorate the IServer with our TaskExecutingServer
return services.Decorate<IServer, TaskExecutingServer>();
}
}
With these two pieces of code we no longer require users to make any changes to their Program.cs file plus we execute our tasks after the application has been fully built, including IStartupFilters
and the middleware pipeline.
The sequence diagram of the startup process now looks a bit like this:
That's pretty much all there is to it. It's such a small amount of code that I wasn't sure it was worth making into a library, but it's out on GitHub and NuGet nonetheless!
I decided to only write a package for the latter approach as it's so easier to consume, and Thomas Levesque already has a NuGet package available for the first approach.
In the implementation on GitHub I manually constructed the decoration (heavily borrowing from Scrutor), to avoid forcing a dependency on Scrutor. But the best approach is probably to just copy and paste the code into your own projects 🙂 and go from there!
Summary
In this post I showed two possible ways to run tasks asynchronously on app start up while blocking the actual startup process. The first approach requires modifying your Program.cs slightly, but is "safer" in that it doesn't require messing with internal implementation details like IServer
. The second approach, decorating IServer
, gives a better user experience, but feels more heavy-handed. I'd be interested in which approach people feel is the better one and why, so do let me know in the comments!