Я использую . NET Core 3.1 . Я следовал за этой записью в блоге от Стивена Клири, чтобы добиться потоковой передачи файла .zip непосредственно клиенту (без предварительного сохранения в памяти). Я добавил много комментариев, чтобы код было легче понять.
AttachmentsController.cs
public IActionResult DownloadAll(int commentId)
{
var syncIOFeature = HttpContext.Features.Get<IHttpBodyControlFeature>();
if (syncIOFeature != null)
{
syncIOFeature.AllowSynchronousIO = true; // DOESN'T WORK IF I REMOVE THIS
}
List<Attachment> attachments = _attachmentsService.GetAll(commentId);
return new FileCallbackResult(new MediaTypeHeaderValue("application/octet-stream"), async (outputStream, _) =>
{
using (var zipArchive = new ZipArchive(outputStream, ZipArchiveMode.Create))
{
foreach (Attachment attachment in attachments)
{
var zipEntry = zipArchive.CreateEntry(attachment.FileName));
using (var zipStream = zipEntry.Open())
{
using (var transaction = _dbContext.Database.BeginTransaction())
{
try
{
using (var stream = _dbContext.GetLargeObjectStream(attachment.Id))
{
await stream.CopyToAsync(zipStream);
}
transaction.Commit();
}
catch
{
transaction.Rollback();
}
}
}
}
}
})
{
FileDownloadName = "Attachments.zip"
};
}
FileCallbackResult.cs
/// <summary>
/// Represents an <see cref="ActionResult"/> that when executed will
/// execute a callback to write the file content out as a stream.
/// </summary>
public class FileCallbackResult : FileResult
{
private Func<Stream, ActionContext, Task> _callback;
/// <summary>
/// Creates a new <see cref="FileCallbackResult"/> instance.
/// </summary>
/// <param name="contentType">The Content-Type header of the response.</param>
/// <param name="callback">The stream with the file.</param>
public FileCallbackResult(string contentType, Func<Stream, ActionContext, Task> callback)
: this(MediaTypeHeaderValue.Parse(contentType), callback)
{ }
/// <summary>
/// Creates a new <see cref="FileCallbackResult"/> instance.
/// </summary>
/// <param name="contentType">The Content-Type header of the response.</param>
/// <param name="callback">The stream with the file.</param>
public FileCallbackResult(MediaTypeHeaderValue contentType, Func<Stream, ActionContext, Task> callback)
: base(contentType?.ToString())
{
Callback = callback ?? throw new ArgumentNullException(nameof(callback));
}
/// <summary>
/// Gets or sets the callback responsible for writing the file content to the output stream.
/// </summary>
public Func<Stream, ActionContext, Task> Callback
{
get
{
return _callback;
}
set
{
_callback = value ?? throw new ArgumentNullException(nameof(value));
}
}
/// <inheritdoc />
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);
}
}
/// <summary>
/// An action result handler of type file
/// </summary>
internal sealed class FileCallbackResultExecutor : FileResultExecutorBase
{
/// <summary>
/// Creating an instance of a class <see cref="FileCallbackResultExecutor"/>
/// </summary>
/// <param name="loggerFactory"></param>
public FileCallbackResultExecutor(ILoggerFactory loggerFactory)
: base(CreateLogger<FileCallbackResultExecutor>(loggerFactory))
{ }
/// <summary>
/// Handler execution
/// </summary>
/// <param name="context">Action context</param>
/// <param name="result">Action result</param>
/// <returns><see cref="Task"/></returns>
public Task ExecuteAsync(ActionContext context, FileCallbackResult result)
{
SetHeadersAndLog(context, result, null, result.EnableRangeProcessing);
return result.Callback(context.HttpContext.Response.Body, context);
}
}
Как я могу заставить его работать без установки syncIOFeature.AllowSynchronousIO = true
? Я предполагаю, что эта строка заставляет код работать медленнее? Я заметил, что загрузка zip примерно в 4 раза медленнее, чем загрузка только одного файла без его архивирования.