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

JTW*_*JTW 52 c# asp.net-core-mvc asp.net-core asp.net-core-middleware

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

尝试使用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());
}
Run Code Online (Sandbox Code Playgroud)

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;
    }
}  
Run Code Online (Sandbox Code Playgroud)

Ron*_*n C 67

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

原始答案

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

   context.Request.EnableRewind()
Run Code Online (Sandbox Code Playgroud)

理想情况下,在任何需要阅读正文之前,在中间件中尽早这样做.

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

        app.Use(async (context, next) => {
            context.Request.EnableRewind();
            await next();
        });
Run Code Online (Sandbox Code Playgroud)

在启用Rewind之前,与之关联的流Request.Body是仅前向流,不支持第二次搜索或读取流.这样做是为了使请求处理的默认配置尽可能轻量级和高性能.但是一旦启用了倒带,流就会升级到支持多次搜索和读取的流.您可以通过在调用EnableRewind和观察Request.Body属性之前和之后设置断点来观察此"升级" .因此,例如Request.Body.CanSeekfalse改为true.

更新:从ASP.NET Core 2.1开始,Request.EnableBuffering()它可以升级Request.BodyFileBufferingReadStream类似的Request.EnableRewind(),因为Request.EnableBuffering()它位于公共命名空间而不是内部命名空间,它应该优先于EnableRewind().(感谢@ArjanEinbu指出)

然后,你可以读取正文流,例如:

   string bodyContent = new StreamReader(Request.Body).ReadToEnd();
Run Code Online (Sandbox Code Playgroud)

不要将StreamReader创建包装在using语句中,否则它将在使用块结束时关闭底层正文流,并且稍后在请求生命周期中的代码将无法读取正文.

另外,为了安全起见,遵循上面的代码行可能是一个好主意,该代码行使用这行代码读取正文内容以将正文的流位置重置为0.

request.Body.Position = 0;
Run Code Online (Sandbox Code Playgroud)

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

更新的答案

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

你的"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;
            }

        } 
Run Code Online (Sandbox Code Playgroud)

  • 此外:我可以检查 context.Response.ContentLength 是否为空值和零值条件,并使用“await _next(context); return;”快捷方式。这会阻止我记录 null 或空响应,并在刷新页面时停止 304 异常。但这可能是一个不好的方法,如果它是错误的,请删除它。 (2认同)
  • @RonC:我找到了`context.Request.EnableBuffering()`.这是一个更正确/最新的解决方案吗? (2认同)

Rod*_*voi 19

.NET 6.0+ 解决方案

在 ASP.NET Core 6.0+ 中考虑使用内置扩展:

var builder = WebApplication.CreateBuilder(args);
//...
builder.Services.AddHttpLogging(options => // <--- Setup logging
{
    // Specify all that you need here:
    options.LoggingFields = HttpLoggingFields.RequestHeaders |
                            HttpLoggingFields.RequestBody |
                            HttpLoggingFields.ResponseHeaders |
                            HttpLoggingFields.ResponseBody;
});
//...
var app = builder.Build();
//...
app.UseHttpLogging(); // <--- Add logging to pipeline
//...
app.Run();
Run Code Online (Sandbox Code Playgroud)

  • 将相同的行程请求/响应正文记录在一起? (3认同)
  • 谢谢,但我想在中间件中一起记录请求/响应。 (2认同)

D.K*_*D.K 15

@RonC 的答案在很大程度上有效。但我想添加这个。ASP.NET Core 似乎不喜欢直接从内存流呈现 Web 内容(除非它是一个简单的字符串而不是整个 HTML 页面)。我花了几个小时试图解决这个问题,所以我想将其发布在这里,这样其他人就不会像我一样浪费时间来解决这个问题。

以下是@RonC 关于响应部分的答案的一点修改:

public class ResponseBufferMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task InvokeAsync(HttpContext context)
    {
        // Store the original body stream for restoring the response body back to its original stream
        var originalBodyStream = context.Response.Body;

        // Create new memory stream for reading the response; Response body streams are write-only, therefore memory stream is needed here to read
        await using var memoryStream = new MemoryStream();
        context.Response.Body = memoryStream;

        // Call the next middleware
        await _next(context);

        // Set stream pointer position to 0 before reading
        memoryStream.Seek(0, SeekOrigin.Begin);

        // Read the body from the stream
        var responseBodyText = await new StreamReader(memoryStream).ReadToEndAsync();

        // Reset the position to 0 after reading
        memoryStream.Seek(0, SeekOrigin.Begin);

        // Do this last, that way you can ensure that the end results end up in the response.
        // (This resulting response may come either from the redirected route or other special routes if you have any redirection/re-execution involved in the middleware.)
        // This is very necessary. ASP.NET doesn't seem to like presenting the contents from the memory stream.
        // Therefore, the original stream provided by the ASP.NET Core engine needs to be swapped back.
        // Then write back from the previous memory stream to this original stream.
        // (The content is written in the memory stream at this point; it's just that the ASP.NET engine refuses to present the contents from the memory stream.)
        context.Response.Body = originalBodyStream;
        await context.Response.Body.WriteAsync(memoryStream.ToArray());

        // Per @Necip Sunmaz's recommendation this also works:
        // Just make sure that the memoryStrream's pointer position is set back to 0 again.
        // await memoryStream.CopyToAsync(originalBodyStream);
        // context.Response.Body = originalBodyStream;
    }
}
Run Code Online (Sandbox Code Playgroud)

这样,您可以正确呈现网页内容,但也可以根据需要读取响应正文。这已经经过彻底测试。

另请注意,此代码是使用 .NET Core 3.1 和 C# 语言版本 8.0 编写的。 @DalmTo 确认此代码适用于 .NET 5 和 C# 9。

  • 也适用于 .Net 5 + C# 9。❤ (2认同)

Geo*_*kis 9

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

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

可以最终达到大型对象堆(如果请求或响应的主体大于 85,000 字节)。这增加了应用程序中内存泄漏的危险。为了避免 LOH,可以使用相关库将内存流替换为Recyclable Memory 流

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

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

注意。LOH 的危害并未完全消除,因为textWriter.ToString()另一方面您可以使用支持结构化日志记录的日志客户端库(即 Serilog)并注入可回收内存流的实例。

  • `await responseStream.CopyToAsync(originalBody);` 在我的情况下没有复制。我使用了`responseStream.WriteTo(originalBody);` (3认同)
  • 我想在您记录请求后立即需要`request.Body.Position = 0`。否则,您将获得空体异常。 (2认同)

Nko*_*osi 8

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

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

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

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

引用ASP.NET核心中间件基础知识

警告

小心修改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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)