Asynchronous code in Startup ASP.NET Core: 4 ways to get around GetAwaiter (). GetResult ()

Since the async / await mechanism was introduced in C # 5.0, we have been constantly taught in all articles and docs that using asynchronous code in synchronous is very bad. And they call for fear like GetAwaiter (). GetResult () constructions. However, there is one case where Microsoft programmers themselves do not disdain this design.



Background about the work task


We are now in the process of transitioning from legacy authentication to OAuth 2.0, which is already a standard in our industry. The service I'm working on now has become a pilot for integration with the new system and for the transition to JWT authentication.

During the integration process, we experimented, considering various options, how to reduce the load on the token provider (IdentityServer in our case) and ensure greater reliability of the entire system. Connecting JWT-based validation to ASP.NET Core is very simple and not tied to a specific implementation of the token provider:

services
      .AddAuthentication()
      .AddJwtBearer(); 

But what is hidden behind these two lines? Under their hood, a JWTBearerHandler is created, which already deals with JWT from the API client.


Interaction of the client, API and the token provider when requesting

When the JWTBearerHandler receives the token from the client, it does not send the token to the provider for validation, but rather requests the Signing Key provider - the public part of the key that the token is signed with. Based on this key, it is verified that the token is signed by the correct provider.

Inside JWTBearerHandler sits HttpClient, which interacts with the provider over the network. But, if we assume that the Signing Key of our provider does not plan to change often, then you can pick it up once when you start the application, cache yourself and get rid of constant network requests.

I got this code for Signing Key:

public static AuthenticationBuilder AddJwtAuthentication(this AuthenticationBuilder builder, AuthJwtOptions options)
{
    var signingKeys = new List<SecurityKey>();

    var jwtBearerOptions = new JwtBearerOptions {Authority = options?.Authority};
    
    new JwtBearerPostConfigureOptions().PostConfigure(string.Empty, jwtBearerOptions);
    try
    {
        var config = jwtBearerOptions.ConfigurationManager
            .GetConfigurationAsync(new CancellationTokenSource(options?.AuthorityTimeoutInMs ?? 5000).Token)
            .GetAwaiter().GetResult();
        var providerSigningKeys = config.SigningKeys;
        signingKeys.AddRange(providerSigningKeys);
    }
    catch (Exception)
    {
        // ignored
    }

    builder
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                // ...
                IssuerSigningKeys = signingKeys,
                // ...
            };
        });
    return builder;
}

In line 12 we meet .GetAwaiter (). GetResult (). That's because AuthenticationBuilder is configured inside public void ConfigureServices (IServiceCollection services) {...} of the Startup class, and this method does not have an asynchronous version. Trouble.

Starting with C # 7.1, we have an asynchronous Main (). But the asynchronous Startup configuration methods in Asp.NET Core have not yet been delivered. I was aesthetically bothered to write GetAwaiter (). GetResult () (I was taught not to do this!), So I went online to look for how others deal with this problem.

I'm bothered by GetAwaiter (). GetResult (), but Microsoft is not


One of the first I found an option that Microsoft programmers used in a similar task to get secrets from Azure KeyVault . If you go down through several layers of abstraction, then we will see:

public override void Load() => LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult();

Hello again, GetAwaiter (). GetResult ()! Are there any other solutions?

After a short google, I found a series of great articles by Andrew Lock, who a year ago thought about the same issue as me. Even for the same reasons - he aesthetically doesn't like synchronously invoking asynchronous code.

In general, I recommend everyone who is interested in this topic to read the entire series of five articles by Andrew. There, he analyzes in detail which work tasks lead to this problem, then considers several incorrect approaches, and only then describes solutions. In my article I will try to present a brief squeeze of his research, concentrating more on solutions.

The role of asynchronous tasks in starting a web service


Take a step back to see the whole picture. What is the conceptual problem that I tried to solve, regardless of the framework?
Problem: it is necessary to start the web service so that it processes the requests of its clients, but there is a set of some (relatively) lengthy operations, without which the service either cannot respond to the client, or its answers will be incorrect.
Examples of such operations:

  • Validation of strongly typed configs.
  • Filling the cache.
  • Preliminary connection to the database or other services.
  • JIT and Assembly loading (service warming up).
  • Database Migration. This is one of the examples of Andrew Lock, but he himself admits that after all, this operation is undesirable when starting the service .

I would like to find a solution that will allow you to perform arbitrary asynchronous tasks at application startup, and in a natural way for them, without GetAwaiter (). GetResult ().

These tasks must be completed before the application starts accepting requests, but for their work they may need the configuration and registered services of the application. Therefore, these tasks must be performed after the DI configuration.

This idea can be represented in the form of a diagram:


Solution # 1: a working solution that can confuse the heirs


The first working solution offered by Lock :

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

       using (var scope = webHost.Services.CreateScope())
       {
           //   
           var myService = scope.ServiceProvider.GetRequiredService<MyService>();

           await myService.DoAsyncJob();
       }

       await webHost.RunAsync();
   }

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

This approach was made possible by the advent of the asynchronous Main () from C # 7.1. Its only negative is that we transferred the configuration part from Startup.cs to Program.cs. Such a non-standard solution for the ASP.NET framework can confuse a person who will inherit our code.

