Потоковая сгенерированный в памяти файл в ASP.NET Core - PullRequest
0 голосов
/ 05 июня 2019

Потратив несколько часов в интернете, я теряюсь в том, как решить мою проблему для ASP.NET Core 2.x.

Я генерирую CSV на лету (это может занять несколько минут), а затем пытаюсь отправить его обратно клиенту. У многих клиентов истекает время ожидания, прежде чем я начну посылать ответ, поэтому я пытаюсь передать им файл обратно (с немедленным ответом 200) и выполнить асинхронную запись в поток. Казалось, что это было возможно с PushStreamContent ранее в ASP, но я не уверен, как структурировать мой код, чтобы генерация 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 ]

4 голосов
/ 05 июня 2019

Нет типа PushStreamContext, встроенного в ASP.NET Core.Однако вы можете создать свой собственный FileCallbackResult, который делает то же самое.Этот пример кода должен сделать это:

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:если в обратном вызове возникает ошибка , веб-сервер не может сообщить клиенту об этой ошибке.Все, что вы можете сделать, это распространить исключение, которое заставит ASP.NET преждевременно закрыть соединение, поэтому клиенты получат сообщение об ошибке «соединение неожиданно закрыто» или «загрузка прервана».Это связано с тем, что HTTP отправляет код ошибки first в заголовке, прежде чем тело начнет потоковую передачу.

3 голосов
/ 05 июня 2019

Если создание документа занимает 2+ минут, оно должно быть asynchronous. Это может быть так:

  1. клиент отправляет запрос на генерацию документа
  2. вы принимаете запрос, запускаете генерацию в фоновом режиме и отвечаете сообщением типа generation has been started, we will notify you
  3. на клиенте вы периодически проверяете, готов ли документ и, наконец, получаете ссылку

Вы также можете сделать это с signalr . Шаги такие же, но клиенту не нужно проверять статус документа. Вы можете нажать на ссылку, когда документ будет завершен.

...