在 Configure() 之后启动 IHostedService

mor*_*eng 6 c# .net-core asp.net-core asp.net-core-hosted-services

我有一个 .NET Core 3.1 应用程序,它提供一个描述应用程序运行状况的端点,以及一个处理数据库中数据的 IHostedService。但是有一个问题,HostedService 的 worker 函数开始处理很长时间,导致Configure()Startup 中的方法没有被调用,/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");
            });
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

托管服务

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

我试图探索在 中添加 HostedService Configure(),但它app.ApplicationServices是一个 ServiceProvider,因此是只读的。

use*_*411 10

我认为提出的解决方案是一种解决方法。

如果您在 ConfigureServices()中添加托管服务,它将在Kestrel之前启动,因为当您调用时, GenericWebHostService(实际上运行Kestrel)会添加到Program.cs中

.ConfigureWebHostDefaults(webBuilder =>
        webBuilder.UseStartup<Startup>()
)
Run Code Online (Sandbox Code Playgroud)

所以它总是最后添加。

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

.ConfigureServices(s => s.AddYourServices()) 拨打电话后ConfigureWebHostDefaults()

像这样的东西:

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

你应该完成了。


mor*_*eng 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...");
    }
}
Run Code Online (Sandbox Code Playgroud)

  • `await Task.Yield();` 行必须是方法中的第一行。其原因是“释放”任务,以便 Host.StartAsync 可以继续。如果任何前面的代码阻塞(是的,甚至等待的东西也可能阻塞),那么主机将阻塞该服务,并且不会继续启动任何其他服务。在您的代码片段中...假设“PreExecuteAsyncInternal”可以返回异步,您将获得此行为作为副作用。将其放入循环中“有效”,但只需将其作为第一行即可为您提供所需的行为。 (2认同)