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 IHostedService
s. 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();
lifetime.ApplicationStarted.Register(() => startedSource.SetResult());
var cancelledSource = new TaskCompletionSource();
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.