105 votes

Comment lire ASP.NET Core Response.Body?

J'ai eu du mal à obtenir l' Response.Body de la propriété d'un ASP.NET de Base d'action et la seule solution que j'ai été en mesure d'identifier semble sous-optimale. La solution nécessite d'échanger Response.Body avec un MemoryStream lors de la lecture du flux dans une variable de chaîne, puis échange de revenir avant de les envoyer au client. Dans les exemples ci-dessous, je vais essayer d'obtenir l' Response.Body de la valeur personnalisé dans un middleware de classe. Response.Body est un jeu seule propriété ASP.NET de Base pour une raison quelconque? Suis-je tout simplement manque quelque chose ici, ou est-ce un oubli/bug/problème de conception? Est-il une meilleure façon de lire Response.Body?

Courant (sous-optimal) solution:

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

Tentative de solution à l'aide de EnableRewind(): Cela fonctionne uniquement pour Request.Body, pas Response.Body. Cette résultats dans la lecture d'une chaîne vide à partir d' Response.Body plutôt que sur le corps de la réponse de contenu.

De démarrage.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;
    }
}  

118voto

Ron C Points 8275

Dans ma réponse initiale, j'avais totalement mal interprété la question et de la pensée de l'affiche a été demandé comment lire l' Request.Body , Mais il avait demandé comment lire l' Response.Body. Je pars de mon original de répondre à préserver l'histoire, mais aussi de le mettre à jour pour montrer comment je pourrais répondre à la question, une fois la lecture correctement.

Réponse Originale À Cette Question

Si vous voulez un tampon ruisseau qui prend en charge la lecture plusieurs fois, vous devez définir le

   context.Request.EnableRewind()

Idéalement le faire au plus tôt dans le middleware avant tout besoin de lire le corps.

Ainsi, par exemple, vous pouvez placer le code suivant au début de l' Configure méthode de Démarrage.cs fichier:

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

Avant d'activer la Rembobiner le flux associé à l' Request.Body est un avant seulement de flux qui ne prend pas en charge la recherche ou de la lecture du flux d'une deuxième fois. Cela a été fait pour rendre la configuration par défaut de traitement de la demande le plus léger et performant que possible. Mais une fois que vous activez rembobiner le flux de données est mise à niveau vers un ruisseau qui prend en charge la recherche et la lecture à plusieurs reprises. Vous pouvez observer cette "mise à niveau" en mettant un point d'arrêt juste avant et juste après l'appel à l' EnableRewind , et en observant l' Request.Body propriétés. Ainsi, par exemple, Request.Body.CanSeek va changer à partir de false de true.

mise à jour: Départ en ASP.NET de Base 2.1 Request.EnableBuffering() est disponible mises à niveau de l' Request.Body d'un FileBufferingReadStream comme Request.EnableRewind() et depuis Request.EnableBuffering() est publique dans un espace de noms plutôt que de l'interne, il doit être préférée à l'EnableRewind(). (Merci à @ArjanEinbu pour préciser)

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

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

Ne pas envelopper l' StreamReader création dans une instruction d'utilisation ou si elle va fermer le sous-jacent flux de corps à la conclusion de l'aide du bloc de code et au plus tard dans le cycle de vie de demande ne sera pas en mesure de lire le corps.

Aussi juste pour être sûr, il pourrait être une bonne idée de suivre la ligne de code qui lit le contenu du corps avec cette ligne de code pour réinitialiser le corps du flux de position à 0.

request.Body.Position = 0;

De cette façon, le code plus tard dans le cycle de vie de demande trouverez la demande.Corps dans un état tout comme il n'a pas été encore lire.

Mise À Jour De Réponse

Désolé l'origine, j'avais mal lu votre question. Le concept de mise à jour le flux associé à un tampon de flux s'applique toujours. Cependant, vous ne devez le faire manuellement, je suis pas du tout intégré .Net à la fonctionnalité de Base qui vous permet de lire le flux de réponse une fois écrit dans la façon dont EnableRewind() permet à un développeur de relire la demande de cours d'eau après qu'il a été lu.

Votre "hacky" approche est de nature tout à fait convenable. Vous êtes essentiellement de la conversion d'un flux qui ne peut pas demander à celui qui peut. À la fin de la journée, l' Response.Body flux a pour obtenir échangé avec un flux qui est mis en mémoire tampon et prend en charge la recherche. Voici une autre de prendre sur un middleware pour le faire, mais vous remarquerez que c'est assez similaire à votre approche. Je n'ai cependant choisir d'utiliser un bloc finally comme protection supplémentaire pour mettre le flux d'origine de retour sur l' Response.Body et j'ai utilisé l' Position de la propriété de la rivière plutôt que de l' Seek méthode, car la syntaxe est un peu plus simple, mais l'effet n'est pas différente de celle 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;
            }

        } 

13voto

George Kargakis Points 968

Vous pouvez utiliser un middleware dans le pipeline de demande, afin de tracer les requêtes et les réponses.

Mais qui augmente le risque d' memory leak, en raison de la facth que: 1. Les ruisseaux, 2. Paramètre Octet de Tampons et de 3. La conversion en chaîne

peut mettre fin à des Objets Volumineux (dans le cas où le corps de la requête ou de la réponse est de plus de 85 000 octets). Cela augmente le risque de fuite de mémoire dans votre application. Afin d'éviter la liturgie des heures, la mémoire des cours d'eau peut être remplacée par Recyclables flux de Mémoire à l'aide de l'pertinentes de la bibliothèque.

Une mise en œuvre qui utilise Recyclables mémoire de flux:

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 danger de LOH est pas d'éradiquer complètement en raison d' textWriter.ToString() d'autre part, vous pouvez utiliser un enregistrement de la bibliothèque du client qui prend en charge structurée d'enregistrement (ie. Serilog) et injecter de l'instance de Recyclables de Flux de Mémoire.

8voto

Nkosi Points 95895

Ce que vous décrivez comme un hack est en fait l'approche proposée de la façon de gérer les flux de réponse dans la coutume de middleware.

En raison de la canalisation de la nature du milieu de la vaisselle design, où le milieu de chaque ware est pas au courant de la précédente ou suivante gestionnaire dans le pipeline. Il n'y a aucune garantie que l'actuel moyen-vaisselle serait celui de l'écriture de la réponse à moins qu'il détient sur le flux de réponse, il a été donné avant de passer sur un flux, qu'il (l'actuel moyen-vaisselle) des contrôles. Cette conception a été vu dans OWIN et, éventuellement, cuit au four dans des asp.net-core.

Une fois que vous commencer à écrire pour le flux de réponse, il envoie le corps et les en-têtes (la réponse) pour le client. Si un autre gestionnaire dans le pipeline n'est que avant l'actuel gestionnaire eu la chance de alors il ne sera pas en mesure d'ajouter quelque chose à la réponse une fois qu'il a été déjà envoyée.

Ce qui est nouveau n'est pas garanti que le flux de réponse si la précédente middleware dans le pipeline a suivi la même stratégie de passage d'un autre ruisseau en bas de la ligne.

Référencement ASP.NET Core Middleware Fondamentaux

Avertissement

Attention la modification de l' HttpResponse après avoir invoqué next, parce que la réponse 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 téléphoner next.Invoke après l'appel à un write méthode. Un middleware composant du produit une réponse ou des appels next.Invoke, mais pas les deux.

Exemple de construction de base, les middlewares de aspnet/BasicMiddleware dépôt 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);
        }
    }
}

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