我一直在努力从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;
}
}
}
尝试使用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;
}
}
发布于 2017-04-14 03:23:08
在我最初的回答中,我完全误解了这个问题,认为海报是在问如何阅读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 Core2.1开始,Request.EnableBuffering()
是可用的,它可以像Request.EnableRewind()
一样将Request.Body
升级为FileBufferingReadStream
,而且由于Request.EnableBuffering()
位于公共名称空间而不是内部名称空间中,所以它应该比EnableRewind()更好。(感谢@ArjanEinbu指出)
然后,要读取身体流,可以这样做:
string bodyContent = new StreamReader(Request.Body).ReadToEnd();
但是,不要将StreamReader
创建封装在using语句中,否则它将在using块结束时关闭底层的body流,而请求生命周期后期的代码将无法读取主体。
同样,为了安全起见,最好遵循上面的代码行,用这一行代码读取主体内容,将主体的流位置重新设置为0。
request.Body.Position = 0;
这样,请求生命周期后面的任何代码都会发现request.Body处于一个状态,就像它还没有被读取一样。
最新答案
对不起,我一开始误解了你的问题。将关联流升级为缓冲流的概念仍然适用。但是,您确实必须手动完成该操作,我不知道.Net核心功能中有任何内置功能,这些功能允许您读取响应流,其方式是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;
}
}
}
发布于 2018-09-14 08:42:45
您可以在请求管道中使用中间件,以便记录请求和响应。
但是,由于以下因素,增加了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;
}
}
注意:LOH的危险并没有完全消除,因为textWriter.ToString()
,另一方面,您可以使用支持结构化日志记录的日志客户端库(即。并注入可回收内存流的实例。
发布于 2021-12-24 23:42:57
.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();
https://stackoverflow.com/questions/43403941
复制相似问题