在 Configure() 之后启动 IHostedService

Start IHostedService after Configure()

我有一个 .NET Core 3.1 应用程序,它服务于描述应用程序运行状况的端点,以及一个处理数据库中数据的 IHostedService。 但是有一个问题,HostedService的worker函数开始处理很长时间,导致Star​​tup中的Configure()方法没有被调用,/status端点不是运行.

我希望 /status 端点在 HostedService 启动之前启动 运行。如何在托管服务之前启动端点?

示例代码

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

托管服务

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,因此是只读的。

ExecuteAsync 应该 return 一个 Task,而且应该很快。来自文档(强调我的)

ExecuteAsync(CancellationToken) is called to run the background service. The implementation returns a Task that represents the entire lifetime of the background service. No further services are started until ExecuteAsync becomes asynchronous, such as by calling await. Avoid performing long, blocking initialization work in ExecuteAsync. The host blocks in StopAsync(CancellationToken) waiting for ExecuteAsync to complete.

您应该能够通过将您的逻辑移动到一个单独的方法中并等待 that

来解决这个问题
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); 
    }
}

或者,您可以在方法的开头添加等待:

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

我最终使用 Task.Yield() 并实现了一个抽象 class 来封装它,带有可选的 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)
    {
        _logger.LogInformation("Running...");

        await PreExecuteAsyncInternal(stoppingToken);

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                // Prevent BackgroundService from locking before Startup.Configure()
                await Task.Yield();

                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...");
    }
}

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
    }
}

我认为建议的解决方案是一种变通方法。

如果您将托管服务添加到内部 ConfigureServices(),它将在之前启动] Kestrel 因为 GenericWebHostService(实际上运行 Kestrel)在您调用

时添加到 Program.cs
.ConfigureWebHostDefaults(webBuilder =>
        webBuilder.UseStartup<Startup>()
)

所以它总是被添加到最后。

要在Kestrel 之后启动托管服务,只需将另一个调用链接到

.ConfigureServices(s => s.AddYourServices()) 在调用 ConfigureWebHostDefaults().

之后

像这样:

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

你应该完成了。