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

我一直在努力从ASP.NET核心操作中获取Response.Body属性,而我能够识别的唯一解决方案似乎不是最佳的。 该解决方案需要将Response.BodyMemoryStream交换,同时将流读入字符串变量,然后在发送到客户端之前将其交换回来。 在下面的示例中,我试图在自定义中间件类中获取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; } } 

在我最初的回答中,我完全误读了这个问题,并认为海报是在询问如何阅读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; 

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

更新的答案

对不起我原本误读了你的问题。 将关联流升级为缓冲流的概念仍然适用。 但是你必须手动完成它,我不知道任何内置的.Net Corefunction,它允许你以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; } } 

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

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

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

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

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

警告

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

警告

调用write方法后不要调用next.Invoke 。 中间件组件要么产生响应,要么调用next.Invoke ,但不是两者。

来自aspnet / BasicMiddleware Github repo的内置基本中间件示例

ResponseCompressionMiddleware.cs

 ///  /// Invoke the middleware. ///  ///  ///  public async Task Invoke(HttpContext context) { if (!_provider.CheckRequestAcceptsCompression(context)) { await _next(context); return; } var bodyStream = context.Response.Body; var originalBufferFeature = context.Features.Get(); var originalSendFileFeature = context.Features.Get(); var bodyWrapperStream = new BodyWrapperStream(context, bodyStream, _provider, originalBufferFeature, originalSendFileFeature); context.Response.Body = bodyWrapperStream; context.Features.Set(bodyWrapperStream); if (originalSendFileFeature != null) { context.Features.Set(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); } } } 

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

然而,由于以下事实增加了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(); _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)的日志客户端库并注入可循环内存流的实例。