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
相关产品推荐
相关产品推荐

