在 Configure() 后启动 IHostedService

15

我有一个使用.NET Core 3.1编写的应用程序,提供一个描述应用程序健康状况的端点,并有一个IHostedService在数据库中进行数据处理。

然而,问题在于,HostedService的工作函数开始长时间处理,结果导致Startup中的Configure()方法未被调用,/status端点没有运行。

我希望在HostedService启动之前,/status端点就能够开始运行。如何在Hosted Service之前启动端点?

示例代码:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHostedService<SomeHostedProcessDoingHeavyWork>();
    }

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

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGet("/status", async context =>
            {
                await context.Response.WriteAsync("OK");
            });
        });
    }
}

托管服务(HostedService)

public class SomeHostedProcessDoingHeavyWork : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await MethodThatRunsForSeveralMinutes();
            await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        }
    }

    private async Task MethodThatRunsForSeveralMinutes()
    {
        // Process data from db....

        return;
    }
}

我尝试在Configure()中添加HostedService,但是app.ApplicationServices是只读的ServiceProvider


1
尝试将ExecuteAsync的主体移动到一个单独的方法中,然后在ExecuteAsync中使用await DoWork(stoppingToken)。您还可以尝试将await Task.Delay(1);放置在第一行。服务的创建被阻塞,直到它们变成异步。我期望在第一个等待时立即变为异步,但文档中的类似示例使用我建议的第一种机制。 - pinkfloydx33
在执行工作任务之前,通过添加Task.Delay()来使其正常工作。你会将其发布为答案吗? - morteng
1
也许我错过了什么,但是下面的解决方案都对我不起作用。我不得不在 Program.cs 中添加服务,而不是在 Startup.ConfigureServices 中添加。否则服务器实际上并没有启动。 - user2146414
6个回答

10

我认为提出的解决方案是一种权宜之计。

如果你把你的托管服务放在ConfigureServices()方法内部,那么它将会在Kestrel服务之前启动,因为GenericWebHostService(实际上运行Kestrel的)是在Program.cs中添加的,当你调用时。

.ConfigureWebHostDefaults(webBuilder =>
        webBuilder.UseStartup<Startup>()
)

所以它总是被添加为最后一个。

要在Kestrel之后启动托管服务,只需在调用ConfigureWebHostDefaults()后再链接另一个调用:

.ConfigureServices(s => s.AddYourServices())

像这样:

IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args)
 .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>())
 .ConfigureServices(s => { 
      s.AddHostedService<SomeHostedProcessDoingHeavyWork>();
  });

你应该做完了。


1
这个答案能否更新到.NET 6+?由于现在没有显式的Startup.cs(至少默认情况下),所以提供的示例代码已经不存在了。 - k3davis
它完美地运行。对于那些需要实现上述代码的人,我建议将.NET 6/7(“Minimal Hosting”)重写为旧样式,因为新样式是表面的,大量文档都是旧样式的。此外,我注意到他们没有担心制作“逃生门”,所以我们可以充分扩展访问。因此,对于高级场景,似乎我们被困在旧样式中。 - Cesar

3
ExecuteAsync应该返回一个Task,并且应该快速返回。根据文档(重点在于)
ExecuteAsync(CancellationToken)被调用以运行后台服务。实现返回一个表示整个后台服务生命周期的任务。在ExecuteAsync变成异步之前,不要启动任何其他服务,例如通过调用await来避免在ExecuteAsync中执行长时间的阻塞初始化工作。主机在StopAsync(CancellationToken)中阻塞等待ExecuteAsync完成。
您可以通过将逻辑移动到单独的方法并等待它来解决这个问题。
protected override async Task ExecuteAsync(CancellationToken stoppingToken) 
{ 
    await BackgroundProcessing(stoppingToken);
}

private async Task BackgroundProcessing(CancellationToken stoppingToken) 
{ 
    while (!stoppingToken.IsCancellationRequested)
    { 
        await MethodThatRunsForSeveralMinutes();
        await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); 
    }
}

或者你只需在方法开头添加一个 await:

protected override async Task ExecuteAsync(CancellationToken stoppingToken) 
{ 
    await Task.Yield();
    while (!stoppingToken.IsCancellationRequested)
    { 
        await MethodThatRunsForSeveralMinutes();
        await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); 
    }
}

3

对于任何遇到这个问题的人: Andrew Lock在他的博客上提供了一个非常好的解决方案,使用IHostApplicationLifetime

