在 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
。可能是这样的:
- 客户端发送生成文档的请求
- 您接受请求,开始在后台生成并回复
generation has been started, we will notify you
之类的消息
- 在客户端你定期检查文档是否准备好并最终得到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"
};
}
请记住 FileCallbackResult
与 PushStreamContext
具有相同的限制:如果 在回调 中发生错误,则 Web 服务器没有任何好处通知客户该错误的方式。您所能做的就是传播异常,这将导致 ASP.NET 提前关闭连接,因此客户端会收到 "connection unexpectedly closed" 或 "download aborted" 错误。这是因为在 body 开始流式传输之前,HTTP 在 header 中首先发送错误代码 。
在网上搜索了几个小时后,我不知道如何解决 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
。可能是这样的:
- 客户端发送生成文档的请求
- 您接受请求,开始在后台生成并回复
generation has been started, we will notify you
之类的消息
- 在客户端你定期检查文档是否准备好并最终得到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"
};
}
请记住 FileCallbackResult
与 PushStreamContext
具有相同的限制:如果 在回调 中发生错误,则 Web 服务器没有任何好处通知客户该错误的方式。您所能做的就是传播异常,这将导致 ASP.NET 提前关闭连接,因此客户端会收到 "connection unexpectedly closed" 或 "download aborted" 错误。这是因为在 body 开始流式传输之前,HTTP 在 header 中首先发送错误代码 。