我面临的挑战是我有一个由用户创建的 SQL,这个 SQL 我现在通过 XPO(DevExpress,很快就不再直接通过 NPGSQL 进行包装)运行。在那里,我已经将所有数据加载到内存中并将结果转换为我自己的包装类。
现在我使用自己的对象并使用 CSVHelper 从它创建 CSV 文件。然后我通过 WebAPI 将此文件返回给用户。
由于SQL查询的大小可以是1-2GB,因此内存消耗大量增加。
防止这种情况的最佳方法是什么?
我之前没有太多地使用流,目前正在阅读它们。如果我理解正确的话,MemoryStream 并没有给我带来太多,因为那里的数据也直接加载到内存中。
[HttpGet("export/data")]
public async Task<IActionResult> ExportData(Guid sqlId)
{
this.OpenConn(); //opens the connection
string sql = this.GetSql(sqlId);
using (NpgsqlCommand command = new NpgsqlCommand(sql, conn))
{
int val;
NpgsqlDataReader reader = command.ExecuteReader();
while (reader.Read())
{
// Logic to create the csv file
}
this.CloseConn(); //close the current connection
}
}
我现在可以使用带有 MemoryStream 的 CSVHelper 创建 CSV 文件,然后返回 MemoryStream 进行下载。
var ms = new MemoryStream();
var streamWriter = new StreamWriter(ms, Encoding.UTF8);
var csvWriter = new CsvWriter(streamWriter, CultureInfo.InvariantCulture);
// Logic to create the csv file, with the reader from npgsql
// [...]
return File(ms, "text/csv", "export.csv");
如果我理解正确的话,这并没有给我带来很多好处,因为我已经将数据存储在 MemoryStream 中,因此内存无论如何都被填满了。 处理这个问题的最佳方法是什么?将 CSV 文件写入磁盘上的临时目录然后返回? 例如,我可以在此处访问 S3Bucket,或者使用我自己在 Kubernetes 内的目录。如果我通过文件返回流,我不能在成功传输后简单地删除文件,可以吗?我无法执行最后的操作?我实际上只想传输请求的文件,然后不将其存储在某处。
谢谢!
最简单的方法是在本地保存文件并进行流式传输。
或者,您可以尝试直接写入
Response.Body
(不要忘记刷新)或使用 Stephen Cleary 的 FileCallbackResult
方法:
public class FileCallbackResult : FileResult
{
private Func<Stream, ActionContext, Task> _callback;
public FileCallbackResult(string contentType, Func<Stream, ActionContext, Task> callback)
: base(contentType)
{
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, false);
return result._callback(context.HttpContext.Response.Body, context);
}
}
}
以及示例用法:
[HttpGet("data")]
public async Task<IActionResult> ExportData()
{
return new FileCallbackResult("text/csv", async (outStream, context) =>
{
await using var sw = new StreamWriter(outStream, leaveOpen: true);
await using var csvWriter = new CsvWriter(sw, CultureInfo.InvariantCulture, true);
for (int i = 0; i < 100; i++)
{
await Task.Delay(10);
csvWriter.WriteRecord(new {Int = i, Text = "Qww" + i});
await csvWriter.NextRecordAsync();
}
})
{
FileDownloadName = "qwerty.csv"
};
}