實現打印請求參數和響應結果的中間件,本以為比較容易,但是花了不少時間。
正確的代碼:
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中。