Solution # 2: embed asynchronous operations in DI


Therefore, Andrew proposed an improved version of the solution. An interface for asynchronous tasks is declared:

public interface IStartupTask
{
    Task ExecuteAsync(CancellationToken cancellationToken = default);
}

As well as an extension method that registers these tasks in DI:

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddStartupTask<T>(this IServiceCollection services)
        where T : class, IStartupTask
        => services.AddTransient<IStartupTask, T>();
}

Next, another extension method is declared, already for IWebHost:

public static class StartupTaskWebHostExtensions
{
    public static async Task RunWithTasksAsync(this IWebHost webHost, CancellationToken cancellationToken = default)
    {
        //      DI
        var startupTasks = webHost.Services.GetServices<IStartupTask>();

        //   
        foreach (var startupTask in startupTasks)
        {
            await startupTask.ExecuteAsync(cancellationToken);
        }

        //    
        await webHost.RunAsync(cancellationToken);
    }
}

And in Program.cs we change only one line. Instead:

await CreateWebHostBuilder(args).Build().Run();

We call:

await CreateWebHostBuilder(args).Build().RunWithTasksAsync();

In my opinion, a great approach that makes working with long operations as transparent as possible when starting the application.

Solution # 3: for those who switched to ASP.NET Core 3.x


But if you are using ASP.NET Core 3.x, then there is another option. I will again refer to an article by Andrew Lock .

Here is the WebHost launch code from ASP.NET Core 2.x:

public class WebHost
{
    public virtual async Task StartAsync(CancellationToken cancellationToken = default)
    {
        // ... initial setup
        await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false);

        // Fire IApplicationLifetime.Started
        _applicationLifetime?.NotifyStarted();

        // Fire IHostedService.Start
        await _hostedServiceExecutor.StartAsync(cancellationToken).ConfigureAwait(false);

        // ...remaining setup
    }
}

And here is the same method in ASP.NET Core 3.0:

public class WebHost
{
    public virtual async Task StartAsync(CancellationToken cancellationToken = default)
    {
        // ... initial setup

        // Fire IHostedService.Start
        await _hostedServiceExecutor.StartAsync(cancellationToken).ConfigureAwait(false);

        // ... more setup
        await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false);

        // Fire IApplicationLifetime.Started
        _applicationLifetime?.NotifyStarted();

        // ...remaining setup
    }
}

In ASP.NET Core 3.x, HostedServices is first launched, and only then the main WebHost, and earlier it was exactly the opposite. What does this give us? Now all asynchronous operations can be called inside the StartAsync (CancellationToken) method of the IHostedService interface and achieve the same effect without creating separate interfaces and extension methods.

Solution # 4: the story of health check and Kubernetes


One could calm down on this, but there is another approach, and it suddenly turns out to be important in the current realities. This is the use of health check.

The basic idea is to start the Kestrel server as early as possible in order to inform the load balancer that the service is ready to accept requests. But at the same time, all non-health check requests will return 503 (Service Unavailable). There is a fairly extensive article on the Microsoft site about how to use health check in ASP.NET Core. I wanted to consider this approach without special details as applied to our task.

Andrew Lock has a separate article for this approach. Its main advantage is that it avoids network timeouts.
, , , . Kestrel , , « ».

I will not give here the full Andrew Lock solution for the health check approach. It is quite voluminous, but there is nothing complicated in it.

I’ll tell you in a nutshell: you need to start the web service without waiting for the completion of asynchronous operations. In this case, the health check endpoint should be aware of the status of these operations, issue 503 while they are being executed, and 200 when they have already completed.

Honestly, when I studied this option, I had a certain skepticism. The whole solution looked cumbersome compared to previous approaches. And if we draw an analogy, then this is how to again use the EAP approach with event subscription, instead of the already familiar async / await.

But then Kubernetes came into play. He has his own concept of readiness probe. I will quote from the book “Kubernetes in Action” in my free presentation:
Always determine readiness probe.

If you do not have a readiness probe, your pods become endpoints of services almost instantly. If your application takes too much time preparing to accept incoming requests, client requests for the service will also get to start pods that are not yet ready to accept incoming connections. As a result, clients will receive a “Connection refused” error.

I conducted a simple experiment: I created an ASP.NET Core 3 service with a long asynchronous task in HostedService:

public class LongTaskHostedService : IHostedService
{
    public async Task StartAsync(CancellationToken cancellationToken)
    {
            Console.WriteLine("Long task started...");
            await Task.Delay(5000, cancellationToken);
            Console.WriteLine("Long task finished.");
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {...}
}

When I started this service using minikube, and then increased the number under to two, within 5 seconds of the delay, every second request of mine did not produce useful information, but “Connection refused”.

Kubernetes 1.16 UPD
, , Kubernetes 1.16 startup probe ( ). , readiness probe. . .

findings


What conclusion can be drawn from all these studies? Perhaps everyone should decide for his project which solution is best suited. If it is assumed that the asynchronous operation will not take too much time, and the clients have some kind of retry policy, then you can use all approaches, starting with GetAwaiter (). GetResult () and ending with IHostedService in ASP.NET Core 3.x.

On the other hand, if you use Kubernetes and your asynchronous operations can be performed for a noticeably long time, then you can not do without a health check (aka readiness / startup probe).

All Articles