在 ASP.NET Core 中构建可扩展的 Web 应用程序时,通常需要执行耗时的任务,例如发送电子邮件、数据处理或调用外部 API,而不会阻止主要的请求-响应流。在后台运行这些操作可以显著提高应用程序性能。
在这里,我们将学习如何在不使用 Hangfire 等库的情况下创建自定义后台任务队列和处理器。我们将演示如何使用 QueueBackgroundWorkItem 方法将作业传递到后台服务,并从 _API 控制器_触发_后台_任务,包括发送电子邮件作为示例。
为什么使用后台作业?
_后台_作业对于不需要阻止用户与应用程序交互的任务至关重要。例如:
电子邮件通知: 在用户操作后发送电子邮件。
长时间运行的进程: 执行数据密集型操作。
第三方 API 调用: 与外部服务的非阻塞交互。
通过将这些任务排队以在后台运行,我们可以释放服务器来处理不同的请求,从而提高应用程序的总体效率。
了解后台任务队列
ASP.NET Core 的 BackgroundService 提供了一种实现长时间运行的后台任务的方法。为了使其更具适应性,我们可以设置一个后台任务队列,以便我们添加要稍后处理的任务。排队的任务将由后台 worker 异步处理。
设置任务队列
首先,我们将为后台任务队列定义一个接口:
public interface IBackgroundTaskQueue
{
void QueueBackgroundWorkItem(Func<IServiceProvider, CancellationToken, Task> workItem);
Task<Func<IServiceProvider, CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken);
}
QueueBackgroundWorkItem 方法允许对任务进行排队,而 DequeueAsync 方法检索这些任务进行处理。
任务队列实现
接下来,我们使用 ConcurrentQueue 和 SemaphoreSlim 实现此接口,以便在新任务可用时发出信号:
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private readonly SemaphoreSlim _signal = new(0);
private readonly ConcurrentQueue<Func<IServiceProvider, CancellationToken, Task>> _workItems = new();
public void QueueBackgroundWorkItem(Func<IServiceProvider, CancellationToken, Task> workItem)
{
if (workItem == null) throw new ArgumentNullException(nameof(workItem));
_workItems.Enqueue(workItem);
_signal.Release(); // Signal that a new item is available
}
public async Task<Func<IServiceProvider, CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken)
{
await _signal.WaitAsync(cancellationToken);
_workItems.TryDequeue(out var workItem);
return workItem!;
}
}
这个类允许我们使用 ConcurrentQueue 以线程安全的方式将任务排入队列,并在添加任务时向后台服务发出信号以开始处理。
创建后台处理器
队列就位后,我们需要一个后台服务来处理排队的任务。ASP.NET Core 的 BackgroundService 是实现此目的的理想候选项:
public class QueuedProcessorBackgroundService : BackgroundService
{
private readonly IBackgroundTaskQueue _taskQueue;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger;
public QueuedProcessorBackgroundService(IBackgroundTaskQueue taskQueue,
IServiceProvider serviceProvider,
ILoggerFactory loggerFactory)
{
_taskQueue = taskQueue;
_serviceProvider = serviceProvider;
_logger = loggerFactory.CreateLogger<QueuedProcessorBackgroundService>();
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Queued Processor Background Service is starting.");
while (!cancellationToken.IsCancellationRequested)
{
var workItem = await _taskQueue.DequeueAsync(cancellationToken);
try
{
await workItem(_serviceProvider, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error occurred executing {nameof(workItem)}.");
}
}
_logger.LogInformation("Queued Processor Background Service is stopping.");
}
}
此服务持续检查任务队列,并在任务出队时对其进行处理。如果服务已停止,则取消令牌可确保正常关闭任何正在进行的任务。
从 API 将作业排队
现在,我们可以创建一个 API 终端节点,用于将作业排队以进行后台处理。该作业将从 IServiceProvider 中解析所需的服务(如 _IEmailService_),并异步处理它们。
[ApiController]
[Route("api/[controller]")]
public class JobController : ControllerBase
{
private readonly IBackgroundTaskQueue _taskQueue;
public JobController(IBackgroundTaskQueue taskQueue)
{
_taskQueue = taskQueue;
}
[HttpPost("enqueue-email")]
public IActionResult EnqueueEmailJob([FromBody] EmailRequest emailRequest)
{
_taskQueue.QueueBackgroundWorkItem(async (serviceProvider, token) =>
{
var logger = serviceProvider.GetRequiredService<ILogger<JobController>>();
var emailService = serviceProvider.GetRequiredService<IEmailService>();
logger.LogInformation("Email job started");
try
{
// Send the email
await emailService.SendEmailAsync(emailRequest.To, emailRequest.Subject, emailRequest.Body, token);
logger.LogInformation("Email job completed successfully.");
}
catch (Exception ex)
{
logger.LogError(ex, "Error occurred while sending email.");
}
});
return Ok("Email job has been queued.");
}
}
此 API 终端节点接收电子邮件请求,并使用任务队列对电子邮件发送作业进行排队。
完整示例:在后台发送电子邮件
要在后台发送电子邮件,我们将定义一个模型 EmailRequest 来处理传入的电子邮件数据,并定义一个电子邮件服务来模拟发送电子邮件。
EmailRequest 模型
public class EmailRequest
{
public string To { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
}
IEmailService 和 EmailService 实现
public interface IEmailService
{
Task SendEmailAsync(string to, string subject, string body, CancellationToken token);
}
public class EmailService : IEmailService
{
private readonly ILogger<EmailService> _logger;
public EmailService(ILogger<EmailService> logger)
{
_logger = logger;
}
public async Task SendEmailAsync(string to, string subject, string body, CancellationToken token)
{
_logger.LogInformation($"Sending email to {to} with subject {subject}.");
// Simulate email sending delay
await Task.Delay(2000, token);
_logger.LogInformation($"Email to {to} sent successfully.");
}
}
此服务模拟发送具有较小延迟的电子邮件。在实际场景中,这将涉及与 SMTP 服务器或第三方电子邮件提供商(如 SendGrid)集成。
在 Startup.cs 中注册服务
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
services.AddHostedService<QueuedProcessorBackgroundService>();
services.AddSingleton<IEmailService, EmailService>();
services.AddControllers();
services.AddLogging();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
}
测试 API
现在,您可以通过向 /api/job/enqueue-email 终端节点发送 POST 请求来触发电子邮件作业:
POST /api/job/enqueue-email
Content-Type: application/json
{
"to": "example@example.com",
"subject": "Test Email",
"body": "This is a test email body."
}
这会将电子邮件作业排入队列,后台服务将处理它,而不会阻止 API 响应。
最佳实践
尊重取消令牌: 始终确保您的后台任务遵循 CancellationToken 以允许正常关闭任务。
错误处理: 在后台作业中实施适当的错误处理,以处理任何故障并提供适当的日志记录。
依赖项解析: 在 QueueBackgroundWorkItem 中正确使用 IServiceProvider 以确保正确的服务生存期(例如,范围服务)。
监测: 考虑使用日志记录或监控工具来跟踪排队和已处理的任务。
在这里,我们构建了一个轻量级解决方案,用于在 ASP.NET Core 中运行后台作业,而无需依赖 Hangfire 等外部库。我们创建了一个后台服务来处理任务,并展示了一种从队列中将任务排入队列并发送电子邮件的方法。此方法可帮助您很好地处理耗时的任务,同时保持应用程序的响应性。
如果你喜欢我的文章,请给我一个赞!谢谢