public class TestHostedService: BackgroundService
{
    private readonly IHostApplicationLifetime _lifetime;
    private readonly TaskCompletionSource _source = new();
    public TestHostedService(IHostApplicationLifetime lifetime)
    {
        _lifetime = lifetime;
        _lifetime.ApplicationStarted.Register(() => _source.SetResult()); 
    }

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

如果应用程序无法启动,则可能会出现潜在问题:如果ApplicationStarted标记从未触发,则TaskCompletionSource.Task将永远不会完成,ExecuteAsync方法也将永远无法完成。为解决此问题,您可以使用以下方法:
public class TestHostedService: BackgroundService
{
    private readonly IHostApplicationLifetime _lifetime;
    public TestHostedService(IHostApplicationLifetime lifetime)
    {
        _lifetime = lifetime;
    }

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

        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);

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

1
这是一个很好的解决方案...特别适用于较新版本的.NET。 - tsiorn

1

我最终使用了Task.Yield()并实现了一个抽象类来封装它,其中包括可选的PreExecuteAsyncInternal钩子和错误处理程序ExecuteAsyncExceptionHandler

public abstract class AsyncBackgroundService : BackgroundService
{
    protected ILogger _logger;
    private readonly TimeSpan _delay;

    protected AsyncBackgroundService(ILogger logger, TimeSpan delay)
    {
        _logger = logger;
        _delay = delay;
    }

    public virtual Task PreExecuteAsyncInternal(CancellationToken stoppingToken)
    {
        // Override in derived class
        return Task.CompletedTask;
    }

    public virtual void ExecuteAsyncExceptionHandler(Exception ex)
    {
        // Override in derived class
    }

    public abstract Task ExecuteAsyncInternal(CancellationToken stoppingToken);

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {   
        // Prevent BackgroundService from locking before Startup.Configure()
        await Task.Yield();

        _logger.LogInformation("Running...");

        await PreExecuteAsyncInternal(stoppingToken);

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await ExecuteAsyncInternal(stoppingToken);
                await Task.Delay(_delay, stoppingToken);
            }
            catch (TaskCanceledException)
            {
                // Deliberate
                break;
            }
            catch (Exception ex)
            {
                _logger.LogCritical($"Error executing {nameof(ExecuteAsyncInternal)} in {GetType().Name}", ex.InnerException);

                ExecuteAsyncExceptionHandler(ex);

                break;
            }
        }

        _logger.LogInformation("Stopping...");
    }
}

2
await Task.Yield(); 这行代码需要放在方法的第一行。 这样做的原因是为了“释放”任务,以便 Host.StartAsync 可以继续执行。 如果有任何前置代码块(是的,即使是等待的内容也可能会阻塞),那么 Host 将在此服务处阻塞,而不会继续启动其他服务。在您的片段中... 假设 PreExecuteAsyncInternal 可以返回异步结果,则会出现此行为。将其放在循环中“可以工作”,但将其放在第一行将为您提供所需的行为。 - AndyPook

0
较新的WebApplicationBuilder的源代码建议利用ConfigureContainer来实现这种行为,尽管我个人认为这不是最清晰的解决方案,而且很可能在将来出现问题。
    /// <summary>
    /// Builds the <see cref="WebApplication"/>.
    /// </summary>
    /// <returns>A configured <see cref="WebApplication"/>.</returns>
    public WebApplication Build()
    {
        // ConfigureContainer callbacks run after ConfigureServices callbacks including the one that adds GenericWebHostService by default.
        // One nice side effect is this gives a way to configure an IHostedService that starts after the server and stops beforehand.
        _hostApplicationBuilder.Services.Add(_genericWebHostServiceDescriptor);
        Host.ApplyServiceProviderFactory(_hostApplicationBuilder);
        _builtApplication = new WebApplication(_hostApplicationBuilder.Build());
        return _builtApplication;
    }


0

await Task.Yield 对我没用。

最简单明显的解决方案:

Startup.cs

public class Startup
{
   public void ConfigureServices(IServiceCollection services)
   {
      // Implementation omitted
      services.AddSingleton<ApplicationRunState>();
   }

   public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
   {
      // Implementation omitted
      app.MarkConfigurationAsFinished();
   }
}

StartupExtensions.cs

public static void MarkConfigurationAsFinished(this IApplicationBuilder builder)
{
   builder.ApplicationServices.GetRequiredService<ApplicationRunState>()
      .ConfigurationIsFinished = true;
}

ExampleBackgroundService.cs

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        if (!_serviceProvider.GetRequiredService<ApplicationRunState>()
            .ConfigurationIsFinished)
        {
            await Task.Delay(5000);
            continue;
        }

        // Further implementation omitted
    }
}

你的解决方案可行,但是有一种更好的Task.Delay形式。使用SemaphoreSlim可以立即释放。 - Marcelo Vieira

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接