.NET Core控制器实现Dapper查询后台写文件遇fire and forget失效问题
问题分析与解决方案
原代码存在的核心问题
- 请求级CancellationToken干扰:传入的
cancellationToken会在HTTP请求返回后立即触发取消,导致数据库查询GetData被中途终止,任务无法完成。 - 不可靠的后台任务执行:在ASP.NET Core中直接用
Task.Run启动后台任务,一旦应用池回收、进程重启或服务器资源紧张,未完成的任务会直接丢失,完全不具备可靠性。 - 服务生命周期错误:代码中直接使用控制器注入的
_bkgservice,而非从创建的scope中获取,可能导致服务生命周期与子任务不匹配,引发对象已释放等异常。 - 无错误处理机制:子任务中的异常会被静默吞掉,无法追踪任务执行失败的原因。
正确实现"即发即弃"的可靠方案
方案1:内置后台服务+任务队列(轻量可靠)
1. 定义任务队列接口与实现
public interface IBackgroundTaskQueue { void QueueBackgroundWorkItem(Func<IServiceProvider, CancellationToken, Task> workItem); Task<Func<IServiceProvider, CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken); } public class BackgroundTaskQueue : IBackgroundTaskQueue { private readonly ConcurrentQueue<Func<IServiceProvider, CancellationToken, Task>> _workItems = new(); private readonly SemaphoreSlim _signal = new(0); public void QueueBackgroundWorkItem(Func<IServiceProvider, CancellationToken, Task> workItem) { if (workItem == null) { throw new ArgumentNullException(nameof(workItem)); } _workItems.Enqueue(workItem); _signal.Release(); } public async Task<Func<IServiceProvider, CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken) { await _signal.WaitAsync(cancellationToken); _workItems.TryDequeue(out var workItem); return workItem; } }
2. 实现后台服务消费队列
public class QueuedHostedService : BackgroundService { private readonly ILogger<QueuedHostedService> _logger; private readonly IBackgroundTaskQueue _taskQueue; private readonly IServiceProvider _serviceProvider; public QueuedHostedService(IBackgroundTaskQueue taskQueue, IServiceProvider serviceProvider, ILogger<QueuedHostedService> logger) { _taskQueue = taskQueue; _serviceProvider = serviceProvider; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Queued hosted service is starting."); while (!stoppingToken.IsCancellationRequested) { var workItem = await _taskQueue.DequeueAsync(stoppingToken); try { await workItem(_serviceProvider, stoppingToken); } catch (Exception ex) { _logger.LogError(ex, "Error occurred executing background task"); } } _logger.LogInformation("Queued hosted service is stopping."); } }
3. 注册服务到DI容器
在Program.cs中添加:
builder.Services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>(); builder.Services.AddHostedService<QueuedHostedService>();
4. 修改控制器代码
[HttpPost] [Route("request/ret")] public IActionResult PostData([FromBody][BindRequired] modelnh request, [Required] string code, [FromServices]IBackgroundTaskQueue taskQueue) { // 将任务加入后台队列,立即返回响应 taskQueue.QueueBackgroundWorkItem(async (sp, ct) => { using var scope = sp.CreateScope(); var bkgService = scope.ServiceProvider.GetRequiredService<IBKGService>(); var fileHelper = scope.ServiceProvider.GetRequiredService<IFileHelper>(); var qrest = await bkgService.GetData(request, code, ct).ConfigureAwait(false); await fileHelper.SaveToFile(qrest.ToList(), tgtdirec, append).ConfigureAwait(false); }); return Ok(); }
5. 修正BKGService方法参数
补充缺失的CancellationToken参数:
public async Task<IEnumerable<rbt>> GetData(modelnh request, string code, CancellationToken cancellationToken) { string query = "SELECT * ........"; result = await _dbprov.RunQueryAsync<rbt>(query, cancellationToken).ConfigureAwait(false); return result; }
方案2:使用Hangfire(第三方库,适合复杂场景)
如果需要任务持久化、重试机制、监控面板等功能,可直接使用Hangfire:
- 安装Hangfire包(如
Hangfire.SqlServer) - 配置Hangfire到DI:
builder.Services.AddHangfire(config => config.UseSqlServerStorage(builder.Configuration.GetConnectionString("HangfireConnection"))); builder.Services.AddHangfireServer();
- 控制器中调度任务:
[HttpPost] [Route("request/ret")] public IActionResult PostData([FromBody][BindRequired] modelnh request, [Required] string code, [FromServices]IBackgroundJobClient backgroundJobClient, [FromServices]IServiceProvider serviceProvider) { backgroundJobClient.Enqueue<IBKGService>(service => service.GetData(request, code, CancellationToken.None) .ContinueWith(task => { var fileHelper = serviceProvider.GetRequiredService<IFileHelper>(); return fileHelper.SaveToFile(task.Result.ToList(), tgtdirec, append); }, TaskContinuationOptions.OnlyOnRanToCompletion)); return Ok(); }
关键注意事项
- 禁止用
Task.Run做ASP.NET Core后台任务:这类方式完全依赖进程存活,应用重启后任务直接丢失。 - 使用独立CancellationToken:不要传递请求级的CancellationToken给后台任务,避免请求结束导致任务被取消。
- 必须通过ServiceScope获取服务:后台任务生命周期独立于请求,必须从Scope中获取服务,避免使用已释放的请求级服务。
- 添加错误日志:后台任务异常无法反馈给前端,必须通过日志记录便于排查问题。
内容的提问来源于stack exchange,提问作者ajaloha
相关产品推荐
相关产品推荐

