在 .NET Core 5 托管服务的 StartAsync 和 StopAsync 中返回什么?

Kon*_*ten 5 c# .net-core asp.net-core-hosted-services asp.net-core-5.0

尝试大致遵循MSDN,我在课堂上的范围服务之后添加了托管服务StartUp

public void ConfigureServices(IServiceCollection services)
{
  ...
  services.AddScoped<IUtilityService, UtilityService>();
  services.AddHostedService<StartupService>();
  ...
}
Run Code Online (Sandbox Code Playgroud)

我是StartAsync这样实现的。

public class StartupService : IHostedService
{
  private IServiceProvider Provider { get; }
  public StartupService(IServiceProvider provider)
  {
    Provider = provider;
  }

  public Task StartAsync(CancellationToken cancellationToken)
  {
    IServiceScope scope = Provider.CreateScope();
    IUtilityService service = scope.ServiceProvider
      .GetRequiredService<IUtilityService>();

    service.Seed();

    return Task.CompletedTask;
  }

  public Task StopAsync(CancellationToken cancellationToken)
  {
    throw new NotImplementedException();
  }
}
Run Code Online (Sandbox Code Playgroud)

我读过很多文章和博客,但我无法理解方法末尾应该返回什么。它现在似乎有效,但我可以清楚地看到,我通过不使用异步调用并返回一个虚拟值来违反这个想法(甚至在停止时也没有!),所以我可以安全地得出结论,我做错了(尽管不是)显然,但我确信它将来会咬我的屁股)。

我应该在实现中返回什么以确保我“使用”而不是反对框架?

whe*_*don 2

StartAsync 需要返回一个任务,该任务可能正在运行,也可能没有运行(但理想情况下它应该正在运行,这就是 HostedService 的要点 - 在应用程序的生命周期内运行的操作/任务,或者只是在一段较长的时间内运行比正常时间长)。

看起来您正在尝试使用 HostedService 执行额外的启动项,而不是仅仅尝试运行将持续到应用程序的整个生命周期的任务/操作。

如果是这种情况,您可以进行非常简单的设置。您想要从 StartAsync() 方法返回的是一个任务。当您返回 Task.CompletedTask 时,您表示工作已经完成并且没有代码执行 - 任务已完成。您想要返回的是正在执行任务对象内部运行的额外启动项的代码。asp.net 中 HostedService 的好处是任务运行多长时间并不重要(因为它意味着在应用程序的整个生命周期中运行任务)。

代码示例之前的一个重要注意事项 - 如果您在任务中使用 Scoped 服务,那么您需要使用 IServiceScopeFactory 生成一个范围,请在这篇 StackOverflow 帖子中阅读相关内容

如果您重构服务方法以返回任务,您可以只返回该任务:

public Task StartAsync(CancellationToken)
{
    IServiceScope scope = Provider.CreateScope();
    IUtilityService service = scope.ServiceProvider
      .GetRequiredService<IUtilityService>();

    // If Seed returns a Task
    return service.Seed();
}
Run Code Online (Sandbox Code Playgroud)

如果您有多个服务方法都返回一个任务,则可以返回一个正在等待所有任务完成的任务

public Task StartAsync(CancellationToken)
{
    IServiceScope scope = Provider.CreateScope();
    IUtilityService service = scope.ServiceProvider
      .GetRequiredService<IUtilityService>();
    ISomeOtherService someOtherService = scope.ServiceProvider
      .GetRequiredService<ISomeOtherService>();

    var tasks = new List<Task>();

    tasks.Add(service.Seed());
    tasks.Add(someOtherService.SomeOtherStartupTask());

    return Task.WhenAll(tasks);
}
Run Code Online (Sandbox Code Playgroud)

如果您的启动任务执行大量 CPU 密集型工作,只需返回 Task.Run(() => {});

