Как читать ASP.NET Core Response.Body?

Я изо всех сил пытался получить свойство Response.Body из действия ASP.NET Core, и единственное решение, которое я смог определить, кажется неоптимальным. Решение требует замены Response.Body на MemoryStream при чтении потока в строковую переменную, а затем обменять его перед отправкой клиенту. В приведенных ниже примерах я пытаюсь получить значение Response.Body в пользовательском классе промежуточного программного обеспечения. Response.Body это свойство только для набора в ASP.NET Core по какой-то причине? Я просто что-то здесь упускаю, или это проблема надзора/ошибки/дизайна? Есть ли лучший способ читать Response.Body?

Текущее (неоптимальное) решение:

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

Попытка решения с использованием EnableRewind(): Это работает только для Request.Body, но не для Response.Body. В результате получается чтение пустой строки из Response.Body, а не фактического содержимого тела ответа.

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

Ответ 1

В своем первоначальном ответе я совершенно неправильно понял вопрос и подумал, что на плакате спрашивается, как читать Request.Body. Но он спросил, как читать Response.Body. Я оставляю свой первоначальный ответ, чтобы сохранить историю, но также обновляю его, чтобы показать, как я отвечу на вопрос, прочитав его правильно.

Оригинальный ответ

Если вы хотите буферизованный поток, который поддерживает чтение несколько раз, вам нужно установить

   context.Request.EnableRewind()

В идеале сделайте это на ранних этапах промежуточного программного обеспечения, прежде чем что-либо понадобится для чтения тела.

Например, вы можете поместить следующий код в начало метода Configure файла Startup.cs:

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

До включения перемотки назад поток, связанный с Request.Body, является потоком только для прямой передачи, который не поддерживает поиск или чтение потока во второй раз. Это было сделано, чтобы сделать конфигурацию обработки запросов по умолчанию максимально простой и производительной. Но после включения перемотки потока выполняется обновление до потока, который поддерживает поиск и чтение несколько раз. Вы можете наблюдать это "обновление", установив точку останова непосредственно перед и сразу после вызова EnableRewind и наблюдая свойства Request.Body. Так, например, Request.Body.CanSeek изменится с false на true.

обновление: начиная с ASP.NET Core 2.1, доступен Request.EnableBuffering(), который обновляет Request.Body до FileBufferingReadStream так же, как Request.EnableRewind(), и поскольку Request.EnableBuffering() находится в общедоступном пространстве имен, а не во внутреннем, его следует отдать предпочтение перед EnableRewind(). (Спасибо @ArjanEinbu за указание)

Затем, чтобы прочитать основной поток, вы можете, например, сделать это:

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

Не включайте создание StreamReader в оператор using, иначе оно закроет основной поток тела после завершения использования блока, и код позже в жизненном цикле запроса не сможет прочитать тело.

Также, чтобы быть в безопасности, было бы неплохо следовать приведенной выше строке кода, которая считывает содержимое тела с этой строкой кода, чтобы сбросить позицию потока тела обратно в 0.

request.Body.Position = 0;

Таким образом, любой код позже в жизненном цикле запроса найдет запрос. Он находится в состоянии, как будто он еще не был прочитан.

Обновленный ответ

Извините, я изначально неправильно понял ваш вопрос. Концепция обновления ассоциированного потока до буферизованного потока все еще применима. Однако вам придется делать это вручную, я не знаю ни о какой встроенной функциональности .Net Core, которая позволяет вам читать поток ответов, когда он был написан так, что EnableRewind() позволяет разработчику перечитать поток запроса после его чтения.

Ваш "хакерский" подход, вероятно, полностью уместен. Вы в основном конвертируете поток, который не может искать, в поток, который может. В конце дня поток Response.Body должен быть заменен потоком, который буферизован и поддерживает поиск. Вот еще один пример использования промежуточного программного обеспечения, но вы заметите, что он очень похож на ваш подход. Однако я решил использовать блок finally в качестве дополнительной защиты для возврата исходного потока на Response.Body, и я использовал свойство потока Position, а не метод Seek, поскольку синтаксис немного проще, но Эффект ничем не отличается от вашего подхода.

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

        } 

Ответ 2

То, что вы описываете как хак, на самом деле является предложенным подходом к управлению потоками ответов в пользовательском промежуточном программном обеспечении.

Из-за конвейерной природы проекта промежуточного программного обеспечения, где каждое промежуточное программное обеспечение не знает о предыдущем или следующем обработчике в конвейере. Нет гарантии, что текущее промежуточное программное обеспечение будет тем, которое записывает ответ, если только оно не удерживает поток ответа, который был передан перед передачей потока, которым он (текущее промежуточное программное обеспечение) управляет. Этот дизайн был замечен в OWIN и в конечном итоге встроен в asp.net-ядро.

Как только вы начинаете запись в поток ответов, он отправляет тело и заголовки (ответ) клиенту. Если другой обработчик в конвейере сделает это до того, как текущий обработчик сможет это сделать, он не сможет добавить что-либо к ответу после его отправки.

Что, опять же, не гарантируется как фактический поток ответов, если предыдущее промежуточное ПО в конвейере следовало той же стратегии передачи другого потока по линии.

Ссылка Основные принципы промежуточного программного обеспечения ASP.NET

Warning

Будьте осторожны, изменяя HttpResponse после вызова next, потому что ответ, возможно, уже отправлен клиенту. Вы можете использовать HttpResponse.HasStarted, чтобы проверить, были ли отправлены заголовки.

Warning

Не вызывайте next.Invoke после вызова метода write. Промежуточное программное обеспечение компонент либо выдает ответ, либо вызывает next.Invoke, но не и другие.

Пример встроенного базового промежуточного программного обеспечения из aspnet/BasicMiddleware Github repo

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

Ответ 3

Вы можете использовать промежуточное ПО в конвейере запросов, для регистрации запросов и ответов.

Однако повышается опасность memory leak из-за того, что: 1. Потоки, 2. Установка байтовых буферов и 3. Строковые преобразования

может заканчиваться кучей больших объектов (в случае, если тело запроса или ответа превышает 85 000 байт). Это увеличивает опасность утечки памяти в вашем приложении.  Чтобы избежать LOH, потоки памяти можно заменить на поток вторичной памяти, используя соответствующую библиотеку.

Реализация, в которой используются повторно используемые потоки памяти:

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. Опасность LOH не полностью устранена из-за textWriter.ToString(), с другой стороны, вы можете использовать клиентскую библиотеку журналирования, которая поддерживает структурированное журналирование (т.е. Serilog) и внедрить экземпляр потока вторичной памяти.