实现打印请求参数和响应结果的中间件,本以为比较容易,但是花了不少时间。
正确的代码:
public class LogginMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
public LogginMiddleware(RequestDelegate next, ILogger<LogginMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task Invoke(HttpContext httpContext)
{
var req = httpContext.Request;
req.EnableBuffering();
using (StreamReader requestReader = new StreamReader(req.Body, Encoding.UTF8))
{
//log request
var bodyStr = await requestReader.ReadToEndAsync();
req.Body.Position = 0;
_logger.LogInformation("Url:[{url}] ", httpContext.Request.GetDisplayUrl());
if (!string.IsNullOrEmpty(bodyStr))
{
_logger.LogInformation("Body:[{request}]", bodyStr);
}
using (var buffer = new MemoryStream())
{
//replace the context response with our buffer
var stream = httpContext.Response.Body;
httpContext.Response.Body = buffer;
//invoke the rest of the pipeline
await _next.Invoke(httpContext);
//reset the buffer and read out the contents
buffer.Seek(0, SeekOrigin.Begin);
var reader = new StreamReader(buffer);
using (var bufferReader = new StreamReader(buffer))
{
string body = await bufferReader.ReadToEndAsync();
//reset to start of stream
buffer.Seek(0, SeekOrigin.Begin);
//copy our content to the original stream and put it back
await buffer.CopyToAsync(stream);
httpContext.Response.Body = stream;
_logger.LogInformation("Response:[{response}]", body);
}
}
}
}
}
无论对于request
和response
,都是Stream
类型,当被读取后,内部的偏移会移动。而两者情况又有不同。
Request
request
如果被读取后,后面的组件就无法再次读取,但是.net提供了EnableBuffering()
方法允许对request重复读取。
但是这里有一点需要注意,我原本将读取的代码提取到一个单独的方法中,把request
传入读取。
async Task<string> ReadBodyStr(HttpRequest req) {
req.EnableBuffering();
using (StreamReader requestReader = new StreamReader(req.Body, Encoding.UTF8))
{
var bodyStr = await requestReader.ReadToEndAsync();
req.Body.Position = 0;
return bodyStr;
}
}
这里用req.Body
传入StreamReader
,using结束后stream会被自动关闭,导致request也被关闭,后续的组件无法读取到任何内容。
我调试了很久,最后发现只有写在一个方法中才能让后面的组件正确获取内容
Response
Response
的问题在于默认的Response
不支持seek
,而当后面的组件开始写入Response后,写入的内容可能已经发往客户端,我这里就读不到了。所以有了SO上的这个hack方式。
即用MemoryStream替换Response
中原本的Body,给后面的组件处理后,读出内容,Seek
到开始位置,再写入原始的Stream中。