运行 asp.net 核心中的按需后台任务 3.x

Running background task on demand in asp.net core 3.x

每当我从 api 端点收到特定请求时,我正在尝试按需启动后台任务。所有的任务都是发送一封电子邮件,延迟 30 秒。所以我认为 BackgroundService 会适合。但问题是 BackgroundService 看起来主要用于重复性任务,而不是根据此 answer.

按需执行

那么我还有什么其他选择,我希望不必依赖像 Hangfire 这样的第三方库?我正在使用 asp.net 核心 3.1。

这是我的后台服务。

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace ProjectX.Services {
    public  class EmailOfflineService : BackgroundService {

        private readonly ILogger<EmailOfflineService> log;
        private readonly EmailService emailService;
        public EmailOfflineService(
            ILogger<EmailOfflineService> log, 
            EmailService emailService
        ) {
            this.emailService = emailService;
            this.log = log;
        }

        protected async override Task ExecuteAsync(CancellationToken stoppingToken)
        {

            log.LogDebug("Email Offline Service Starting...");
            stoppingToken.Register(() => log.LogDebug("Email Offline Service is stopping."));

            while(!stoppingToken.IsCancellationRequested)
            {
                // wait for 30 seconds before sending
                await Task.Delay(1000 * 30, stoppingToken);

                await emailService.EmailOffline();
                
                // End the background service
                break;
            }
            log.LogDebug("Email Offline Service is stoped.");
        }
    }
}

使用 Hangfire,它的后台方法功能很棒,并且免费为您提供了一个漂亮的仪表板: https://docs.hangfire.io/en/latest/background-methods/index.html

我认为最简单的方法是在处理发送电子邮件请求的代码中进行即发即弃调用,就像这样-

//all done, time to send email
Task.Run(async () => 
{
    await emailService.EmailOffline(emailInfo).ConfigureAwait(false); //assume all necessary info to send email is saved in emailInfo
});

这将启动一个线程来发送电子邮件。 该代码将 return 立即发送给调用者。 在您的 EmailOffline 方法中,您可以根据需要包含时间延迟逻辑。 确保在其中也包含错误日志记录逻辑,否则来自 EmailOffline 的异常可能会被默默地吞下。

P.S。 - 对 Coastpear 和 FlyingV 的回答 -

无需关心调用上下文的结束。这项工作将在一个单独的线程上完成,它完全独立于调用上下文。

我已经在生产中使用类似的机制几年了,到目前为止零问题。

如果您的网站不是特别忙,而且工作也不重要,这是最简单的解决方案。 只需确保您在工作人员(本例中为 EmailOffline)中捕获并记录错误即可。

如果您需要更可靠的解决方案,我建议使用AWS SQS 等成熟的队列产品,不要费心自己创建一个。打造一个真正优秀的排队系统,并不是一件容易的事。

您可以尝试将异步队列与 BackgroundService 结合使用。

public class BackgroundEmailService : BackgroundService
{
    private readonly IBackgroundTaskQueue _queue;

    public BackgroundEmailService(IBackgroundTaskQueue queue)
    {
        _queue = queue;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var job = await _queue.DequeueAsync(stoppingToken);
            
            _ = ExecuteJobAsync(job, stoppingToken);
        }
    }

    private async Task ExecuteJobAsync(JobInfo job, CancellationToken stoppingToken)
    {
        try
        {
            await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
            // todo send email
        }
        catch (Exception ex)
        {
            // todo log exception
        }
    }
}

public interface IBackgroundTaskQueue
{
    void EnqueueJob(JobInfo job);

    Task<JobInfo> DequeueAsync(CancellationToken cancellationToken);
}

通过这种方式,您可以在控制器中注入 IBackgroundTaskQueue 并将作业排入其中,而 JobInfo 将包含一些在后台执行作业的基本信息,例如:

public class JobInfo
{
    public string EmailAddress { get; set; }
    public string Body { get; set; }
}

后台队列示例(受ASP.NET Core documentation启发):

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private ConcurrentQueue<JobInfo> _jobs = new ConcurrentQueue<JobInfo>();
    private SemaphoreSlim _signal = new SemaphoreSlim(0);

    public void EnqueueJob(JobInfo job)
    {
        if (job == null)
        {
            throw new ArgumentNullException(nameof(job));
        }

        _jobs.Enqueue(job);
        _signal.Release();
    }

    public async Task<JobInfo> DequeueAsync(CancellationToken cancellationToken)
    {
        await _signal.WaitAsync(cancellationToken);
        _jobs.TryDequeue(out var job);

        return job;
    }
}