上一篇文章中我们已经成功的记录了Request部分的信息,现在我们来看下如何记录Response的内容。
相比于Request,Response额外多了个StatusCode
,然后内容都是通过Body
读取,不过不同于Request.Body
的只读,Response.Body
是个只写的数据流。
可以看到默认Response.Body
数据流数据类型为Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpResponseStream
。通过查看其源代码,我们可以看到该数据流其实是个空壳,其仅仅是IHttpResponseControl
的外部封装,因为前面已经有了替换Request.Body
的经验,所以此处理所当然的,我们也应该将默认的Response.Body
替换成一个可以读取的输出流。
为了保证通用,我们肯定不能按照HttpResponseStream
的做法来重写一个可以读取并且可以重新设置位置的Stream
,所以此处思路上直接简单的考虑对MemoryStream
进行封装,其内部包含默认的HttpResponseStream
,这样不管未来实际Response.Body
究竟是什么,我们都可以做到从中读取并恢复数据流当前位置,保证程序的正确运行。
通过查看HttpResponseStream
源代码,我们已经可以知道其核心是Write
和Flush
两个方法,既然是打算对MemoryStream
进行封装,那么我们也需要了解下其源代码。通过源码,我们可以发现其BeginWrite
、WriteByte
、WriteAsync
等方法底层其实都是在调用Write
方法,再加上必需的Dispose
和Flush
,所以最终我们得到如下代码
public class MemoryWrappedHttpResponseStream : MemoryStream
{
private Stream _innerStream;
public MemoryWrappedHttpResponseStream(Stream innerStream)
{
this._innerStream = innerStream ?? throw new ArgumentNullException(nameof(innerStream));
}
public override void Flush()
{
this._innerStream.Flush();
base.Flush();
}
public override void Write(byte[] buffer, int offset, int count)
{
base.Write(buffer, offset, count);
this._innerStream.Write(buffer, offset, count);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
this._innerStream.Dispose();
}
}
public override void Close()
{
base.Close();
this._innerStream.Close();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
然后我们将上篇内容中读取数据流的代码稍作调整
private async Task<string> ReadBodyAsync(HttpResponse response)
{
if (response.Body.Length > 0)
{
//var position = response.Body.Position;
response.Body.Seek(0, SeekOrigin.Begin);
var encoding = this.GetEncoding(response.ContentType);
var retStr = await ReadStreamAsync(response.Body, encoding, false).ConfigureAwait(false);
//response.Body.Position = position;
//读取完成后再重新赋值位置这个过程可能不需要,因为数据流是只写的
return retStr;
}
return null;
}
private Encoding GetEncoding(string contentType)
{
var mediaType = contentType == null ? default(MediaType) : new MediaType(contentType);
var encoding = mediaType.Encoding;
if (encoding == null)
{
encoding = Encoding.UTF8;
}
return encoding;
}
private void EnableReadAsync(HttpResponse response)
{
if (!response.Body.CanRead || !response.Body.CanSeek)
{
response.Body = new MemoryWrappedHttpResponseStream(response.Body);
}
}
private async Task<string> ReadStreamAsync(Stream stream, Encoding encoding,bool forceSeekBeginZero=true)
{
using (StreamReader sr = new StreamReader(stream, encoding, true, 1024, true))//这里注意Body部分不能随StreamReader一起释放
{
var str = await sr.ReadToEndAsync();
if (forceSeekBeginZero)
{
stream.Seek(0, SeekOrigin.Begin);//内容读取完成后需要将当前位置初始化,否则后面的InputFormatter会无法读取
}
return str;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
这样如何读取Body部分的准备工作就做完了,下面我们要做的就是在中间件的InvokeAsync
或Invoke
方法中声明相关的调用,其大致代码如下
public async Task InvokeAsync(HttpContext context)
{
this.EnableReadAsync(context.Response);
context.Response.OnCompleted(async o =>
{
var c = o as HttpContext;
if (c != null)
{
var retStr = await this.ReadBodyAsync(c.Response).ConfigureAwait(false);
//this._logger.LogDebug("Response at:{Time} Response:{Response}", DateTime.Now, retStr);
}
}, context);
await _next(context);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
上面已经完成了如何在中间件中读取Response.Body
,但实际不可能所有的响应都要做记录,另外上面的方法其实额外消耗了服务器内存(而且是大量),所以我们应该还要研究下如何筛选哪些请求需要记录,然后在此基础上再确认哪些响应内容应当被记录,这些将在下次研究完成后再述。
ASP.NET Core 如何记录每次响应的Response信息 - sky 胡萝卜星星 - CSDN博客
原文:https://www.cnblogs.com/lonelyxmas/p/10937017.html