blog post image
Andrew Lock avatar

Andrew Lock

~7 min read

Waiting for your ASP.NET Core app to be ready from an IHostedService in .NET 6

In this post I describe how you can wait for your ASP.NET Core app to be ready to receive requests from inside an IHostedService/BackgroundService in .NET 6. This can be useful if your IHostedService needs to send requests to your ASP.NET Core app, if it needs to find the URLs the app is listening on, or if it otherwise needs to wait for the app to be fully started.

Why do we need to find the URLs in a hosted service?

One of the most popular posts on my blog is "5 ways to set the URLs for an ASP.NET Core app". In a follow up post, I showed how you could tell ASP.NET Core to randomly choose a free port, instead of having to provide a specific port. The difficulty with that approach is finding out which port ASP.NET Core has chosen.

I was recently writing a small test app in which I needed to find which URLs the application was listening on from inside a BackgroundService. The details of why aren't very important, but I wanted the BackgroundService to call some public endpoints in the app as a "self test":

  • App starts up, starts listening on a random port
  • Hosted service calls public endpoint in the app
  • After receiving the response, the service triggers the app to shut down.

The question was - how could I determine which URLs Kestrel was listening on from the hosted service.

Finding which URLs ASP.NET Core is listening on

As I discussed in a previous post, finding the URLs an ASP.NET Core app is listening on is easy enough. If you fetch an IServer instance using dependency injection, then you can check the IServerAddressesFeature on the Features property. This exposes the Addresses property, which lists the addresses.

void PrintAddresses(IServiceProvider services)
{
    Console.WriteLine("Checking addresses...");
    var server = services.GetRequiredService<IServer>();
    var addressFeature = server.Features.Get<IServerAddressesFeature>();
    foreach(var address in addressFeature.Addresses)
    {
        Console.WriteLine("Listing on address: " + address);
    }
}

So if it's as simple as that, then there shouldn't be any problems right? You can fetch the addresses from the IHostedService/BackgroundService and send requests to them? Not exactly…

IHostedService startup order in .NET 6

In .NET Core 2.x, before the introduction of the generic IHost abstraction, the IHostedService for your application would start after Kestrel had been fully configured and started listening for requests. I discussed this in a series on running async startup tasks back then. Somewhat ironically, the reason IHostedService wasn't suitable for running async startup tasks back then (they started after Kestrel) would make it perfect for my use case now, as I could fetch the Kestrel addresses, knowing that they would be available.

In .NET Core 3.0, when ASP.NET Core was re-platformed on top of the generic IHost, things changed. Now Kestrel would run as an IHostedService itself, and it would be started last, after all other IHostedServices. This made IHostedService perfect for the async start tasks, but now you couldn't rely on Kestrel being available when your IHostedService runs.

In .NET 6, things changed slightly again with the introduction of the minimal hosting API. With these hosting APIs you can create incredibly terse programs (no need for Startup classes, and "magic" method names etc) but there are some differences around how things are created and started. Specifically, the IHostedServices are started when you call WebApplication.Run(), which is typically after you've configured your middleware and endpoints:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHostedService<TestHostedService>();
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run(); // πŸ‘ˆ TestHostedService is started here

This differs slightly from .NET Core 3.x/.NET 5/IHost scenario, in which the hosted services would be started before the Startup.Configure() method was called. Now all the endpoints and middleware are added, and it's only when you call WebApplication.Run() that all the hosted services are started.

This difference doesn't necessarily change anything for our scenario, but it's something to be aware of if you need your IHostedService to start before the middleware routes are configured. See this GitHub issue for more details.

The end result is that we can't rely on Kestrel having started and being available when your IHostedService/BackgroundService runs, so we need a way of waiting for this in our service.

Receiving app status notifications with IHostApplicationLifetime

Luckily, there's a service available in all ASP.NET Core 3.x+ apps that can notify you as soon as your application has finished starting, and is handling requests: IHostApplicationLifetime. This interface includes 3 properties which can notify you about stages of your application lifecycle, and one method for triggering your application to shut down

public interface IHostApplicationLifetime
{
    CancellationToken ApplicationStarted { get; }
    CancellationToken ApplicationStopping { get; }
    CancellationToken ApplicationStopped { get; }
    void StopApplication();
}

As you can see, each of the properties are a CancellationToken. This might seem an odd choice for receiving notifications (nothing is cancelled when your application has just started!πŸ€”) but it provides a convenient way to safely run callbacks when an event occurs. For example:

public void PrintStartedMessage(IHostApplicationLifetime lifetime)
{
    lifetime.ApplicationStarted.Register(() => Console.WriteLine("App has started!"));
}

As this shows, you can call Register() and pass in an Action which is executed when the app has started up. Similarly, you can receive notifications for the other statuses, such as "stopping" or "stopped".

The Stopping callback is particularly useful, for example, as it allows you to block shutdown until the callback completes, giving you a chance to drain resource or do other long-running cleanup for example.

While this is useful, it is just one piece of the puzzle. We need to run some asynchronous code (calling an HTTP API for example) when the app has started, so how can we do that safely?

Waiting for Kestrel to be ready in a background service

Lets start with something concrete, a BackgroundService which we want to "block" until the application has started:

public class TestHostedService: BackgroundService
{
    private readonly IServiceProvider _services;
    public TestHostedService(IServiceProvider services)
    {
        _services = services;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // TODO: wait here until Kestrel is ready

        PrintAddresses(_services);
        await DoSomethingAsync();
    }
}

