【问题标题】:What is the simplest way to run a single background task from a controller in .NET Core?从 .NET Core 中的控制器运行单个后台任务的最简单方法是什么?
【发布时间】:2020-09-25 02:17:19
【问题描述】:

我有一个带有 WebAPI 控制器的 ASP.NET Core Web 应用程序。我所要做的就是,在某些控制器中,能够启动一个将在后台运行的进程,但控制器应该继续并在该进程完成之前返回。我不希望服务的消费者不得不等待这项工作完成。

我看过所有关于 IHostedService 和 BackgroundService 的帖子,但似乎没有一个是我想要的。此外,所有这些示例都向您展示了如何设置,而不是如何实际调用它,或者我不理解其中的一些。

我尝试了这些,但是当您在 Startup 中注册 IHostedService 时,它​​会在那个时间点立即运行。这不是我想要的。我不想在启动时运行任务,我希望能够在需要时从控制器调用它。另外,我可能有几个不同的,所以只注册 services.AddHostedService() 是行不通的,因为我可能有一个 MyServiceB 和 MyServiceC,那么我如何从控制器中获得正确的一个(我不能只注入 IHostedService) ?

最终,我所看到的一切都是一大堆错综复杂的代码,而这些代码看起来应该是一件很简单的事情。我错过了什么?

【问题讨论】:

标签: c# asp.net-core .net-core


【解决方案1】:

您有以下选择:

  1. IHostedService 类可以是长时间运行的方法,在应用程序的整个生命周期内都在后台运行。为了让它们处理某种后台任务,您需要在您的应用程序中实现某种“全局”队列系统,以便控制器存储数据/事件。这个队列系统可以像 Singleton 类一样简单,您可以将 ConcurrentQueue 传递给控制器​​,或者像 IDistributedCache 或更复杂的外部发布/订阅系统。然后你可以在你的IHostedService 中轮询队列并基于它运行某些操作。这是用于处理队列https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio#queued-background-tasksIHostedService 实现的微软示例 请注意,Singleton 类方法可能会在multi-server 环境中引起问题。 单例方法的示例实现如下:
// Needs to be registered as a Singleton in your Startup.cs
public class BackgroundJobs {
  public ConcurrentQueue<string> BackgroundTasks {get; set;} = new ConcurrentQueue<string>();
}

public class MyController : ControllerBase{
  private readonly BackgroundJobs _backgroundJobs;
  public MyController(BackgroundJobs backgroundJobs) {
    _backgroundJobs = backgroundJobs;
  }

  public async Task<ActionResult> FireAndForgetEndPoint(){
    _backgroundJobs.BackgroundTasks.Enqueue("SomeJobIdentifier");
  }
}

public class MyBackgroundService : IHostedService {
  private readonly BackgroundJobs _backgroundJobs;
  public MyBackgroundService(BackgroundJobs backgroundJobs)
  {
    _backgroundJobs = backgroundJobs;
  }

  public void StartAsync(CancellationToken ct)
  {
    while(!ct.IsCancellationRequested)
    {
      if(_backgroundJobs.BackgroundTasks.TryDequeue(out var jobId))
      {
        // Code to do long running operation
      }
    Task.Delay(TimeSpan.FromSeconds(1)); // You really don't want an infinite loop here without having any sort of delays.
    }
  }
}
  1. 创建一个返回Task 的方法,将IServiceProvider 传递给该方法并在其中创建一个新的Scope 以确保ASP.NET 在控制器Action 完成时不会终止任务。像
IServiceProvider _serviceProvider;

public async Task<ActionResult> FireAndForgetEndPoint()
{
  // Do stuff
  _ = FireAndForgetOperation(_serviceProvider);
  Return Ok();
}

public async Task FireAndForgetOperation(IServiceProvider serviceProvider)
{
  using (var scope = _serviceProvider.CreateScope()){
    await Task.Delay(1000);
    //... Long running tasks
  }
}

更新:这是微软做类似事情的例子:https://docs.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?view=aspnetcore-3.1#do-not-capture-services-injected-into-the-controllers-on-background-threads

