7

我有一个 .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;
    }
}

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

4

4 回答 4

3

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

如果您 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>();
  });

你应该完成了。

于 2021-12-29T13:35:46.773 回答
1

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

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

protected override async Task ExecuteAsync(CancellationToken stoppingToken) 
{ 
    await Task.Yield();
    while (!stoppingToken.IsCancellationRequested)
    { 
        await MethodThatRunsForSeveralMinutes();
        await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); 
    }
}
于 2020-05-18T15:30:43.130 回答
0

我最终使用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)
    {
        _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...");
    }
}
于 2020-05-19T10:44:47.790 回答
0

await Task.Yield对我不起作用。

最简单明显的解决方案:

启动.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
    }
}
于 2020-12-21T17:01:08.597 回答