web-dev-qa-db-fra.com

Comment lire ASP.NET Core Response.Body?

J'ai eu du mal à obtenir le Response.Body La propriété _ d'une action ASP.NET Core et la seule solution que j'ai pu identifier semble sous-optimale. La solution nécessite d’échanger Response.Body avec MemoryStream lors de la lecture du flux dans une variable chaîne, puis de le permuter avant de l'envoyer au client. Dans les exemples ci-dessous, j'essaie d'obtenir le Response.Body valeur dans une classe de middleware personnalisé. Response.Body est une propriété définie uniquement dans ASP.NET Core pour une raison quelconque? Est-ce que je manque juste quelque chose ici, ou est-ce un problème de surveillance/bug/conception? Y a-t-il une meilleure façon de lire Response.Body?

Solution actuelle (sous-optimale):

public class MyMiddleWare
{
    private readonly RequestDelegate _next;

    public MyMiddleWare(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        using (var swapStream = new MemoryStream())
        {
            var originalResponseBody = context.Response.Body;

            context.Response.Body = swapStream;

            await _next(context);

            swapStream.Seek(0, SeekOrigin.Begin);
            string responseBody = new StreamReader(swapStream).ReadToEnd();
            swapStream.Seek(0, SeekOrigin.Begin);

            await swapStream .CopyToAsync(originalResponseBody);
            context.Response.Body = originalResponseBody;
        }
    }
}  

Solution tentée avec EnableRewind (): Cela ne fonctionne que pour Request.Body, ne pas Response.Body. Cela se traduit par la lecture d'une chaîne vide de Response.Body plutôt que le contenu réel du corps de la réponse.

Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime appLifeTime)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    app.Use(async (context, next) => {
        context.Request.EnableRewind();
        await next();
    });

    app.UseMyMiddleWare();

    app.UseMvc();

    // Dispose of Autofac container on application stop
    appLifeTime.ApplicationStopped.Register(() => this.ApplicationContainer.Dispose());
}

MyMiddleWare.cs

public class MyMiddleWare
{
    private readonly RequestDelegate _next;

    public MyMiddleWare(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        await _next(context);
        string responseBody = new StreamReader(context.Request.Body).ReadToEnd(); //responseBody is ""
        context.Request.Body.Position = 0;
    }
}  
47
JTW

Dans ma réponse initiale, j'avais totalement mal interprété la question et je pensais que l'affiche demandait comment lire le Request.Body. Mais il avait demandé comment lire le Response.Body. Je laisse ma réponse originale pour préserver l’historique, mais je le mets également à jour pour montrer comment je répondrais à la question une fois que je l’aurais lue correctement.

Réponse originale

Si vous voulez un flux en mémoire tampon prenant en charge la lecture à plusieurs reprises, vous devez définir

   context.Request.EnableRewind()

Idéalement, faites cela tôt dans le middleware avant que quoi que ce soit ait besoin de lire le corps.

Ainsi, par exemple, vous pouvez placer le code suivant au début de la méthode Configure du fichier Startup.cs:

        app.Use(async (context, next) => {
            context.Request.EnableRewind();
            await next();
        });

Avant d'activer Rembobiner, le flux associé à Request.Body Est un flux uniquement qui ne prend pas en charge la recherche ou la lecture du flux une seconde fois. Ceci a été fait pour rendre la configuration par défaut de la gestion des demandes aussi légère et performante que possible. Mais une fois que vous activez le rembobinage, le flux est mis à niveau vers un flux qui prend en charge la recherche et la lecture plusieurs fois. Vous pouvez observer cette "mise à niveau" en définissant un point d'arrêt juste avant et juste après l'appel à EnableRewind et en observant les propriétés Request.Body. Ainsi, par exemple, Request.Body.CanSeek Passera de false à true.

