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

Reb*_*cum 10 c# .net-core asp.net-core

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

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

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

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

Sab*_*viş 17

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

在控制器参数中,添加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..");
}
Run Code Online (Sandbox Code Playgroud)

发布请求后,您将立即收到“正在进行中..”文本,但您的任务将在后台运行。

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

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

我的代码基于存储库模式。您不应该忘记在Startup.cs中注入服务类

services.AddScoped<ICloudStorage, AzureCloudStorage>();
Run Code Online (Sandbox Code Playgroud)

请在此处查找详细文档。


NvM*_*Mat 8

您有以下选择:

  1. IHostedService类可以是长时间运行的方法,它们在应用程序的整个生命周期中都在后台运行。为了让它们处理某种后台任务,您需要在您的应用程序中实现某种“全局”队列系统,以便控制器存储数据/事件。这个队列系统可以很简单,就像一个Singleton带有ConcurrentQueue的类,你传递给你的控制器,或者像一个IDistributedCache或更复杂的外部发布/订阅系统。然后你可以轮询你的队列IHostedService并基于它运行某些操作。这是IHostedService处理队列的微软实施示例https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio#queued-background -任务 请注意,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.
    }
  }
}
Run Code Online (Sandbox Code Playgroud)
  1. 创建一个返回 a 的方法,将 aTask传递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
  }
}
Run Code Online (Sandbox Code Playgroud)

更新:这是微软做类似事情的例子:https : //docs.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?view=aspnetcore-3.1#do-not-capture-服务注入到后台线程中的控制器

  • @RebelScum `Task.Run` 是一种不好的做法,因为这意味着您没有充分利用 DI。事实上,如果您在后台任务的 HttpRequest 范围内使用它,您可能会使用已处置的对象并收到异常。因此,您正在向应用程序引入不确定的行为 (3认同)

Ste*_*ary 7

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

我不希望服务的使用者必须等待这项工作完成。

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

问题在于 ASP.NET 是一个用于编写 Web 服务的框架,Web 服务是响应请求的应用程序。但是,一旦您的代码说“我不希望服务的使用者必须等待”,那么您就在谈论在请求之外运行代码即请求外部代码)。这就是为什么所有解决方案都很复杂:您的代码必须绕过/扩展框架本身,以试图强制它执行其未设计的操作。

对于请求外部代码,唯一正确的 解决方案是拥有一个带有单独后台进程的持久队列。任何正在进行的事情(例如,带有)都会有可靠性问题;特别是,这些解决方案有时会失效。ConcurrentQueueIHostedService