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



我有一个 ASP.NET Core Web应用程序,带有WebAPI控制器。 我试图做的只是,在某些控制器中,能够启动一个将在后台运行的进程,但控制器应该在该过程完成之前继续并返回。 我不希望服务的使用者必须等待此作业完成。

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

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

最终,我所看到的一切都是一个巨大的、错综复杂的代码,似乎应该是一件简单的事情。 我错过了什么?

您有以下选项:

  1. IHostedService类可以是长时间运行的方法,在应用的生存期内在后台运行。为了使它们能够处理某种后台任务,您需要在应用中实现某种"全局"队列系统,以便控制器存储数据/事件。此队列系统可以像一个Singleton类一样简单,其中包含传递给控制器的 ConcurrentQueue,或者类似于IDistributedCache或更复杂的外部发布/订阅系统。然后,您可以轮询IHostedService中的队列并基于它运行某些操作。下面是用于处理队列IHostedService实现的 Microsoft 示例 https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio#queued-background-tasks 请注意,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传递给该方法,并在其中创建一个新的作用域,以确保 ASP.NET 不会在控制器操作完成时终止任务。类似的东西
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
}
}

更新:这是执行类似操作的Microsoft示例:https://learn.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

据我从您的问题中了解到,您想创建一个火灾并忘记记录到数据库之类的任务。在此方案中,您不必等待日志插入数据库。 我也花了很多时间来发现一个易于实施的解决方案。这是我的发现:

在控制器参数中,添加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..");
}

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

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

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

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

services.AddScoped<ICloudStorage, AzureCloudStorage>();

在此处查找详细文档。

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

我不希望服务的使用者必须等待此作业完成。

最终,我所看到的一切都是一个巨大的、错综复杂的代码,似乎应该是一件简单的事情。我错过了什么?

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

请求外部代码的唯一正确解决方案是拥有一个具有单独后台进程的持久队列。任何过程中的东西(例如,带有IHostedServiceConcurrentQueue)都会有可靠性问题;特别是,这些解决方案偶尔会失去工作。

最新更新