ASP.NET 核心手动处理请求体流

ASP.NET Core manually dispose request body stream

我有一种将 CSV 文件上传到服务器的方法。 如果此 CSV 文件有 100 000 行,则处理需要 10 分钟。 所以我尝试在 2 秒后 return 结果并在后台任务中继续处理:

[HttpPost]
[RequestSizeLimit(53 * 1024 * 1024)]
public async Task<IActionResult> Load(IFormFile data)
{
    await await Task.WhenAny(
            Task.Delay(TimeSpan.FromSeconds(2)),
            AsyncProcessData(data));
    return RedirectToAction("Index");
}

如果执行时间超过 2 秒,用户将看到上次上传的状态为“正在处理”,我认为这比用户在几分钟内没有响应的网页要好。

不幸的是,这种方法的结果是只处理了 1760 条记录,并且在读取第 1761 行时抛出错误 - Cannot access a closed file.

据我所知,2 秒后发送了完整的响应并处理了带有传入数据的流。

我希望 ASP.NET 基础架构不使用请求主体处理流,这样我就可以自己做,像这样:

Task.WhenAny(
    Task.Delay(TimeSpan.FromSeconds(2)),
    AsyncProcessData(data).ContinueWith(_ => <dispose_stream_with_request_body>));

我可以吗?

这是不可能的。

这样想:如果您 return 响应 (return RedirectToAction("Index");) 那么客户端(浏览器?)将停止发送数据并因此结束流,即使您以某种方式设法做到避免处理它 server-side。如果客户端停止发送数据,您将无能为力 server-side.

关于您可以做什么的一些建议...

将所有数据加载到内存中

这可能是您最接近当前工作流程的方式。

我不确定 AsyncProcessData 中发生了什么,但我想您正在 line-by-line/chunk-by-chunk 从请求流中读取数据并一次处理一个数据。如果您改为将整个流写入另一个流或类似流,然后使用该新流代替您的处理,您可以在后台线程上安排处理,并在复制流后立即 return 响应。

注意:这意味着您保留了请求 in-memory 中的整个有效负载,这不能很好地扩展。换句话说,如果您允许上传大文件或许多用户同时使用它,您很可能 运行 很快就会内存不足。

保存到 disk/DB 并异步处理

我建议改为如下内容。

当调用 Load 时,您将有效负载(CSV-file)保存到磁盘或数据库中的临时文件中。然后,您安排在后台线程上处理此文件,并像现在一样 return 做出响应。完成后,您可能需要在此处删除临时文件(从磁盘或数据库中)。

注意:就像此处其他解决方案的内存问题一样,您需要确保拥有必要的磁盘space。磁盘 space 通常比内存便宜很多,所以这更具可扩展性。但是,您确实需要确保在完成后记得删除该文件(即使出现问题并且您的应用程序崩溃了)。