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

ASP.NET Core异步文件处理方法二次调用出现System.ObjectDisposedException

问题根源

问题出在IFormFile的流生命周期绑定到当前HTTP请求:ASP.NET Core在请求处理完成后会自动释放IFormFile关联的流,而你用Task.Run启动的后台任务是在请求上下文之外异步执行的,当后台任务尝试读取流时,流已经被框架释放,导致ObjectDisposedException。首次调用成功只是巧合(后台任务在请求结束前完成了流读取),后续调用时请求已提前结束,流被释放。

解决方案

1. 提前缓存文件内容

在启动后台任务前,将IFormFile的内容复制到独立的内存流或临时文件,脱离请求上下文的生命周期绑定:

private async Task<string> ProcessFileAsync(IFormFile file, string apiUrl)
{
    string taskId = Guid.NewGuid().ToString();
    _tasksStatus.TryAdd(taskId, "Processing");

    // 提前复制文件内容到内存流(脱离请求绑定)
    using var memoryStream = new MemoryStream();
    await file.CopyToAsync(memoryStream);
    memoryStream.Position = 0; // 重置流位置以便后续读取

    // 传递内存流到后台任务
    _backgroundTaskQueue.QueueBackgroundWorkItem(async token =>
    {
        try
        {
            using var client = _httpClientFactory.CreateClient();
            using var content = new StreamContent(memoryStream);
            // 设置Content-Type等请求头
            content.Headers.ContentType = MediaTypeHeaderValue.Parse(file.ContentType);
            
            var response = await client.PostAsync(apiUrl, content, token);
            response.EnsureSuccessStatusCode();

            _tasksStatus.AddOrUpdate(taskId, "Completed", (key, old) => "Completed");
        }
        catch (Exception ex)
        {
            _tasksStatus.AddOrUpdate(taskId, "Error", (key, old) => "Error");
        }
    });

    return taskId;
}

2. 使用ASP.NET Core官方后台任务队列替代Task.Run

直接用Task.Run启动的后台任务不受主机生命周期管理,可能在应用重启、回收时被强制终止。ASP.NET Core推荐使用**后台任务队列(IBackgroundTaskQueue)**来管理异步后台任务,确保任务被正确调度和跟踪:

第一步:定义后台任务队列

public interface IBackgroundTaskQueue
{
    void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
    Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, Task>> _queue;

    public BackgroundTaskQueue(int capacity)
    {
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<CancellationToken, Task>>(options);
    }

    public void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        _queue.Writer.TryWrite(workItem);
    }

    public async Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken)
    {
        var workItem = await _queue.Reader.ReadAsync(cancellationToken);
        return workItem;
    }
}

第二步:实现后台任务处理器(HostedService)

public class QueuedHostedService : BackgroundService
{
    private readonly ILogger<QueuedHostedService> _logger;
    private readonly IBackgroundTaskQueue _taskQueue;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, ILogger<QueuedHostedService> logger)
    {
        _taskQueue = taskQueue;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Queued hosted service is running.");

        while (!stoppingToken.IsCancellationRequested)
        {
            var workItem = await _taskQueue.DequeueAsync(stoppingToken);

            try
            {
                await workItem(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error occurred executing {WorkItem}.", nameof(workItem));
            }
        }

        _logger.LogInformation("Queued hosted service is stopping.");
    }
}

第三步:注册服务到DI容器

在Program.cs中添加:

builder.Services.AddSingleton<IBackgroundTaskQueue>(_ => new BackgroundTaskQueue(100));
builder.Services.AddHostedService<QueuedHostedService>();

3. 确保线程安全的任务状态跟踪

ConcurrentDictionary的TryAdd是线程安全的,但修改已有键值时要使用AddOrUpdate方法,避免并发冲突:

// 替代直接赋值_tasksStatus[taskId] = "Completed"
_tasksStatus.AddOrUpdate(taskId, "Completed", (key, oldValue) => "Completed");
关键最佳实践
  • 不要直接在后台任务中使用请求绑定的资源:IFormFile、HttpContext等都是请求上下文的一部分,请求结束后会被释放,必须提前复制或缓存资源。
  • 使用官方后台任务机制:避免用Task.Run或ThreadPool.QueueUserWorkItem管理长期后台任务,ASP.NET Core的IBackgroundTaskQueue+BackgroundService是标准实现,支持任务调度、生命周期管理和取消。
  • 线程安全的状态管理:对于多个后台任务共享的状态(如_tasksStatus),必须使用线程安全的集合(如ConcurrentDictionary)并采用线程安全的修改方法。

内容的提问来源于stack exchange,提问作者Clash Secondo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 22:44:53