我一直在努力从ASP.NET Core操作中获取 Response.Body
属性,而我能够识别的唯一解决方案似乎不是最佳的 . 该解决方案需要将 MemoryStream
与 MemoryStream
交换,同时将流读入字符串变量,然后在发送到客户端之前将其交换回来 . 在下面的示例中,我试图在自定义中间件类中获取 Response.Body
值 . 由于某些原因, Response.Body
是ASP.NET Core中的一个仅设置属性?我在这里遗漏了什么,或者这是一个疏忽/错误/设计问题?有没有更好的方法来阅读 Response.Body
?
Current (sub-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;
}
}
}
Attempted solution using 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;
}
}
3 回答
在我最初的回答中,我完全误读了这个问题,并认为海报在询问如何阅读
Request.Body
但他曾问过如何阅读Response.Body
. 我将离开原来的答案以保存历史记录,但也会更新它以显示如果正确阅读它我将如何回答这个问题 .Original Answer
如果您想要一个支持多次读取的缓冲流,则需要进行设置
理想情况下,在需要读取正文的任何内容之前,尽早在中间件中执行此操作 .
例如,您可以将以下代码放在Startup.cs文件的
Configure
方法的开头:在启用Rewind之前,与
Request.Body
关联的流是仅向前流,不支持第二次搜索或读取流 . 这样做是为了使请求处理的默认配置尽可能轻量级和高性能 . 但是一旦启用了倒带,流就会升级到支持多次搜索和读取的流 . 您可以通过在调用EnableRewind
之前和之后设置断点并观察Request.Body
属性来观察此"upgrade" . 因此,例如Request.Body.CanSeek
将从false
更改为true
.更新:从ASP.NET Core 2.1
Request.EnableBuffering()
开始,它将Request.Body
升级为FileBufferingReadStream
,就像Request.EnableRewind()
一样,并且由于Request.EnableBuffering()
位于公共命名空间而不是内部命名空间,因此应优先于EnableRewind() . (感谢@ArjanEinbu指出)然后,你可以读取身体流,例如这样做:
不要将
StreamReader
创建包装在using语句中,否则它将在使用块结束时关闭底层正文流,并且稍后在请求生命周期中的代码将无法读取正文 .另外,为了安全起见,最好遵循上面的代码行,用这行代码读取正文内容,将正文的流位置重置为0 .
这样,请求生命周期中稍后的任何代码都会找到request.Body处于尚未读取的状态 .
Updated Answer
对不起我原本误读了你的问题 . 将关联流升级为缓冲流的概念仍然适用 . 但是你必须手动完成它,我不知道任何内置的.Net Core功能,它允许你以
EnableRewind()
允许开发人员在读取请求流后重新读取请求流的方式读取响应流 .您的"hacky"方法可能完全合适 . 您基本上是将无法寻找的流转换为可以的流 . 在一天结束时,
Response.Body
流必须与缓冲的流交换出来并支持搜索 . 这是中间件的另一种做法,但你会注意到它与你的方法非常相似 . 然而,我确实选择使用finally块作为添加保护,将原始流放回Response.Body
并使用流的Position
属性而不是Seek
方法,因为语法稍微简单但效果与您没有区别做法 .您所描述的黑客实际上是如何管理自定义中间件中的响应流的建议方法 .
由于中间件设计的管道特性,每个中间件都不知道管道中的前一个或下一个处理器 . 无法保证当前的中间件是编写响应的中间件,除非它保留在传递它(当前中间件)控制的流之前给出的响应流 . 这种设计在OWIN中被看到并最终被融入到asp.net-core中 .
一旦开始写入响应流,它就会将正文和 Headers (响应)发送给客户端 . 如果管道下的另一个处理程序在当前处理程序有机会之前执行该操作,那么一旦它已经被发送,它将无法向响应添加任何内容 .
如果管道中的先前中间件遵循相同的传递策略,那么再次不能保证是实际的响应流另一条线下线 .
引用ASP.NET Core Middleware Fundamentals
来自 aspnet/BasicMiddleware Github repo的内置基本中间件示例
ResponseCompressionMiddleware.cs
您可以在请求管道中使用middleware,以便记录请求和响应 .
然而,由于以下事实增加了
memory leak
的危险:1 . 流,2 . 设置字节缓冲区和3.字符串转换最终可以Large Object Heap(如果请求或响应的主体大于85,000字节) . 这会增加应用程序中内存泄漏的危险 . 为了避免LOH,可以使用相关的library将内存流替换为Recyclable Memory stream .
使用可循环内存流的实现:
NB . 由于
textWriter.ToString()
,LOH的危害并未完全消除,另一方面,您可以使用支持结构化日志记录(即Serilog)的日志客户端库并注入可循环内存流的实例 .