Asynchronous initialization in ASP.NET Core with custom middleware

Update: I no longer recommend the approach described in this post. I propose a better solution here: Asynchronous initialization in ASP.NET Core, revisited.

Sometimes you need to perform some initialization steps when your web application starts. However, putting such code in the Startup.Configure method is generally not a good idea, because:

  • There’s no current scope in the Configure method, so you can’t use services registered with "scoped" lifetime (this would throw an InvalidOperationException: Cannot resolve scoped service ‘MyApp.IMyService’ from root provider).
  • If the initialization code is asynchronous, you can’t await it, because the Configure method can’t be asynchronous. You could use .Wait to block until it’s done, but it’s ugly.

Async initialization middleware

A simple way to do it involves writing a custom middleware that ensures initialization is complete before processing a request. This middleware starts the initialization process when the app starts, and upon receiving a request, will wait until the initialization is done before passing the request to the next middleware. A basic implementation could look like this:

public class AsyncInitializationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;
    private Task _initializationTask;

    public AsyncInitializationMiddleware(RequestDelegate next, IApplicationLifetime lifetime, ILogger<AsyncInitializationMiddleware> logger)
    {
        _next = next;
        _logger = logger;

        // Start initialization when the app starts
        var startRegistration = default(CancellationTokenRegistration);
        startRegistration = lifetime.ApplicationStarted.Register(() =>
        {
            _initializationTask = InitializeAsync(lifetime.ApplicationStopping);
            startRegistration.Dispose();
        });
    }

    private async Task InitializeAsync(CancellationToken cancellationToken)
    {
        try
        {
            _logger.LogInformation("Initialization starting");

            // Do async initialization here
            await Task.Delay(2000);

            _logger.LogInformation("Initialization complete");
        }
        catch(Exception ex)
        {
            _logger.LogError(ex, "Initialization failed");
            throw;
        }
    }

    public async Task Invoke(HttpContext context)
    {
        // Take a copy to avoid race conditions
        var initializationTask = _initializationTask;
        if (initializationTask != null)
        {
            // Wait until initialization is complete before passing the request to next middleware
            await initializationTask;

            // Clear the task so that we don't await it again later.
            _initializationTask = null;
        }

        // Pass the request to the next middleware
        await _next(context);
    }
}

We can then add this middleware to the pipeline in the Startup.Configure method. It should be added early in the pipeline, before any other middleware that would need the initialization to be complete.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseMiddleware<AsyncInitializationMiddleware>();

    app.UseMvc();
}

Dependencies

At this point, our initialization middleware doesn’t depend on any service. If it has transient or singleton dependencies, they can just be injected into the middleware constructor as usual, and used from the InitializeAsync method.

However, if the dependencies are scoped, we’re in trouble: the middleware is instantiated directly from the root provider, not from a scope, so it can’t take scoped dependencies in its constructor.

Depending on scoped dependencies for initialization code doesn’t make a lot of sense anyway, since by definition scoped dependencies only exist in the context of a request. But if for some reason you need to do it anyway, the solution is to perform initialization in the middleware’s Invoke method, injecting the dependencies as method parameters. This approach has at least two drawbacks:

  • Initialization won’t start until a request is received, so the first requests will have a delayed response time; this can be an issue if the initialization takes a long time.
  • You need to take special care to ensure thread safety: the initialization code must run only once, even if several requests arrive before initialization is done.

Writing thread-safe code is hard and error-prone, so avoid getting in this situation if possible, e.g. by refactoring your services so that your initialization middleware doesn’t depend on any scoped service.

17 thoughts on “Asynchronous initialization in ASP.NET Core with custom middleware”

    1. Hi Peter,

      As far as I can tell, a hosted service wouldn’t ensure the initialization is done before processing requests

  1. Nice one! But there is no need to set the task variable to null. If you `await` a completed task it will *not* be executed again (like an Action or Func would), it will return immediately (and returning the original result, if it is a Task).
    This means that the Invoke method can be simplified into:

    public async Task Invoke(HttpContext context)
    {
    // Wait until initialization is complete before passing the request to next middleware
    await _initializationTask;

    // Pass the request to the next middleware
    await _next(context);
    }

    1. Hi Mattias,

      Indeed, it would work. It’s just slightly more efficient if you don’t await at all (just checking for null is faster than calling .GetAwaiter().IsCompleted)

  2. RE: Scoped Initialisation,

    I think it would be better (although I agree that I do not think it makes sense to eagerly initialise scoped services) to create a new `ScopedAsyncInitializationMiddleware` class, which gets registered to a ScopedLifetime and which takes the services to initialise as constructor params and initialises them.

    Just my 2 cents, but nice article!

  3. Not sure it makes sense to have startup code in middleware that runs for every request. Startup.cs is the correct place for startup code (or a class that is called from there). It is true that you cannot used scoped dependencies here but that is a good thing. A scoped dependency is supposed to provide a per request lifetime and at startup there is obviously no request context. Async code at startup is slightly awkward right now but a separate ConfigureServicesAsync method can be added and called from ConfigureServices via a slightly nasty but perfectly legitimate call: ConfigureServicesAsync(services).GetAwaiter().GetResult()

    1. Hi Ardy,

      The initialization code doesn’t run for every request. The middleware just ensures initialization is complete.
      Yes, ConfigureServicesAsync(services).GetAwaiter().GetResult() (which is the same as ConfigureServicesAsync(services).Wait()) would work, but as I mentioned, it’s slightly ugly, so I wanted to avoid it.

  4. The init code doesn’t run for every request but the middleware and the check does when is a little odd.

    BTW GetAwaiter().GetResult() provides a better stacktrace than Wait() so is generally preferred.

    1. What we do, when we build web host (configuring whole server), we run the async init work and after all is done then we call IWebHost.StartAsync()

      => that way we dont need to check on every request if init task is not null.

      IMHO, using middleware for this is quite odd, just my 2 cents

      1. Hi @Frantisek,

        Yes, you could do that, but that means you can’t use the services defined in Startup.ConfigureServices

          1. Ah, I see. Yes, you’re right of course, it’s probably a better approach in most cases.

            I see one case where it could be useful to do it in a middleware, though. In my example, I just blocked until initialization was completed, but instead the middleware could return a 503 error to indicate that the server is starting. With your approach, the server just wouldn’t be listening yet, so the connection would just fail.

  5. It depends what you use as a shell. I.e. we run in Azure Paasv1 and we signal to the environment -> which means to load balancer to be ready ONLY and ONLY when the initialization is done. So we don’t have any 503 and we don’t make middleware chain dirty. Middlewares aren’t designed for this. Honestly, I think you are misusing them

  6. just to add more clarity: we signal to the environment -> which means to load balancer to be ready ONLY and ONLY when the initialization is done and the server is started.

    same applies when we need to warm-up the instance. it’s not good to run all the initialization inside the middleware.

Leave a Reply

Your email address will not be published. Required fields are marked *