update : à partir de ASP.NET Core 2.1, Request.EnableBuffering() est disponible, ce qui met à niveau le Request.Body en FileBufferingReadStream tout comme Request.EnableRewind() et puisque Request.EnableBuffering() se trouve dans un espace de noms public plutôt que dans un espace de noms interne, il est préférable de le remplacer par EnableRewind (). (Merci à @ArjanEinbu pour l'avoir signalé)

Ensuite, pour lire le flux de corps, vous pouvez par exemple faire ceci:

   string bodyContent = new StreamReader(Request.Body).ReadToEnd();

N'emballez pas la création StreamReader dans une instruction using, sinon le flux de corps sous-jacent sera fermé à la conclusion du bloc using et le code plus tard dans le cycle de vie de la demande ne pourra pas lire le corps.

Pour être sûr également, il peut être judicieux de suivre la ligne de code ci-dessus qui lit le contenu du corps avec cette ligne de code pour réinitialiser la position de flux du corps à 0.

request.Body.Position = 0;

Ainsi, tout code figurant ultérieurement dans le cycle de vie de la requête trouvera la requête. Corps dans un état identique à celui où il n'a pas encore été lu.

Réponse mise à jour

Désolé, j'ai mal interprété votre question. Le concept de mise à niveau du flux associé en tant que flux mis en mémoire tampon s'applique toujours. Cependant, vous devez le faire manuellement. Je ne suis au courant d'aucune fonctionnalité .Net Core intégrée qui vous permet de lire le flux de réponses une fois écrit de la manière dont EnableRewind() permet à un développeur de relire le flux de demandes été lu.

Votre approche "hacky" est probablement tout à fait appropriée. Vous convertissez un flux qui ne peut pas chercher en un flux qui le peut. À la fin de la journée, le flux Response.Body Doit être échangé avec un flux mis en mémoire tampon et prenant en charge la recherche. Voici une autre version du middleware, mais vous remarquerez que cela ressemble beaucoup à votre approche. J'ai cependant choisi d'utiliser un bloc finally comme protection supplémentaire pour remettre le flux d'origine sur le Response.Body Et j'ai utilisé la propriété Position du flux plutôt que la méthode Seek. puisque la syntaxe est un peu plus simple mais l’effet n’est pas différent de votre approche.

public class ResponseRewindMiddleware {
        private readonly RequestDelegate next;

        public ResponseRewindMiddleware(RequestDelegate next) {
            this.next = next;
        }

        public async Task Invoke(HttpContext context) {

            Stream originalBody = context.Response.Body;

            try {
                using (var memStream = new MemoryStream()) {
                    context.Response.Body = memStream;

                    await next(context);

                    memStream.Position = 0;
                    string responseBody = new StreamReader(memStream).ReadToEnd();

                    memStream.Position = 0;
                    await memStream.CopyToAsync(originalBody);
                }

            } finally {
                context.Response.Body = originalBody;
            }

        } 
63
Ron C

Ce que vous décrivez comme un piratage est en fait l'approche suggérée pour gérer les flux de réponses dans un middleware personnalisé.

En raison de la nature pipeline de la conception de middleware, chaque middleware ignore le gestionnaire précédent ou suivant du pipeline. Il n'y a aucune garantie que le middleware actuel soit celui qui écrit la réponse, sauf s'il conserve le flux de réponse qui lui a été donné avant de transmettre un flux qu'il contrôle (le middleware actuel). Cette conception a été vue dans OWIN et finalement intégrée dans asp.net-core.

Une fois que vous commencez à écrire dans le flux de réponses, il envoie le corps et les en-têtes (la réponse) au client. Si un autre gestionnaire sur le pipeline le fait avant le gestionnaire actuel, il ne pourra rien ajouter à la réponse une fois que celle-ci aura déjà été envoyée.

Ce qui, encore une fois, n’est pas garanti pour être le flux de réponse réel si le middleware précédent dans le pipeline a suivi la même stratégie consistant à transmettre un autre flux sur la ligne.

Référencement Principes de base du middleware ASP.NET Core

Avertissement

Soyez prudent lors de la modification de HttpResponse après avoir appelé next, car la réponse a peut-être déjà été envoyée au client. Vous pouvez utiliser HttpResponse.HasStarted pour vérifier si les en-têtes ont été envoyés.

Avertissement

Ne pas appeler next.Invoke après avoir appelé une méthode write. Un composant middleware produit une réponse ou appelle next.Invoke, mais pas les deux.

Exemple de middlewares de base intégrés de aspnet/BasicMiddleware repo Github

ResponseCompressionMiddleware.cs

/// <summary>
/// Invoke the middleware.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task Invoke(HttpContext context)
{
    if (!_provider.CheckRequestAcceptsCompression(context))
    {
        await _next(context);
        return;
    }

    var bodyStream = context.Response.Body;
    var originalBufferFeature = context.Features.Get<IHttpBufferingFeature>();
    var originalSendFileFeature = context.Features.Get<IHttpSendFileFeature>();

    var bodyWrapperStream = new BodyWrapperStream(context, bodyStream, _provider,
        originalBufferFeature, originalSendFileFeature);
    context.Response.Body = bodyWrapperStream;
    context.Features.Set<IHttpBufferingFeature>(bodyWrapperStream);
    if (originalSendFileFeature != null)
    {
        context.Features.Set<IHttpSendFileFeature>(bodyWrapperStream);
    }

    try
    {
        await _next(context);
        // This is not disposed via a using statement because we don't want to flush the compression buffer for unhandled exceptions,
        // that may cause secondary exceptions.
        bodyWrapperStream.Dispose();
    }
    finally
    {
        context.Response.Body = bodyStream;
        context.Features.Set(originalBufferFeature);
        if (originalSendFileFeature != null)
        {
            context.Features.Set(originalSendFileFeature);
        }
    }
}
8
Nkosi

Vous pouvez utiliser un middleware dans le pipeline de demandes afin de consigner les demandes et les réponses.

Cependant, le risque de memory leak Est accru, en raison du fait que: 1. Les flux, 2. La définition de la mémoire tampon d'octets et 3. la conversion de chaînes.

peut se terminer jusqu'à Large Object Heap (si le corps de la demande ou de la réponse dépasse 85 000 octets). Cela augmente le risque de fuite de mémoire dans votre application. Afin d'éviter LOH, les flux de mémoire peuvent être remplacés par flux de mémoire recyclable à l'aide de la bibliothèque correspondante.

Une implémentation qui utilise des flux de mémoire recyclables:

public class RequestResponseLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;
    private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
    private const int ReadChunkBufferLength = 4096;

    public RequestResponseLoggingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
    {
        _next = next;
        _logger = loggerFactory
            .CreateLogger<RequestResponseLoggingMiddleware>();
        _recyclableMemoryStreamManager = new RecyclableMemoryStreamManager();
    }

    public async Task Invoke(HttpContext context)
    {
        LogRequest(context.Request);
        await LogResponseAsync(context);
    }

    private void LogRequest(HttpRequest request)
    {
        request.EnableRewind();
        using (var requestStream = _recyclableMemoryStreamManager.GetStream())
        {
            request.Body.CopyTo(requestStream);
            _logger.LogInformation($"Http Request Information:{Environment.NewLine}" +
                                   $"Schema:{request.Scheme} " +
                                   $"Host: {request.Host} " +
                                   $"Path: {request.Path} " +
                                   $"QueryString: {request.QueryString} " +
                                   $"Request Body: {ReadStreamInChunks(requestStream)}");
        }
    }

    private async Task LogResponseAsync(HttpContext context)
    {
        var originalBody = context.Response.Body;
        using (var responseStream = _recyclableMemoryStreamManager.GetStream())
        {
            context.Response.Body = responseStream;
            await _next.Invoke(context);
            await responseStream.CopyToAsync(originalBody);
            _logger.LogInformation($"Http Response Information:{Environment.NewLine}" +
                                   $"Schema:{context.Request.Scheme} " +
                                   $"Host: {context.Request.Host} " +
                                   $"Path: {context.Request.Path} " +
                                   $"QueryString: {context.Request.QueryString} " +
                                   $"Response Body: {ReadStreamInChunks(responseStream)}");
        }

        context.Response.Body = originalBody;
    }

    private static string ReadStreamInChunks(Stream stream)
    {
        stream.Seek(0, SeekOrigin.Begin);
        string result;
        using (var textWriter = new StringWriter())
        using (var reader = new StreamReader(stream))
        {
            var readChunk = new char[ReadChunkBufferLength];
            int readChunkLength;
            //do while: is useful for the last iteration in case readChunkLength < chunkLength
            do
            {
                readChunkLength = reader.ReadBlock(readChunk, 0, ReadChunkBufferLength);
                textWriter.Write(readChunk, 0, readChunkLength);
            } while (readChunkLength > 0);

            result = textWriter.ToString();
        }

        return result;
    }
}

NB Le risque de LOH n’est pas totalement éliminé en raison de textWriter.ToString(). Par ailleurs, vous pouvez utiliser une bibliothèque cliente de journalisation prenant en charge la journalisation structurée (par exemple, Serilog) et injecter l’instance d’un flux de mémoire recyclable.

4
George Kargakis