在 ASP.NET Core 中流式传输内存中生成的文件

Streaming an in-memory generated file in ASP.NET Core

在网上搜索了几个小时后,我不知道如何解决 ASP.NET Core 2.x 的问题。

我正在动态生成 CSV(这可能需要几分钟),然后尝试将其发送回客户端。许多客户端在我开始发送响应之前超时,因此我试图将文件流式传输回它们(立即响应 200)并异步写入流。以前 ASP 中的 PushStreamContent 似乎可以做到这一点,但我不确定如何构建我的代码,以便异步完成 CSV 生成并立即返回 HTTP 响应。

[HttpGet("csv")]
public async Task<FileStreamResult> GetCSV(long id)
{
    // this stage can take 2+ mins, which obviously blocks the response
    var data = await GetData(id);
    var records = _csvGenerator.GenerateRecords(data); 

    // using the CsvHelper Nuget package
    var stream = new MemoryStream();
    var writer = new StreamWriter(stream);
    var csv = new CsvWriter(writer);

    csv.WriteRecords(stream, records);
    await writer.FlushAsync();

    return new FileStreamResult(stream, new MediaTypeHeaderValue("text/csv))
    {
        FileDownloadName = "results.csv"
    };
 }

如果您向此控制器方法发出请求,在整个 CSV 生成完成之前您将一无所获,然后您最终会收到响应,此时大多数客户端请求已超时。

我试过将 CSV 生成代码包装在 Task.Run() 中,但这对我的问题也没有帮助。

如果文档生成需要 2+ 分钟,则应该是 asynchronous。可能是这样的:

  1. 客户端发送生成文档的请求
  2. 您接受请求,开始在后台生成并回复 generation has been started, we will notify you
  3. 之类的消息
  4. 在客户端你定期检查文档是否准备好并最终得到link

您也可以使用 signalr 来完成。步骤相同,但客户端不需要检查文档的状态。您可以在文档完成后按link。

没有 PushStreamContext 类型的 built-in 到 ASP.NET 核心。但是,您可以 build your own FileCallbackResult which does the same thing. This example code 应该这样做:

public class FileCallbackResult : FileResult
{
    private Func<Stream, ActionContext, Task> _callback;

    public FileCallbackResult(MediaTypeHeaderValue contentType, Func<Stream, ActionContext, Task> callback)
        : base(contentType?.ToString())
    {
        if (callback == null)
            throw new ArgumentNullException(nameof(callback));
        _callback = callback;
    }

    public override Task ExecuteResultAsync(ActionContext context)
    {
        if (context == null)
            throw new ArgumentNullException(nameof(context));
        var executor = new FileCallbackResultExecutor(context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>());
        return executor.ExecuteAsync(context, this);
    }

    private sealed class FileCallbackResultExecutor : FileResultExecutorBase
    {
        public FileCallbackResultExecutor(ILoggerFactory loggerFactory)
            : base(CreateLogger<FileCallbackResultExecutor>(loggerFactory))
        {
        }

        public Task ExecuteAsync(ActionContext context, FileCallbackResult result)
        {
            SetHeadersAndLog(context, result, null);
            return result._callback(context.HttpContext.Response.Body, context);
        }
    }
}

用法:

[HttpGet("csv")]
public IActionResult GetCSV(long id)
{
  return new FileCallbackResult(new MediaTypeHeaderValue("text/csv"), async (outputStream, _) =>
  {
    var data = await GetData(id);
    var records = _csvGenerator.GenerateRecords(data); 
    var writer = new StreamWriter(outputStream);
    var csv = new CsvWriter(writer);
    csv.WriteRecords(stream, records);
    await writer.FlushAsync();
  })
  {
    FileDownloadName = "results.csv"
  };
}

请记住 FileCallbackResultPushStreamContext 具有相同的限制:如果 在回调 中发生错误,则 Web 服务器没有任何好处通知客户该错误的方式。您所能做的就是传播异常,这将导致 ASP.NET 提前关闭连接,因此客户端会收到 "connection unexpectedly closed" 或 "download aborted" 错误。这是因为在 body 开始流式传输之前,HTTP 在 header 中首先发送错误代码