目录
3. .NET 6 中的 IHostedService 启动顺序
4.使用 IHostApplicationLifetime 接收应用状态通知
序言
在这篇文章中,我将介绍如何等待 ASP.NET Core 应用是否已经启动好,已经开始可以接收来自 .NET 6 中的 / 中的请求了。如果你需要编写IHostedService
BackgroundService
IHostedService
,并向 ASP.NET Core 应用发送请求,如果它需要查找应用正在侦听的 URL,或者如果需要等待应用完全启动,这会很有用。
1.困扰
如果在应用中同时启用了多个IHostedService,已开始多个背景任务,那么一个有趣的现象是,其他的任务总是比 web应用启动的快,这有时候会导致一些异常。
当然,除了有些无力的控制感以外,并没有啥大不了的,除非真的有其他需求要求我们必须这么做。
而现在我就遇到了一个类似的问题,我想在背景任务中获取应用已经绑定的端口,啊哈哈,什么,应用还没有启动,那我们搞个毛线…
2. 查找侦听的端口
查找 ASP.NET Core 应用程序正在侦听的 URL 非常容易。
如果使用依赖关系注入获取实例,则可以检查属性 IServerIServerAddressesFeatureFeaturesAddresses
, 这将公开列出地址的属性。
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);
}
}
这个方法不算什么。
可是,怎么确定web应用已经启动了呢?
3. .NET 6 中的 IHostedService 启动顺序
在 .NET Core 2.x 中,引入通用抽象之前,应用程序将在 Kestrel 完全配置并开始侦听请求后启动。
有点讽刺的是,当时的原因不适合运行异步启动任务(它们在 Kestrel 之后开始), 在.NET Core 3.0中,当 ASP.NET Core在通用主机之上重新构建平台时,情况发生了变化。现在 Kestrel 将作为一个自身主机运行,它将在所有其他主机服务之后最后启动。 这使得 IHostedService 非常适合异步启动任务,但现在你不能依赖 Kestrel 在运行时可用。
在 .NET 6 中,随着最小托管 API 的引入,情况再次发生了轻微变化。使用这些托管 API,您可以创建令人难以置信的简洁程序(不需要类和“魔术”方法名称等),但在创建和启动方式方面存在一些差异。
具体来说,在调用StartupIHostedServicesWebApplication.Run()
时启动 ,通常是在配置中间件和端点之后:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHostedService<TestHostedService>();
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run(); // 👈 TestHostedService 在这里启动
这与 .NET Core 3.x/.NET 5/ 方案略有不同,在 .NET Core 3.x/.NET 5/ 方案中,托管服务将在调用方法之前启动。现在,所有端点和中间件都已添加,并且只有在调用IHostStartup.Configure() WebApplication.Run()
时,才会启动所有托管服务。
对于我们的方案,这种差异不一定会改变任何内容,但如果需要在配置中间件路由之前启动,则需要注意这一点。
最终结果是,我们不能依赖 Kestrel 已经开始,并在您的IHostedServiceBackgroundService
运行时可用,因此我们需要一种在我们的服务中等待它的方法。
4.使用 IHostApplicationLifetime 接收应用状态通知
幸运的是,所有 Core 3.x+ 应用程序中都提供一项服务。ASP.NET 可以在应用程序完成启动并处理请求后立即通知您。
这就是 IHostApplicationLifetime
,此接口包括 3 个属性,可以通知您有关应用程序生命周期的各个阶段,以及一种触发应用程序关闭的方法。
public interface IHostApplicationLifetime
{
CancellationToken ApplicationStarted { get; }
CancellationToken ApplicationStopping { get; }
CancellationToken ApplicationStopped { get; }
void StopApplication();
}
正如你看到的, 每个属性都是一个取消token令牌, 这对于接收通知来看,看起来是一个老的选择(当你的程序启动后,没有什么被取消),但是它提供了方法在事件发生时可以安全的运行回调。
public void PrintStartedMessage(IHostApplicationLifetime lifetime)
{
lifetime.ApplicationStarted.Register(() => Console.WriteLine("App has started!"));
}
如上所示,您可以调用并传入在应用启动时执行的Register(Action)
。同样,您可以接收其他状态的通知,例如“正在停止”或“已停止”。
例如,停止回调特别有用,因为它允许您阻止关闭,直到回调完成,让您有机会清理资源或执行其他长时间运行的清理。
虽然这很有用,但它只是拼图的一部分。我们需要在应用程序启动时运行一些异步代码(例如调用HTTP API),那么我们如何安全地做到这一点呢?
5. 等待应用在后台服务中准备就绪
让我们从具体的东西开始,我们想要“阻止它” BackgroundService
,直到应用程序启动:
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();
}
}
在初始方法中,我们可以使用 IHostApplicationLifetime
和一个简单的 bool
等待应用程序准备就绪,一直循环直到我们收到该信号:
public class TestHostedService: BackgroundService
{
private readonly IServiceProvider _services;
private volatile bool _ready = false; // 👈 标志位
public TestHostedService(IServiceProvider services, IHostApplicationLifetime lifetime)
{
_services = services;
lifetime.ApplicationStarted.Register(() => _ready = true); // 👈 如果启动,在这里更新
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while(!_ready)
{
// 如果app没有启动,则等待
await Task.Delay(1_000)
}
PrintAddresses(_services);
await DoSomethingAsync();
}
}
这十分有效,但代码并不漂亮优雅。
该方法每秒都在检查字段,如果未设置,则会再次进入睡眠状态。这种情况可能不会发生太多次(除非你的应用程序启动非常慢),但它仍然感觉有点混乱
我现在明确忽略传递给方法stoppingToken
,我们稍后再讨论!
我发现的最干净的方法是使用帮助程序类作为“已启动”取消令牌信号和我们需要运行的异步代码之间的中介。
理想情况下,我们希望在接收到信号时完成。以下代码就是用来执行此操作的:
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;
// 👇 在这里设置 TaskCompletionSource
_lifetime.ApplicationStarted.Register(() => _source.SetResult());
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await _source.Task.ConfigureAwait(false); // 等待任务完成
PrintAddresses(_services);
await DoSomethingAsync();
}
}
哈哈~~~~ 这种方法要好得多。
我们没有使用轮询来设置字段,而是有一个await
,它在事件触发时完成。这是建议的方法,任何时候你发现自己想要“等待”都可以这样的搞定。
但是,代码中存在潜在问题。如果应用程序永远无法启动怎么办!?
如果令牌从未触发,则我们的ApplicationStartedTaskCompletionSource
永远不会完成,并且该方法将永远不会完成!这不太可能,但例如,如果启动应用程序时出现问题,则可能会发生这种情况。
幸运的是,通过使用stoppingToken
传递给ExecuteAsync
和另一个TaskCompletionSource
来解决此问题!例如:
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)
{
// 👇 建立一个 stoppingToken的 TaskCompletionSource
var tcs = new TaskCompletionSource();
stoppingToken.Register(() => tcs.SetResult());
// 等待任意一个源完成
await Task.WhenAny(tcs.Task, _source.Task).ConfigureAwait(false);
// 如果是停止,则返回
if (stoppingToken.IsCancellationRequested)
{
return;
}
// 否则, App已经准备好了,干吧,兄弟。
PrintAddresses(_services);
await DoSomethingAsync();
}
}
这段代码稍微复杂了一些,但它可以优雅地处理我们需要的一切。
我们甚至可以将其提取到一个方便的辅助方法中。
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;
}
}
无论您采用哪种方法,您现在都可以执行后台任务代码了,因为亲爱的,您已经知道 Kestrel 会监听!