You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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:

  1. 安装Hangfire包(如Hangfire.SqlServer)
  2. 配置Hangfire到DI:
builder.Services.AddHangfire(config => config.UseSqlServerStorage(builder.Configuration.GetConnectionString("HangfireConnection")));
builder.Services.AddHangfireServer();
  1. 控制器中调度任务:
[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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 05:52:09