How to run asynchronous tasks when the application starts in ASP.NET Core? In this article, I showed my last post on the “manually run tasks from the proposed adapted Program.cs ” approach. The implementation uses some simple interfaces and classes to encapsulate the logic of tasks that are running when the application starts. I also showed that another way is to use the service decorated IServer
Kestrel to perform the task before starting.
run asynchronous tasks when the application starts in ASP.NET Core
To recap, we tried to find a solution that would allow us to perform arbitrary asynchronous tasks when the application starts. These tasks should begin accepting requests in the application before the execution, but may need to configure the service and should therefore be in the DI configuration is complete after execution. Examples include things like database migration or populating caches.
The solution I proposed at the end of the previous article involves running tasks “manually” between calls to ProgramIWebHostBuilder.Build()
and .csIWebHost.Run()
. How to run asynchronous tasks when the application starts in ASP.NET Core.
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>(); }
How to run asynchronous tasks when the application starts in ASP.NET Core
This method works, but it is a bit confusing. We will inflate the Program.cs file with code that probably shouldn’t exist, but we can easily extract it into another class. The more problem is that you must remember to call the task manually. If you use the same pattern in multiple applications, it is best to handle this pattern for you automatically. How to run asynchronous tasks when the application starts in ASP.NET Core.
Use DI container registration to start tasks
The solution I propose is based on IStartupFilter
and the pattern used IHostedService
. These interfaces allow you to inject the container registration class into the dependency, which will be executed in the future. How to run asynchronous tasks when the application starts in ASP.NET Core.
First, we create a simple interface for starting the task:
public interface IStartupTask { Task ExecuteAsync(CancellationToken cancellationToken = default); }
How to run asynchronous tasks when the application starts in ASP.NET Core
There is also a convenient way to use DI container registration to start tasks:
public static class ServiceCollectionExtensions { public static IServiceCollection AddStartupTask<T>(this IServiceCollection services) where T : class, IStartupTask => services.AddTransient<IStartupTask, T>(); }
How to run asynchronous tasks when the application starts in ASP.NET Core
Finally, we added an extension method that can IStartupTask
find all registered ones when the application starts, How to run asynchronous tasks when the application starts in ASP.NET Core. run them in turn, and then start 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); } }
How to run asynchronous tasks when the application starts in ASP.NET Core
Everything here is its!
To see it in action, I will use the migration from the EF core database for example in the previous post.
An example-asynchronous database migration
Implementation IStartupTask
is very similar to implementation IStartupFilter
. You can inject the service from the DI container, but if you need to access IServiceProvider
the scoped service, you should inject and manually create a new scope.
Side note-it seems that many people make mistakes, so I consider
RunWithTasksAsync
automatically creating a new scope for each task in the extension method. In this way, you can directly inject services within the scopeIStartupTask
. I decided to aim for consistent behaviorIStartupFilter
andIHostedService
-I will be interested in any ideas for people who have opinions.
The EF Core migration startup task will be similar to 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(); } } }
How to run asynchronous tasks when the application starts in ASP.NET Core
How to run asynchronous tasks when the application starts in ASP.NET Core. Then add the startup task to the DI container 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>(); }
How to run asynchronous tasks when the application starts in ASP.NET Core
Finally, we need to update Program.cs to make the call RunWithTasksAsync()
instead ofRun()
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>(); }
How to run asynchronous tasks when the application starts in ASP.NET Core
This takes advantage of async Task Main
the features of C#7.1. In general, this code is functionally equivalent to the “manual” equivalent in the previous and later versions, but it has some advantages.
- It keeps the task implementation code in Program.cs
- I think it’s easier to understand what’s happening in Program.cs -we are running a startup task and then running the application. In most cases, only the implementation code is moved out of Program.cs
- You can easily add them by adding them to the DI container.
- If you don’t have any tasks, the behavior is the same as calling
RunAsync()
For me, the biggest benefit is that once added RunWithTasksAsync()
, you can easily add other tasks by adding other tasks to the DI container without any other changes.
Thomas Levesque recently wrote a similar article to solve the same problem and proposed a similar solution. He has a NuGet package suitable for this method.
Although not exactly sunshine and roses…
Small font-we have not finished building the application
Apart from the fact that it is not included in the framework (so people have to customize their Program.cs class), I have a small caveat about the method shown above . Even if the task after running IConfiguration
configuration and DI container has been completed, they run in front of IStartupFilter
■ find and run the middleware pipelines have been configured.
Personally, this is not a problem for me, and I can’t think of anything. All tasks I write do not depend on what IStartupFilter
is already running. But this does not mean that it will not happen.
Unfortunately, it is WebHost
not easy to solve this problem with the current code (although it may be changed in 3.0 when ASP.NET Core is running IHostedService
). The problem is that the application is booted (by configuring the middleware pipeline and running IStartupFilter
s) and starts with the same function. When you call Program.Cs , it will WebHost.Run()
be called internally, as shown below, with logging, and some other minor code removed for brevity:WebHost.StartAsync
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); }
How to run asynchronous tasks when the application starts in ASP.NET Core
The problem is that we want to BuildApplication()
insert code between the calls to and Server.StartAsync()
, but there is no mechanism for doing so. How to run asynchronous tasks when the application starts in ASP.NET Core.
I’m not sure if the solution I chose is clumsy or elegant, but it works and provides a better experience for consumers because they don’t need to modify Program.cs …
Another way to decorate IServer
I can see that BuildApplication()
the only way to run asynchronous code between and Server.StartAsync()
is IServer
to replace the implementation (Kestrel) with our own code! This is not as scary as it sounds at first-we didn’t really want to replace the server, we just decorated 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); }
How to run asynchronous tasks when the application starts in ASP.NET Core
The TaskExecutingServer
required instance is IServer
in its construction-it turns out that this KestrelServer
is registered by ASP.NET Core. We IServer
delegate most of the implementation directly to Kestrel, we just intercept the call to it StartAsync
and run the injected task first. How to run asynchronous tasks when the application starts in ASP.NET Core.
The hard part of the implementation is getting the decoration to work properly. As I discussed in the previous article, using decorations with the default ASP.NET Core container can be tricky. I usually use Scrutor to create decorators, but if you don’t want to depend on another library, you can always decorate manually. Be sure to check how Scrutor is guided!
The extension method shown below for adding a has IStartupTask
two things-it IStartupTask
registers one in the DI container and decorates a previously registered IServer
instance ( Decorate()
I omitted the implementation for brevity). If found IServer
is already decorated, it skips the second step. This way, you can AddStartupTask<T>()
safely make any call:
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>(); }
How to run asynchronous tasks when the application starts in ASP.NET Core
With these two pieces of code, we do not need their users to make any changes in Program.cs file plus we perform a task, the application has been fully completed, including IStartupFilters
and middleware pipeline.
Now, the sequence diagram of the startup process looks like this:
This is almost all of it. With so little code, I’m not sure if it’s worth making it into a library, but it’s still published on GitHub and NuGet!
I decided to write only a package for the latter method because it is easy to use, and Thomas Levesque has provided a NuGet package for the first method.
In the implementation on GitHub, I manually constructed decorations (borrowed heavily from Scrutor) to avoid imposing a dependency on Scrutor. But the best way might just be to copy and paste the code into your own project and start from there!
Conclusion
In this article, I showed two possible ways to run tasks asynchronously when the application starts while preventing the actual startup process. The first method requires a slight modification to Program.cs but is “safer” because it does not need to mess up internal implementation details such as IServer. The second method of decorating can IServer
provide a better user experience, but it feels more clumsy. I want to know which method people think is better and why, so please let me know in the comments!