In an initial approach, we can use the IHostApplicationLifetime and a simple bool to wait for the app to be ready, looping until we receive that signal:

public class TestHostedService: BackgroundService
{
    private readonly IServiceProvider _services;
    private volatile bool _ready = false; // πŸ‘ˆ New field
    public TestHostedService(IServiceProvider services, IHostApplicationLifetime lifetime)
    {
        _services = services;
        lifetime.ApplicationStarted.Register(() => _ready = true); // πŸ‘ˆ Update the field when Kestrel has started
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while(!_ready)
        {
            // App hasn't started yet, keep looping!
            await Task.Delay(1_000)
        }

        PrintAddresses(_services);
        await DoSomethingAsync();
    }
}

This works, but it's not exactly pretty. Every second the ExecuteAsync method is checking the _ready field, and is then going to sleep again if it's not set. That probably won't happen too many times (unless your app startup is very slow), but it still feels a bit messy.

I'm explicitly ignoring the stoppingToken passed to the method for now, we'll come back to it later!

The cleanest approach I have found is to use a helper class as an intermediary between the "started" cancellation token signal, and the async code we need to run. Ideally, we want to await a Task that completes when the ApplicationStarted signal is received. The following code uses TaskCompletionSource to do just that:

public class TestHostedService: BackgroundService
{
    private readonly IServiceProvider _services;
    private readonly IHostApplicationLifetime _lifetime;
    private readonly TaskCompletionSource _source = new(); // πŸ‘ˆ New field
    public TestHostedService(IServiceProvider services, IHostApplicationLifetime lifetime)
    {
        _services = services;
        _lifetime = lifetime;

        // πŸ‘‡ Set the result in the TaskCompletionSource
        _lifetime.ApplicationStarted.Register(() => _source.SetResult()); 
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await _source.Task.ConfigureAwait(false); // Wait for the task to complete!

        PrintAddresses(_services);
        await DoSomethingAsync();
    }
}

This approach is much nicer. Instead of using polling to set a field, we have a single await of a Task, which completes when the ApplicationStarted event triggers. This is the suggested approach any time you find yourself wanting to "await a CancellationToken" like this.

However, there's a potential problem in the code. What if the application never starts up!?

If the ApplicationStarted token never triggers, then the TaskCompletionSource.Task will never complete, and the ExecuteAsync method will never complete! This is unlikely, but could happen if there's a problem starting your application, for example.

Luckily there's a fix for this by using the stoppingToken passed to ExecuteAsync and another TaskCompletionSource! For example:

public class TestHostedService: BackgroundService
{
    private readonly IServiceProvider _services;
    private readonly IHostApplicationLifetime _lifetime;
    private readonly TaskCompletionSource _source = new();
    public TestHostedService(IServiceProvider services, IHostApplicationLifetime lifetime)
    {
        _services = services;
        _lifetime = lifetime;

        _lifetime.ApplicationStarted.Register(() => _source.SetResult());
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // πŸ‘‡ Create a TaskCompletionSource for the stoppingToken
        var tcs = new TaskCompletionSource();
        stoppingToken.Register(() => tcs.SetResult());

        // wait for _either_ of the sources to complete
        await Task.WhenAny(tcs.Task, _source.Task).ConfigureAwait(false);

        // if cancellation was requested, stop 
        if (stoppingToken.IsCancellationRequested)
        {
            return;
        }

        // Otherwise, app is ready, do your thing
        PrintAddresses(_services);
        await DoSomethingAsync();
    }
}

This code is slightly more complex, but it gracefully handles everything we need. We could even extract it into a handy helper method.

public class TestHostedService: BackgroundService
{
    private readonly IServiceProvider _services;
    private readonly IHostApplicationLifetime _lifetime;
    public TestHostedService(IServiceProvider services, IHostApplicationLifetime lifetime)
    {
        _services = services;
        _lifetime = lifetime;

    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        if (!await WaitForAppStartup(_lifetime, stoppingToken))
        {
            return;
        }

        PrintAddresses(_services);
        await DoSomethingAsync();
    }

    static async Task<bool> WaitForAppStartup(IHostApplicationLifetime lifetime, CancellationToken stoppingToken)
    {
        var startedSource = new TaskCompletionSource();
        var cancelledSource = new TaskCompletionSource();

        using var reg1 = lifetime.ApplicationStarted.Register(() => startedSource.SetResult());
        using var reg2 = stoppingToken.Register(() => cancelledSource.SetResult());

        Task completedTask = await Task.WhenAny(
            startedSource.Task,
            cancelledSource.Task).ConfigureAwait(false);

        // If the completed tasks was the "app started" task, return true, otherwise false
        return completedTask == startedSource.Task;
    }
}

Whichever approach you take, you can now execute your background task code, safe in the knowledge that Kestrel will be listening!

Summary

In this post I described how to wait in a BackgroundService/IHostedService for your ASP.NET Core application to finish starting, so you can send requests to Kestrel, or retrieve the URLs it's using (for example). This approach uses the IHostApplicationLifetime service available through dependency injection. You can hook up a callback to the ApplicationStarted CancellationToken it exposes to trigger a TaskCompletionSource, which you can then await in your ExecuteAsync method. This avoids the need for looping constructs, or for running async code in a sync context.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?