5 votes

Streaming d'un fichier généré en mémoire dans ASP.NET Core

Après avoir parcouru l'internet pendant des heures, je ne sais plus comment résoudre mon problème pour ASP.NET Core 2.x.

Je génère un CSV à la volée (ce qui peut prendre plusieurs minutes) et j'essaie ensuite de le renvoyer au client. De nombreux clients se coupent avant que je ne commence à envoyer une réponse. J'essaie donc de leur renvoyer le fichier en streaming (avec une réponse 200 immédiate) et d'écrire dans le flux de manière asynchrone. Il semble que cela soit possible avec PushStreamContent précédemment en ASP, mais je ne sais pas comment structurer mon code pour que la génération du CSV se fasse de manière asynchrone et renvoie immédiatement une réponse 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"
    };
 }

Si vous faites une demande à cette méthode de contrôleur, vous n'obtiendrez rien jusqu'à ce que tout le CSV ait fini de se générer et que vous obteniez enfin une réponse, moment auquel la plupart des demandes des clients ont expiré.

J'ai essayé d'inclure le code de génération du CSV dans un fichier de type Task.Run() mais cela n'a pas aidé mon problème non plus.

10voto

Stephen Cleary Points 91731

Il n'y a pas de PushStreamContext type intégré à ASP.NET Core. Vous pouvez toutefois le faire, construisez le vôtre FileCallbackResult qui fait la même chose. Ce site exemple de code devrait le faire :

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);
        }
    }
}

Utilisation :

[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"
  };
}

N'oubliez pas que FileCallbackResult a les mêmes limites que PushStreamContext : que si une erreur se produit dans le rappel le serveur web ne dispose d'aucun moyen efficace pour notifier cette erreur au client. Tout ce que vous pouvez faire, c'est propager l'exception, ce qui amènera ASP.NET à fermer la connexion de manière anticipée, de sorte que les clients obtiennent une erreur "connexion fermée de manière inattendue" ou "téléchargement interrompu". En effet, HTTP envoie le code d'erreur premièrement dans l'en-tête, avant que le corps ne commence à être diffusé.

3voto

mtkachenko Points 1004

Si la génération de documents prend 2+ minutes, il devrait être asynchronous . Ça pourrait être comme ça :

  1. le client envoie une demande pour générer un document
  2. vous acceptez la demande, lancez la génération en arrière-plan et répondez avec un message du type generation has been started, we will notify you
  3. sur le client, vous vérifiez périodiquement si le document est prêt et obtenez finalement le lien.

Vous pouvez également le faire avec signalr . Les étapes sont les mêmes, mais il n'est pas nécessaire que le client vérifie le statut du document. Vous pouvez pousser le lien lorsque le document est terminé.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X