如何阅读ASP.NET Core Response.Body?

时间:2017-04-14 01:18:07

标签: c# asp.net-core asp.net-core-mvc asp.net-core-middleware

我一直在努力从ASP.NET Core操作中获取Response.Body属性,而我能够识别的唯一解决方案似乎不是最佳的。该解决方案需要将Response.BodyMemoryStream交换,同时将流读入字符串变量,然后在发送到客户端之前将其交换回来。在下面的示例中,我尝试在自定义中间件类中获取Response.Body值。出于某种原因,Response.Body是ASP.NET Core中的 set 唯一属性?我在这里遗漏了什么,或者这是一个疏忽/错误/设计问题?有没有更好的方式来阅读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;
    }
}  

4 个答案:

答案 0 :(得分:49)

在我最初的回答中,我完全误解了这个问题,并认为海报在询问如何阅读Request.Body但是他曾问过如何阅读Response.Body。我保留原始答案以保存历史记录,但也会对其进行更新以显示如果正确阅读该问题后我将如何回答这个问题。

原始答案

如果您想要一个支持多次阅读的缓冲流,则需要设置

   context.Request.EnableRewind()

理想情况下,在需要阅读正文之前,请先在中间件中进行此操作。

例如,您可以将以下代码放在Startup.cs文件的Configure方法的开头:

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

在启用Rewind之前,与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;

这样,请求生命周期后期的任何代码都会找到请求.Body处于尚未被读取的状态。

更新了答案

抱歉,我原来误读了你的问题。将关联流升级为缓冲流的概念仍然适用。但是你必须手动完成,我不知道任何内置的.Net Core功能,它允许你以EnableRewind()允许开发人员重新读取请求流之后的方式读取响应流。 #39;已被阅读。

你的" hacky"方法可能完全合适。您基本上是将无法搜索的流转换为可以使用的流。在一天结束时,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;
            }

        } 

答案 1 :(得分:5)

您所描述的黑客实际上是如何在自定义中间件中管理响应流的建议方法。

由于中间件设计的管道特性,每个中间件都不知道管道中的前一个或下一个处理程序。无法保证当前的中间件是编写响应的中间件,除非它在传递它(当前中间件)控制的流之前保留它所给出的响应流。这种设计在OWIN中被看到并最终被加入到asp.net-core中。

一旦开始写入响应流,它就会将正文和标题(响应)发送给客户端。如果管道中的另一个处理程序在当前处理程序有机会之前执行该操作,那么一旦它已经被发送,它将无法向响应添加任何内容。

如果管道中的先前中间件遵循相同的策略将另一个流传递到该行,那么再次不能保证是实际的响应流。

引用ASP.NET Core Middleware Fundamentals

  

警告

     

在调用HttpResponse后小心修改next,因为   响应可能已经发送给客户端。您可以使用   HttpResponse.HasStarted检查标头是否已发送。

     

警告

     

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

答案 2 :(得分:3)

您可以使用middleware 在请求管道中,以便记录请求和响应。

由于以下事实,memory leak的危险增加了: 1.串流 2.设置字节缓冲区和 3.字符串转换

最多可以达到Large Object Heap(如果请求或响应的正文大于85,000字节)。这增加了应用程序中内存泄漏的危险。  为了避免发生LOH,可以使用相关的Recyclable Memory streamlibrary替换内存流。

使用可回收内存流的实现:

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。另一方面,由于textWriter.ToString(),无法完全消除LOH的危险,您可以使用支持结构化日志记录(例如Serilog)的日志记录客户端库并注入可回收内存流的实例。

答案 3 :(得分:3)

在ASP.NET Core 3中,情况甚至更糟:即使您忽略了一个事实,我们谈论的是一个Web框架,该框架已将读取Web请求等基本内容变成了不直观的解决方法和一个API的难题。会在每个版本之间进行更改,然后出现an open issue,这意味着如果您使用EnableBuffering“太晚”(包括在中间件管道中太晚),它将什么也不会做。

就我而言,我使用了一种骇人听闻的解决方案,即尽可能早地在管道中将主体添加到HttpContext.Items中。我敢肯定这是非常低效的,并且可以忽略身体较大时出现的那种问题,但是如果您正在寻找现成的东西(就像我遇到这个问题时一样),那么也许这很有帮助。

具体来说,我使用以下中间件:

    public class RequestBodyStoringMiddleware
    {
        private readonly RequestDelegate _next;

        public RequestBodyStoringMiddleware(RequestDelegate next) =>
            _next = next;

        public async Task Invoke(HttpContext httpContext)
        {
            httpContext.Request.EnableBuffering();
            string body;
            using (var streamReader = new System.IO.StreamReader(
                httpContext.Request.Body, System.Text.Encoding.UTF8, leaveOpen: true))
                body = await streamReader.ReadToEndAsync();

            httpContext.Request.Body.Position = 0;

            httpContext.Items["body"] = body;
            await _next(httpContext);
        }
    }

要使用此功能,请尽早在app.UseMiddleware<RequestBodyStoringMiddleware>();中执行Startup.Configure ;问题在于,取决于您在做什么,身体流最终可能会一路被消耗掉,因此顺序很重要。然后,当您以后需要主体时(在控制器或其他中间件中),请通过(string)HttpContext.Items["body"];访问它。是的,您的控制器现在依赖于配置的实现细节,但是您可以做什么。