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

异步任务中调用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实现可靠的后台任务队列:

  1. 定义任务队列接口与实现
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);
    }
}
  1. 实现后台服务
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("后台任务队列服务停止");
    }
}
  1. 注册服务(Program.cs)
builder.Services.AddSingleton<IBackgroundTaskQueue>(_ => new BackgroundTaskQueue(100));
builder.Services.AddHostedService<QueuedHostedService>();
  1. 修改控制器代码
[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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 13:05:55