C# 如何在 API 控制器中进行即时压缩和发送以避免大内存缓冲区
C# How to do on-the-fly compression-and-send in a API controller to avoid large memory buffers
我想要实现的是从 API 向请求者发送备份(流),并在此过程中避免占用大量 RAM。我一直在尝试使用 Pipes
,但没有成功(可能缺乏知识;))
看看下面的例程:
[HttpGet]
[Route("{projectIdentifier}")]
[SwaggerResponse(StatusCodes.Status200OK)]
[SwaggerResponse(StatusCodes.Status404NotFound)]
public async Task<IActionResult> BackupProject(string projectIdentifier, bool zipped = false)
{
if (!TryGetProject(projectIdentifier, out var project))
return this.NotFoundResult(nameof(projectIdentifier), projectIdentifier);
string filenamePartName = $"Project {project.Identifier}";
string saveFilename = Path.GetInvalidFileNameChars().Aggregate(filenamePartName, (current, c) => current.Replace(c, '_'));
MemoryStream backupstream = new();
await Core.Iapetus.Instance.Projects.BackupAsync(backupstream, project.ID, zipped);
backupstream.Position = 0;
var file = zipped ? File(backupstream, "application/zip", $"{saveFilename}.json.gz") : File(backupstream, "application/json", $"{saveFilename}.json");
return file;
}
BackupAsync 例程支持使用 GZipSTream
class 进行压缩,并且可以即时运行。不幸的是,压缩(或未压缩)结果仍然落在 backupstream
内存缓冲区中,在某些情况下占用的 space 比我喜欢的多得多。
问题
有没有办法“跳过”内存缓冲区,并将数据直接从(压缩的)流发送到 File
响应?并且 - 当然 - 保留整个 shabang async
TIA
ASP.NET 中没有流式文件结果类型,但它们确实提供了定义您自己的文件类型的支持。
您可以使用类似于 on my blog 的流式文件结果类型,更新为 ASP.NET 6:
public sealed class FileCallbackResult : FileResult
{
private readonly Func<Stream, ActionContext, Task> _callback;
public FileCallbackResult(MediaTypeHeaderValue contentType, Func<Stream, ActionContext, Task> callback)
: base(contentType.ToString())
{
_ = callback ?? 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, fileLength: null, enableRangeProcessing: false);
return result._callback(context.HttpContext.Response.Body, context);
}
}
}
它的用法是这样的:
[HttpGet]
[Route("{projectIdentifier}")]
[SwaggerResponse(StatusCodes.Status200OK)]
[SwaggerResponse(StatusCodes.Status404NotFound)]
public IActionResult BackupProject(string projectIdentifier, bool zipped = false)
{
if (!TryGetProject(projectIdentifier, out var project))
return this.NotFoundResult(nameof(projectIdentifier), projectIdentifier);
string filenamePartName = $"Project {project.Identifier}";
string saveFilename = Path.GetInvalidFileNameChars().Aggregate(filenamePartName, (current, c) => current.Replace(c, '_'));
return new FileCallbackResult(
new MediaTypeHeaderValue(zipped ? "application/zip" : "application/json"),
async (outputStream, _) =>
{
await Core.Iapetus.Instance.Projects.BackupAsync(outputStream, project.ID, zipped);
})
{
FileDownloadName = zipped ? $"{saveFilename}.json.gz" : $"{saveFilename}.json",
};
}
一句警告(也注明 on my blog):错误处理现在更加笨拙。以前,如果 BackupAsync
抛出异常,您会 return 向客户端发送错误代码。现在您正在流式传输结果,headers(包括 200 OK
)已经发送,因此如果 BackupAsync
抛出异常, ASP.NET 必须通知客户端的唯一方法是终止连接,不同的浏览器可能对此有不同的解释。我相信大多数浏览器会正确显示“下载错误”或一些此类通用消息,但在这种情况下,用户可能会得到一个被截断的文件。
我想要实现的是从 API 向请求者发送备份(流),并在此过程中避免占用大量 RAM。我一直在尝试使用 Pipes
,但没有成功(可能缺乏知识;))
看看下面的例程:
[HttpGet]
[Route("{projectIdentifier}")]
[SwaggerResponse(StatusCodes.Status200OK)]
[SwaggerResponse(StatusCodes.Status404NotFound)]
public async Task<IActionResult> BackupProject(string projectIdentifier, bool zipped = false)
{
if (!TryGetProject(projectIdentifier, out var project))
return this.NotFoundResult(nameof(projectIdentifier), projectIdentifier);
string filenamePartName = $"Project {project.Identifier}";
string saveFilename = Path.GetInvalidFileNameChars().Aggregate(filenamePartName, (current, c) => current.Replace(c, '_'));
MemoryStream backupstream = new();
await Core.Iapetus.Instance.Projects.BackupAsync(backupstream, project.ID, zipped);
backupstream.Position = 0;
var file = zipped ? File(backupstream, "application/zip", $"{saveFilename}.json.gz") : File(backupstream, "application/json", $"{saveFilename}.json");
return file;
}
BackupAsync 例程支持使用 GZipSTream
class 进行压缩,并且可以即时运行。不幸的是,压缩(或未压缩)结果仍然落在 backupstream
内存缓冲区中,在某些情况下占用的 space 比我喜欢的多得多。
问题
有没有办法“跳过”内存缓冲区,并将数据直接从(压缩的)流发送到 File
响应?并且 - 当然 - 保留整个 shabang async
TIA
ASP.NET 中没有流式文件结果类型,但它们确实提供了定义您自己的文件类型的支持。
您可以使用类似于 on my blog 的流式文件结果类型,更新为 ASP.NET 6:
public sealed class FileCallbackResult : FileResult
{
private readonly Func<Stream, ActionContext, Task> _callback;
public FileCallbackResult(MediaTypeHeaderValue contentType, Func<Stream, ActionContext, Task> callback)
: base(contentType.ToString())
{
_ = callback ?? 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, fileLength: null, enableRangeProcessing: false);
return result._callback(context.HttpContext.Response.Body, context);
}
}
}
它的用法是这样的:
[HttpGet]
[Route("{projectIdentifier}")]
[SwaggerResponse(StatusCodes.Status200OK)]
[SwaggerResponse(StatusCodes.Status404NotFound)]
public IActionResult BackupProject(string projectIdentifier, bool zipped = false)
{
if (!TryGetProject(projectIdentifier, out var project))
return this.NotFoundResult(nameof(projectIdentifier), projectIdentifier);
string filenamePartName = $"Project {project.Identifier}";
string saveFilename = Path.GetInvalidFileNameChars().Aggregate(filenamePartName, (current, c) => current.Replace(c, '_'));
return new FileCallbackResult(
new MediaTypeHeaderValue(zipped ? "application/zip" : "application/json"),
async (outputStream, _) =>
{
await Core.Iapetus.Instance.Projects.BackupAsync(outputStream, project.ID, zipped);
})
{
FileDownloadName = zipped ? $"{saveFilename}.json.gz" : $"{saveFilename}.json",
};
}
一句警告(也注明 on my blog):错误处理现在更加笨拙。以前,如果 BackupAsync
抛出异常,您会 return 向客户端发送错误代码。现在您正在流式传输结果,headers(包括 200 OK
)已经发送,因此如果 BackupAsync
抛出异常, ASP.NET 必须通知客户端的唯一方法是终止连接,不同的浏览器可能对此有不同的解释。我相信大多数浏览器会正确显示“下载错误”或一些此类通用消息,但在这种情况下,用户可能会得到一个被截断的文件。