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? 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 IServerKestrel to perform the task before starting.

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

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 IStartupTaskfind 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 IStartupTaskis very similar to implementation IStartupFilter. You can inject the service from the DI container, but if you need to access IServiceProviderthe 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 scope IStartupTask. I decided to aim for consistent behavior IStartupFilterand IHostedService-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 Mainthe 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 IConfigurationconfiguration 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 IStartupFilteris already running. But this does not mean that it will not happen.

Unfortunately, it is WebHostnot 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 IStartupFilters) 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 IServerto 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 IServerin its construction-it turns out that this KestrelServeris registered by ASP.NET Core. We IServerdelegate most of the implementation directly to Kestrel, we just intercept the call to it StartAsyncand 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 IStartupTasktwo things-it IStartupTaskregisters one in the DI container and decorates a previously registered IServerinstance ( Decorate()I omitted the implementation for brevity). If found IServeris 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 IStartupFiltersand middleware pipeline.

Now, the sequence diagram of the startup process looks like this:

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

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 IServerprovide 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!

Leave a Comment