public Task StartAsync(CancellationToken)
{
    // Return a task which represents my long running cpu startup work...
    return Task.Run(() => {

        IServiceScope scope = Provider.CreateScope();
        IUtilityService service = scope.ServiceProvider
          .GetRequiredService<IUtilityService>();

        service.LongRunningCpuStartupMethod1();
        service.LongRunningCpuStartupMethod2();
    }
}
Run Code Online (Sandbox Code Playgroud)

要使用取消令牌,下面的一些示例代码展示了如何通过在 Try/Catch 中捕获 TaskCanceledException 并强制退出运行循环来完成此操作。

然后我们继续处理将在整个应用程序生命周期中运行的任务。这是我用于所有 HostedService 实现的基类,这些实现被设计为在应用程序关闭之前永远不会停止运行。

public abstract class HostedService : IHostedService
{
    // Example untested base class code kindly provided by David Fowler: https://gist.github.com/davidfowl/a7dd5064d9dcf35b6eae1a7953d615e3

    private Task _executingTask;
    private CancellationTokenSource _cts;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        // Create a linked token so we can trigger cancellation outside of this token's cancellation
        _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

        // Store the task we're executing
        _executingTask = ExecuteAsync(_cts.Token);

        // If the task is completed then return it, otherwise it's running
        return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask;
    }

    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        // Stop called without start
        if (_executingTask == null)
        {
            return;
        }

        // Signal cancellation to the executing method
        _cts.Cancel();

        // Wait until the task completes or the stop token triggers
        await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken));

        // Throw if cancellation triggered
        cancellationToken.ThrowIfCancellationRequested();
    }

    // Derived classes should override this and execute a long running method until 
    // cancellation is requested
    protected abstract Task ExecuteAsync(CancellationToken cancellationToken);
}
Run Code Online (Sandbox Code Playgroud)

在此基类中,您将看到,当调用 StartAsync 时,我们调用 ExecuteAsync() 方法,该方法返回一个包含 while 循环的任务 - 任务不会停止运行,直到我们的取消令牌被触发,或者应用程序优雅/强制地停止。

ExecuteAsync() 方法需要由继承该基类的任何类实现,该基类应该是所有 HostedService 的类。

下面是一个继承自该基类的 HostedService 实现示例,设计为每 30 秒签入一次。您会注意到 ExecuteAsync() 方法进入 while 循环并且永远不会退出 - 它每秒会“滴答”一次,您可以在此处调用其他方法,例如定期检查另一台服务器。此循环中的所有代码都在任务中返回到 StartAsync() 并返回给调用者。在 while 循环退出或应用程序终止或触发取消令牌之前,任务不会终止。

public class CounterHostedService : HostedService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILog _logger;

    public CounterHostedService(IServiceScopeFactory scopeFactory, ILog logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    // Checkin every 30 seconds
    private int CheckinFrequency = 30;
    private DateTime CheckedIn;

    protected override async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        int counter = 0;

        var runningTasks = new List<Task>();
        while (true)
        {
            // This loop will run for the lifetime of the application.

            // Time since last checkin is checked every tick. If time since last exceeds the frequency, we perform the action without breaking the execution of our main Task
            var timeSinceCheckin = (DateTime.UtcNow - CheckedIn).TotalSeconds;
            if (timeSinceCheckin > CheckinFrequency)
            {
                var checkinTask = Checkin();
                runningTasks.Add(checkinTask);
            }

            try
            {
                // The loop will 'tick' every second.
                await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
            }
            catch (TaskCanceledException)
            {
                // Break out of the long running task because the Task was cancelled externally
                break;
            }

            counter++;
        }
    }

    // Custom override of StopAsync. This is only triggered when the application
    // GRACEFULLY shuts down. If it is not graceful, this code will not execute. Neither will the code for StopAsync in the base method.
    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.Info($"HostedService Gracefully Shutting down");

        // Perform base StopAsync
        await base.StopAsync(cancellationToken);
    }

    // Creates a task that performs a checkin, and returns the running task
    private Task Checkin()
    {
        return Task.Run(async () =>
        {
            // await DoTheThingThatWillCheckin();
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,您还可以重写 StopAsync() 方法来执行一些日志记录以及关闭事件所需的任何其他操作。尽量避免 StopAsync 中的关键逻辑,因为它不能保证被调用。