异步任务中调用IFormFile.CopyTo触发ObjectDisposedException问题
问题:异步处理上传文件时触发ObjectDisposedException异常
我开发了一个.NET Core MVC应用,用于上传并处理.xlsx文档。需求是发起POST请求后,创建异步任务处理文件,同时返回一个GUID,后续通过该GUID定期查询处理结果。但在调用IFormFile.CopyTo(也尝试过CopyToAsync)时,遇到了'FileBufferingReadStream'已释放的ObjectDisposedException异常。
控制器代码
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status409Conflict)] [HttpPost] [RequestFormLimits(MultipartBodyLengthLimit = 209715200)] [RequestSizeLimit(209715200)] [SetIdentity] public async Task<IActionResult> Post([FromForm] UploadRequestOptions value, [FromServices] IServiceScopeFactory serviceScopeFactory) { var requestId = Guid.NewGuid().ToString(); try { try { var caller = User.FindFirst(ClaimTypes.NameIdentifier)?.Subject; value.UserId = caller.Name; //尝试修复释放问题的方案#1:缓存文件并在UploadBom方法中获取 var file = value.File; var cachedFile = new Tuple<string, IFormFile>(requestId, file); await _cache.SetAsync(requestId, cachedFile); Task.Run(async () => { using var scope = serviceScopeFactory.CreateScope(); var upService = scope.ServiceProvider.GetRequiredService<IBomUploadService>(); await upService.UploadBom(value, requestId); }); } catch (ValidationException ex) { _logger.LogError(ex, ex.ToString()); return StatusCode(409, ex.Message); } return Ok(requestId); } catch (Exception ex) { _logger.LogError(ex, ex.ToString()); return StatusCode(500, ex.Message); } }
服务端UploadBom方法代码
public async Task<int> UploadBom(UploadRequestOptions options, string requestId) { try { //尝试修复方案#1:获取缓存的文件 var cachedFile = await _cache.GetAsync<Tuple<string, IFormFile>>(requestId); var File = cachedFile.Item2; using (var memoryStream = new MemoryStream()) { //此处触发异常:Cannot access a disposed object. Object name: 'FileBufferingReadStream'. await File.CopyToAsync(memoryStream); if (reader.LoadFile(memoryStream, "Tab1")) { result.Tab1Exists = true; result = await tabValidator.ValidateTabAsync(reader); } else { result.Tab1Exists = false; } } } }
异常堆栈信息
at Microsoft.AspNetCore.WebUtilities.FileBufferingReadStream.ThrowIfDisposed() at Microsoft.AspNetCore.WebUtilities.FileBufferingReadStream.set_Position(Int64 value) at Microsoft.AspNetCore.Http.ReferenceReadStream..ctor(Stream inner, Int64 offset, Int64 length) at Microsoft.AspNetCore.Http.FormFile.OpenReadStream() at Microsoft.AspNetCore.Http.FormFile.CopyTo(Stream target) at Hamilton.Api.Services.BomUploadService.<UploadBom>d__6.MoveNext() in C:\Development\Work Code\api\new-hamilton-api\HamiltonAPI\Services\BomUploadService.cs:line 79
问题根源
IFormFile内部的FileBufferingReadStream与请求上下文强绑定,当控制器返回响应后,ASP.NET Core框架会自动释放该流。你试图在异步任务中复用IFormFile对象,必然触发已释放异常,缓存IFormFile的做法无效,因为它依赖的底层流已经被销毁。
解决方案
方案1:请求上下文内先读取文件内容,再传递给异步任务
在控制器返回响应前,将文件内容读取到内存或临时文件中,缓存内容而非IFormFile对象:
修改后的控制器代码
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status409Conflict)] [HttpPost] [RequestFormLimits(MultipartBodyLengthLimit = 209715200)] [RequestSizeLimit(209715200)] [SetIdentity] public async Task<IActionResult> Post([FromForm] UploadRequestOptions value, [FromServices] IServiceScopeFactory serviceScopeFactory) { var requestId = Guid.NewGuid().ToString(); try { var caller = User.FindFirst(ClaimTypes.NameIdentifier)?.Subject; value.UserId = caller.Name; // 先将文件读取到内存流(大文件建议用临时文件路径) using var memoryStream = new MemoryStream(); await value.File.CopyToAsync(memoryStream); memoryStream.Position = 0; var fileBytes = memoryStream.ToArray(); // 缓存文件内容和请求参数 await _cache.SetAsync($"{requestId}_file", fileBytes); await _cache.SetAsync(requestId, value); Task.Run(async () => { using var scope = serviceScopeFactory.CreateScope(); var upService = scope.ServiceProvider.GetRequiredService<IBomUploadService>(); await upService.UploadBom(requestId); }); return Ok(requestId); } catch (ValidationException ex) { _logger.LogError(ex, ex.ToString()); return StatusCode(409, ex.Message); } catch (Exception ex) { _logger.LogError(ex, ex.ToString()); return StatusCode(500, ex.Message); } }
修改后的服务代码
public async Task<int> UploadBom(string requestId) { try { // 获取缓存的文件内容和请求参数 var fileBytes = await _cache.GetAsync<byte[]>(requestId + "_file"); var options = await _cache.GetAsync<UploadRequestOptions>(requestId); using (var memoryStream = new MemoryStream(fileBytes)) { if (reader.LoadFile(memoryStream, "Tab1")) { result.Tab1Exists = true; result = await tabValidator.ValidateTabAsync(reader); } else { result.Tab1Exists = false; } } // 清理缓存 await _cache.RemoveAsync(requestId); await _cache.RemoveAsync(requestId + "_file"); return result; } catch (Exception ex) { _logger.LogError(ex, "文件处理失败"); throw; } }
方案2:使用后台任务队列(更优实现)
直接用Task.Run并非后台任务的最佳实践,建议基于ASP.NET Core的IHostedService实现可靠的后台任务队列:
- 定义任务队列接口与实现
public interface IBackgroundTaskQueue { ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, Task> workItem); } 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 async ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, Task> workItem) { if (workItem == null) throw new ArgumentNullException(nameof(workItem)); await _queue.Writer.WriteAsync(workItem); } public async ValueTask<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken) { return await _queue.Reader.ReadAsync(cancellationToken); } }
- 实现后台服务
public class QueuedHostedService : BackgroundService { private readonly ILogger<QueuedHostedService> _logger; private readonly IBackgroundTaskQueue _taskQueue; private readonly IServiceScopeFactory _serviceScopeFactory; public QueuedHostedService(IBackgroundTaskQueue taskQueue, ILogger<QueuedHostedService> logger, IServiceScopeFactory serviceScopeFactory) { _taskQueue = taskQueue; _logger = logger; _serviceScopeFactory = serviceScopeFactory; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("后台任务队列服务启动"); while (!stoppingToken.IsCancellationRequested) { var workItem = await _taskQueue.DequeueAsync(stoppingToken); try { using var scope = _serviceScopeFactory.CreateScope(); await workItem(stoppingToken); } catch (Exception ex) { _logger.LogError(ex, "执行后台任务失败"); } } _logger.LogInformation("后台任务队列服务停止"); } }
- 注册服务(Program.cs)
builder.Services.AddSingleton<IBackgroundTaskQueue>(_ => new BackgroundTaskQueue(100)); builder.Services.AddHostedService<QueuedHostedService>();
- 修改控制器代码
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status409Conflict)] [HttpPost] [RequestFormLimits(MultipartBodyLengthLimit = 209715200)] [RequestSizeLimit(209715200)] [SetIdentity] public async Task<IActionResult> Post([FromForm] UploadRequestOptions value, [FromServices] IBackgroundTaskQueue taskQueue, [FromServices] IServiceScopeFactory serviceScopeFactory) { var requestId = Guid.NewGuid().ToString(); try { var caller = User.FindFirst(ClaimTypes.NameIdentifier)?.Subject; value.UserId = caller.Name; // 读取文件内容 using var memoryStream = new MemoryStream(); await value.File.CopyToAsync(memoryStream); var fileBytes = memoryStream.ToArray(); // 缓存数据 await _cache.SetAsync($"{requestId}_file", fileBytes); await _cache.SetAsync(requestId, value); // 将任务加入队列 await taskQueue.QueueBackgroundWorkItemAsync(async token => { using var scope = serviceScopeFactory.CreateScope(); var upService = scope.ServiceProvider.GetRequiredService<IBomUploadService>(); await upService.UploadBom(requestId); }); return Ok(requestId); } catch (ValidationException ex) { _logger.LogError(ex, ex.ToString()); return StatusCode(409, ex.Message); } catch (Exception ex) { _logger.LogError(ex, ex.ToString()); return StatusCode(500, ex.Message); } }
注意事项
- 若上传文件过大(如超过100MB),不建议用内存流缓存,应保存到临时目录,缓存文件路径,处理完成后删除临时文件。
- 合理设置缓存过期时间,避免占用过多资源。
- 根据业务量调整后台任务队列的容量,防止队列溢出。
内容的提问来源于stack exchange,提问作者DeepToot
相关产品推荐
相关产品推荐