【讨论】:

  • 我已经看到 Microsoft 提供的“排队后台任务”示例,但我不太清楚如何更改它以适应我的目的。我不想要 MonitorLoop,而且我不太清楚如何从我的 Controller 中手动对任务进行排队。实际上,在这一点上,我很想像您最初建议的那样使用 Task.Run ,因为我觉得这是“不好的做法”的唯一原因是它仍然占用相同的 CPU 周期。但就我而言,没关系。我可以接受相同的周期,我只想在开始之前返回给用户。
  • MonitorLoop 就是其中的一个例子。您不需要实现它。您唯一需要的是一个至少具有一个属性的类,即您的任务队列,您可以将该类注册为单例并使用依赖注入将其注入到您的控制器和BackgroundService
  • @RebelScum Task.Run 是一种不好的做法,因为这意味着您没有充分利用 DI。事实上,如果您在 HttpRequest 范围内使用它,在您的后台任务中,您可能会使用已处置的对象并获得异常。因此,您将不确定的行为引入到您的应用中
  • 是的,你必须使用创建范围并手动从中检索对象,否则,事情可能会像拉门所说的那样被处置。就个人而言,我认为这是一种代码味道,但它是一种方法。
【解决方案2】:

我从您的问题中了解到,您想要创建一个 fire and forget 任务,例如登录到数据库。在这种情况下,您不必等待将日志插入数据库。我也花了很多时间来发现一个易于实施的解决方案。这是我发现的:

在您的控制器参数中,添加 IServiceScopeFactory。这不会影响请求正文或标头。之后创建一个范围并通过它调用您的服务。

[HttpPost]
public IActionResult MoveRecordingToStorage([FromBody] StreamingRequestModel req, [FromServices] IServiceScopeFactory serviceScopeFactory)
{
    // Move record to Azure storage in the background
    Task.Run(async () => 
    {
        try
        {
            using var scope = serviceScopeFactory.CreateScope();
            var repository = scope.ServiceProvider.GetRequiredService<ICloudStorage>();
            await repository.UploadFileToAzure(req.RecordedPath, key, req.Id, req.RecordCode);
        }
        catch(Exception e)
        {
            Console.WriteLine(e);
        }
    });
    return Ok("In progress..");
}

发布您的请求后,您将立即收到 In Progress.. 文本,但您的任务将在后台运行。

还有一件事,如果您不以这种方式创建任务并尝试调用数据库操作,您将收到类似这样的错误,这意味着您的数据库对象已经死亡并且您正在尝试访问它;

无法访问已处置的对象。此错误的一个常见原因是释放从依赖注入中解析的上下文,然后尝试在应用程序的其他地方使用相同的上下文实例。如果您在上下文上调用 Dispose() 或将上下文包装在 using 语句中,则可能会发生这种情况。如果你使用依赖注入,你应该让依赖注入容器处理上下文实例。\r\n对象名称:'DBContext'。

我的代码基于存储库模式。您不应该忘记在 Startup.cs

中注入服务类
services.AddScoped<ICloudStorage, AzureCloudStorage>();

查找详细文档here

【讨论】:

    【解决方案3】:

    在 .NET Core 中从控制器运行单个后台任务的最简单方法是什么?

    我不希望服务的消费者不得不等待这项工作完成。

    最终,我所看到的一切都是一大堆错综复杂的代码,而这些代码看起来应该是一件很简单的事情。我错过了什么?

    问题在于 ASP.NET 是一个用于编写 Web 服务的框架,这些 Web 服务是响应请求的应用程序。但是,一旦您的代码说“我不希望服务的消费者不得不等待”,那么您就是在谈论在请求之外运行代码(即,请求外部代码)。这就是所有解决方案都很复杂的原因:您的代码必须绕过/扩展框架本身,以试图强迫它做一些它原本不打算做的事情。

    唯一的正确 solution for request-extrinsic code 是拥有一个具有单独后台进程的持久队列。任何进程中的东西(例如,ConcurrentQueueIHostedService)都会有可靠性问题;特别是,这些解决方案有时会失去作用。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-02
      • 2015-10-01
      • 1970-01-01
      • 2015-01-10
      相关资源
      最近更新 更多