- A+
前言
前几天有群友在群里问如何在我之前的文章《ASP.NET Core WebApi返回结果统一包装实践》的时候有点疑问,主要的疑问点就是关于Respouse的读取的问题。在之前的文章《深入探究ASP.NET Core读取Request.Body的正确方式》曾分析过关于Request的读取问题,需要读取Response的场景同样经常遇到,比如读取输出信息或者包装一下输出结果等。无独有偶Response的读取同样存在类似的问题,本文我们便来分析一下如何进行Response的Body读取。
使用方式
我们在日常的使用中是如何读取流呢?很简单,直接使用StreamReader
去读取,方式如下
public override void OnResultExecuted(ResultExecutedContext context) { //操作流之前恢复一下操作位 context.HttpContext.Response.Body.Position = 0; StreamReader stream = new StreamReader(context.HttpContext.Response.Body); string body = stream.ReadToEnd(); _logger.LogInformation("body content:" + body); context.HttpContext.Response.Body.Position = 0; base.OnResultExecuted(context); }
代码很简单,直接读取即可,可是这样读取是有问题的会抛出异常System.ArgumentException:“Stream was not readable.”
异常信息就是的意思是当前Stream不可读,也就是Respouse的Body是不可以被读取的。关于StreamReader到底和Stream有啥关联,我们在之前的文章深入探究ASP.NET Core读取Request.Body的正确方式一文中有过源码分析,这里就不在赘述了,有兴趣的同学可以自行翻阅,强烈建议在阅读本文之前可以看一下那篇文章,方便更容易了解。
如何解决上面的问题呢?方式也很简单,比如你想在你的程序中保证Response的Body都是可读的,你可以定义一个中间件解决这个问题。
public static IApplicationBuilder UseResponseBodyRead(this IApplicationBuilder app) { return app.Use(async (context, next) => { //获取原始的Response Body var originalResponseBody = context.Response.Body; try { //声明一个MemoryStream替换Response Body using var swapStream = new MemoryStream(); context.Response.Body = swapStream; await next(context); //重置标识位 context.Response.Body.Seek(0, SeekOrigin.Begin); //把替换后的Response Body复制到原始的Response Body await swapStream.CopyToAsync(originalResponseBody); } finally { //无论异常与否都要把原始的Body给切换回来 context.Response.Body = originalResponseBody; } }); }
本质就是先用一个可操作的Stream比如咱们这里的MemoryStream
替换默认的ResponseBody,让后续对ResponseBody的操作都是针对新的ResponseBody进行操作,完成之后把替换后的ResponseBody复制到原始的ResponseBody。最终无论异常与否都要把原始的Body给切换回来。需要注意的是,这个中间件的位置尽量要放在比较靠前的位置注册,至少也要保证在你所有要操作ResponseBody之前的位置注册。如下所示
var app = builder.Build(); app.UseResponseBodyRead();
源码探究
通过上面我们了解到了ResponseBody是不可以被读取的,至于为什么呢,这个我们需要通过相关源码了解一下。通过HttpContext
类的源码我们可以看到相关定义
public abstract class HttpContext { public abstract HttpResponse Response { get; } }
这里看到HttpContext
本身是个抽象类,看一下它的属性HttpResponse
类的定义也是一个抽象类
public abstract class HttpResponse { }
由上面可知Response
属性是抽象的,所以抽象类HttpResponse
必然包含一个子类去实现它,否则没办法直接操作相关方法。这里我们介绍一个网站https://source.dot.net用它可以更轻松的阅读微软类库的源码,比如CLR、ASP.NET Core、EF Core等等,双击一个类或者属性方法可以查找引用和定义它们的地方,非常方便,它的源码都是最新版本的,来源就是GitHub上的相关仓库。找到实例化HttpResponse
的为位置在HttpContext
的子类DefaultHttpContext
类中[点击查看源码?]
public sealed class DefaultHttpContext : HttpContext { private readonly DefaultHttpRequest _request; private readonly DefaultHttpResponse _response; public DefaultHttpContext(IFeatureCollection features) { _features.Initalize(features); _request = new DefaultHttpRequest(this); _response = new DefaultHttpResponse(this); } public override HttpRequest Request => _request; public override HttpResponse Response => _response; }
防止大家比较绕解释一下,因为
HttpContext
是抽象类,它包含了抽象属性HttpResponse
类型的属性Response
,所以HttpContext
必然有子类去集成它,由于HttpResponse
也是抽象类,所以也必须包含了子类去继承它。
寻找HttpResponse Body定义
通过上面的代码我们可以看到HttpResponse
的子类为DefaultHttpResponse
类。找到类中Body
属性定义的地方[点击查看源码?]看一下具体实现
internal sealed class DefaultHttpResponse : HttpResponse { private static readonly Func<IFeatureCollection, IHttpResponseBodyFeature?> _nullResponseBodyFeature = f => null; private readonly DefaultHttpContext _context; private FeatureReferences<FeatureInterfaces> _features; public DefaultHttpResponse(DefaultHttpContext context) { _context = context; _features.Initalize(context.Features); } //在FeatureReferences<FeatureInterfaces>中取出ResponseBody的交互操作IHttpResponseBodyFeature private IHttpResponseBodyFeature HttpResponseBodyFeature => _features.Fetch(ref _features.Cache.ResponseBody, _nullResponseBodyFeature)!; //Body本身是Stream它是抽象类 public override Stream Body { //在IHttpResponseBodyFeature实例中查找Stream get { return HttpResponseBodyFeature.Stream; } set { var otherFeature = _features.Collection.GetRequiredFeature<IHttpResponseBodyFeature>(); if (otherFeature is StreamResponseBodyFeature streamFeature && streamFeature.PriorFeature != null && object.ReferenceEquals(value, streamFeature.PriorFeature.Stream)) { _features.Collection.Set(streamFeature.PriorFeature); return; } _features.Collection.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(value, otherFeature)); } } }
Body本身是Stream但是Stream是抽象类,但是这里并没有对Stream的子类直接进行定义,而是引入了IHttpResponseBodyFeature
去和Stream交互,主要原因还是因为ResponseBody涉及到一个交互体系,比如包含PipeWriter、SendFile等操作。所以这里我们只能顺着IHttpResponseBodyFeature
的操作找到相关的实现类,通过查找引用关系我找到了实现类HttpProtocol
[点击查看源码?]我们看一下它的定义
internal partial class HttpProtocol : IFeatureCollection, IHttpRequestFeature, IHttpResponseFeature, IHttpResponseBodyFeature, IRouteValuesFeature, IEndpointFeature, IHttpRequestIdentifierFeature, IHttpRequestTrailersFeature, IHttpExtendedConnectFeature, IHttpUpgradeFeature, IRequestBodyPipeFeature, IHttpConnectionFeature, IHttpRequestLifetimeFeature, IHttpBodyControlFeature, IHttpMaxRequestBodySizeFeature, IHttpRequestBodyDetectionFeature, IHttpWebTransportFeature, IBadRequestExceptionFeature { internal protected IHttpResponseBodyFeature? _currentIHttpResponseBodyFeature; private void FastReset() { //省略一部分代码 _currentIHttpResponseBodyFeature = this; //省略一部分代码 } }
它实现了很多接口,其中包含了IHttpResponseBodyFeature
接口和IFeatureCollection
接口,这两个接口在DefaultHttpResponse
类中都有涉猎,是Response输出的交互类,可以理解为Response类是门面,实际的操作都是调用的具体类。我们可以分析一下包含获取具体类型实例的操作,第一个便是它的索引器
操作
internal protected IHttpResponseBodyFeature? _currentIHttpResponseBodyFeature; object? IFeatureCollection.this[Type key] { get { object? feature = null; //省略一部分代码 if (key == typeof(IHttpResponseBodyFeature)) { feature = _currentIHttpResponseBodyFeature; } //省略一部分代码 return feature ?? ConnectionFeatures?[key]; } set { _featureRevision++; //省略一部分代码 if (key == typeof(IHttpResponseBodyFeature)) { _currentIHttpResponseBodyFeature = (IHttpResponseBodyFeature?)value; } //省略一部分代码 } }
它本身也提供Get和Set相关的类来操作和获取具体的相关的类型
TFeature? IFeatureCollection.Get<TFeature>() where TFeature : default { TFeature? feature = default; if (typeof(TFeature) == typeof(IHttpResponseBodyFeature)) { feature = Unsafe.As<IHttpResponseBodyFeature?, TFeature?>(ref _currentIHttpResponseBodyFeature); } return feature; } void IFeatureCollection.Set<TFeature>(TFeature? feature) where TFeature : default { _featureRevision++; if (typeof(TFeature) == typeof(IHttpResponseBodyFeature)) { _currentIHttpResponseBodyFeature = Unsafe.As<TFeature?, IHttpResponseBodyFeature?>(ref feature); } }
为什么会这样的,相信大家已经猜到了HttpProtocol
实现了很多的接口,意味着它有很多接口的能力。提供的这几个方法可以根据类型快速的获取想得到的实例。因为在HttpProtocol
定义了许多变量承载它实现的接口的变量来承载当前实例,所以在DefaultHttpResponse
看到了类似缓存的效果获取具体接口的对应实例。我们知道了HttpProtocol
实现了IHttpResponseBodyFeature
接口,所以我们在HttpProtocol
类中查找给IHttpResponseBodyFeature的Stream
属性赋值的地方即可,通过上面HttpProtocol
类的定义方式我们可以看到它是partial
也就是部分类,在另一个部分类中找到了赋值的地方[点击查看源码?]
Stream IHttpResponseBodyFeature.Stream => ResponseBody; PipeWriter IHttpResponseBodyFeature.Writer => ResponseBodyPipeWriter; Stream IHttpResponseFeature.Body { get => ResponseBody; set => ResponseBody = value; }
通过这个代码我们可以看到IHttpResponseBodyFeature.Stream
来自ResponseBody
属性,找到给HttpProtocol属性ResponseBody
赋值的地方[点击查看源码?]
protected BodyControl? _bodyControl; public Stream ResponseBody { get; set; } = default!; public PipeWriter ResponseBodyPipeWriter { get; set; } = default!; public void InitializeBodyControl(MessageBody messageBody) { if (_bodyControl == null) { _bodyControl = new BodyControl(bodyControl: this, this); } (RequestBody, ResponseBody, RequestBodyPipeReader, ResponseBodyPipeWriter) = _bodyControl.Start(messageBody); }
上面的代码我们可以看到ResponseBody
定义和赋值的地方,我们可以看到给ResponseBody
赋值来自BodyControl
实例的Start方法里
这个方法传递的是当前HttpProtocol实例
,所以直接找到BodyControl.Start方法
定义的地方[点击查看源码?]查看实现
internal sealed class BodyControl { //HttpResponseStream private readonly HttpResponseStream _response; private readonly HttpResponsePipeWriter _responseWriter; private readonly HttpRequestPipeReader _requestReader; private readonly HttpRequestStream _request; public BodyControl(IHttpBodyControlFeature bodyControl, IHttpResponseControl responseControl) { _requestReader = new HttpRequestPipeReader(); _request = new HttpRequestStream(bodyControl, _requestReader); _responseWriter = new HttpResponsePipeWriter(responseControl); //实例化HttpResponseStream的地方 _response = new HttpResponseStream(bodyControl, _responseWriter); } public (Stream request, Stream response, PipeReader reader, PipeWriter writer) Start(MessageBody body) { //省略代码 if (body.RequestUpgrade) { //默认走不到暂时忽略 } else if (body.ExtendedConnect) { //默认走不到暂时忽略 } else { //默认走到这里 return (_request, _response, _requestReader, _responseWriter); } } }
好了,饶了这么多的弯,我们水落石出了找到了HttpResponse.Body
的最终来源来自HttpResponseStream
类的实例。所以结论就是HttpResponse的Body是HttpResponseStream实例。总结一下
- HttpResponse的Body是Stream类型的,在DefaultHttpResponse中并未给Body直接赋值,而是在
IHttpResponseBodyFeature
实例中获取Stream
属性,这个类负责是ResponseBody相关的交互。 - IHttpResponseBodyFeature的实现类是
HttpProtocol
,这是一个部分类。在这里IHttpResponseBodyFeature.Stream
属性来自HttpProtocol类ResponseBody
属性。 - 给
HttpProtocol类ResponseBody
属性赋值来自BodyControl的Start方法
,它返回的是BodyControl
类的_response
属性,这个属性的是HttpResponseStream
类型的。 - 所以得到结论
HttpResponse.Body
也就是Stream类型的,来自HttpResponseStream
类的实例。
HttpResponseStream类定义
上面饶了这么大的圈找到了HttpResponse.Body
实例的类型HttpResponseStream
类,找到类定义的地方看一下里面的实现[点击查看源码?]
internal sealed partial class HttpResponseStream : Stream { //说明不支持读,如果想知道流是否可读可以使用这个属性先判断 public override bool CanRead => false; //流不可查找 public override bool CanSeek => false; //支持写 public override bool CanWrite => true; //不能获取流的长度否则抛出异常 public override long Length => throw new NotSupportedException(SR.net_noseek); //不可读取和设置位置否则抛出异常 public override long Position { get => throw new NotSupportedException(SR.net_noseek); set => throw new NotSupportedException(SR.net_noseek); } //不支持设置Seek否则抛出异常 public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(SR.net_noseek); //不支持Length否则抛出异常 public override void SetLength(long value) => throw new NotSupportedException(SR.net_noseek); //不支持读取操作否则抛出异常 public override int Read(byte[] buffer, int offset, int size) => throw new InvalidOperationException(SR.net_writeonlystream); //不支持读读相关的操作 public override IAsyncResult BeginRead(byte[] buffer, int offset, int size, AsyncCallback? callback, object? state) { throw new InvalidOperationException(SR.net_writeonlystream); } public override int EndRead(IAsyncResult asyncResult) => throw new InvalidOperationException(SR.net_writeonlystream); //省略写相关方法和释放相关的方法,只看设计到读相关的地方 }
通过HttpResponseStream
类的定义我们可以看到,HttpResponseStream
本身是Stream
抽象类的子类。涉及到读相关的方法是直接抛出异常,也就是最开始我们直接读取HttpResponse.Body
读取直接抛出异常的原因。不仅仅是读取的方法不可用Postion、Length、Seek相关的方法都是不可操作的,操作了都会抛出异常。
UseHttpLogging的解决方式
从ASP.NET Core6.0之后开始,推出了HTTP日志记录功能,使用方式如下
var builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpLogging(logging => { logging.LoggingFields = HttpLoggingFields.ResponseBody; logging.RequestBodyLogLimit = 4096; }); var app = builder.Build(); app.UseHttpLogging();
不过我们通过上面看到了HttpResponse.Body
默认情况下是不可以读取的,但是输出Http日志时候是可以读取ResponseBody
的,所以我们可以看一下里面的相关实现,在HttpLoggingMiddleware
中间件里,因为这个中间件里涉及到Http日志记录的相关逻辑实现,而ResponseBody只是其中的一个选项,所以咱们只关注这一部分的实现[点击查看源码?]
ResponseBufferingStream? responseBufferingStream = null; IHttpResponseBodyFeature? originalBodyFeature = null; try { //获取原始的response var response = context.Response; if (options.LoggingFields.HasFlag(HttpLoggingFields.ResponseBody)) { //保存原始的IHttpResponseBodyFeature也就是上面提到的ResponseBody交互类 originalBodyFeature = context.Features.Get<IHttpResponseBodyFeature>()!; //实例化ResponseBufferingStream responseBufferingStream = new ResponseBufferingStream(originalBodyFeature, options.ResponseBodyLogLimit, _logger, context, options.MediaTypeOptions.MediaTypeStates, options); //用ResponseBufferingStream实例替换原始ResponseBody response.Body = responseBufferingStream; //将responseBufferingStream设置到当前的IHttpResponseBodyFeature context.Features.Set<IHttpResponseBodyFeature>(responseBufferingStream); } await _next(context); //输出日志 if (requestBufferingStream?.HasLogged == false) { requestBufferingStream.LogRequestBody(); } if (responseBufferingStream != null) { var responseBody = responseBufferingStream.GetString(responseBufferingStream.Encoding); if (!string.IsNullOrEmpty(responseBody)) { _logger.ResponseBody(responseBody); } } } finally { responseBufferingStream?.Dispose(); if (originalBodyFeature != null) { //还原原始的IHttpResponseBodyFeature context.Features.Set(originalBodyFeature); } }
通过上面的代码我们可以看到,其实也是实现了类似的操作,用ResponseBufferingStream
替换掉原始的HttpResponseStream
类型,替换的逻辑要在中间件执行next()
之前,操作完成之后也就是执行了next()
之后再把原始的IHttpResponseBodyFeature
替换回来,有关具体的ResponseBufferingStream
实现方式咱们这里不做详细描述了,不是本文重点。
ResponseBufferingStream
的实现并不是使用MemoryStream
这种可读取的流替换掉默认的HttpResponseStream,ResponseBufferingStream
的LogRequestBody()
方法使用ILogger
输出日志并没有直接去读取Stream,而是反其道重写了Stream的Write()
方法,因为对HttpResponseBody实例
HttpResponseStream
的输出写操作本质是调用Stream的Write()
方法,重写了Write()
方法之后会把写入的内容记录到Buffer
中,LogRequestBody()
方法通过读取Buffer中的内容得到字符串,使用ILogger输出日志。
答疑解惑
在之前的讨论中有许多小伙伴对用MemoryStream
替换ResponseBody存在一个疑惑,就是既然已经替换掉了,一直用MemoryStream
不就好了嘛,为啥还要把ResponseBody原始值记录下来,结束后再替换回来。这个疑问咋一听确实也没毛病,但是等大致了解了它的使用过程之后才恍然大悟,原来是这么回事,在这里咱们就看一下为啥会是这样。
首先说一下结论,如果把ResponseBody替换为MemoryStream
之后,不对原始的ResponseBody进行操作的话,在这个中间件(类似上面说的到的UseResponseBodyRead中间件)之后的操作,可能是后续的其它中间件或者是各种终结点比如Controller的Action亦或者是MinimalApi的Map方法等,是可以读取和写入值的,也就是在替换中间件的范围内,也就是大家经常说的套娃模式,被它套进去的是一直生效的,没任何问题,终结点本身也是中间件
。下面这张图相信大家经常看到打个比方如果我的UseResponseBodyRead
中间件是图里的Middleware1把ResponseBody替换为MemoryStream
,那么后续的操作比如Middleware2和Middleware3还有后续的终结点之类的读取ResponseBody是完全没有问题的。但是最终Http的输出结果肯定是不符合预期的,这主要涉及到HttpResponseStream.Write()
的问题,我们知道最终我们输出的结果会体现在Write()
方法上[点击查看源码?],核心代码如下所示
internal sealed class HttpResponseStream : Stream { private readonly HttpResponsePipeWriter _pipeWriter; private readonly IHttpBodyControlFeature _bodyControl; public HttpResponseStream(IHttpBodyControlFeature bodyControl, HttpResponsePipeWriter pipeWriter) { _bodyControl = bodyControl; _pipeWriter = pipeWriter; } //重写Stream的Write操作 public override void Write(byte[] buffer, int offset, int count) { if (!_bodyControl.AllowSynchronousIO) { throw new InvalidOperationException(CoreStrings.SynchronousWritesDisallowed); } //调用WriteAsync方法 WriteAsync(buffer, offset, count, default).GetAwaiter().GetResult(); } public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { //本质调用了HttpResponsePipeWriter的写方法 return _pipeWriter.WriteAsync(new ReadOnlyMemory<byte>(buffer, offset, count), cancellationToken).GetAsTask(); } )
通过上面我们可以看到HttpResponseStream
的Write()
方法本质是调用了HttpResponsePipeWriter
的WriteAsync()
方法,HttpResponseStream本身不存储写入的数据。而HttpResponsePipeWriter
实例的构建是在BodyControl
类中上面咱们已经粘贴过实例化的源码了,可自行翻阅上去看看HttpResponsePipeWriter
类的定义相关。所以上面把ResponseBody替换为MemoryStream
,最终的结果要体现在HttpResponseStream
实例中,否则的话没有办法正常输出。可以用一个伪代码例子演示一下这个原理
Order order1 = new Order { Address = "北京市海淀区" }; SetOrder(order1); Console.WriteLine($"最后地址:{order1.Address}"); public void SetOrder(Order order2) { order2 = new Order { Address = "上海市闵行区" }; Console.WriteLine($"设置地址:{order2.Address}"); }
这个示例中即使SetOrder
方法中设置了新的Address,但是脱离了SetOrder
方法作用域后,外面的最后地址依然是北京市海淀区
。在调用SetOrder
进入方法的时候order1和方法形参order2都指向的是Address = "北京市海淀区"
,在SetOrder方法内部完成实例化之后order2指向的是Address = "上海市闵行区"
,但是order1依然指向的是Address = "北京市海淀区"
,因为引用传递形参本身只是存储的引用地址,更换了引用地址就和原来的地址脱钩了,如果想让内外行为一直必须要体现到原始值上面去。我们替换ResponseBody
的时候也是同理,最终Write本质还是要依赖HttpResponseStream
里的HttpResponsePipeWriter
属性,但是MemoryStream
可没有HttpResponsePipeWriter
。你可能会有疑问,我上面也没把MemoryStream
结果Write()
到HttpResponseStream
里去啊?但是上面使用了CopyToAsync
方法与原始的的ResponseBody类型HttpResponseStream
交互,CopyToAsync方法本质就是在调用WriteAsync()
方法,口说无凭直接上代码[点击查看源码?],核心代码如下所示
public virtual Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) { //省略一部分代码 return Core(this, destination, bufferSize, cancellationToken); static async Task Core(Stream source, Stream destination, int bufferSize, CancellationToken cancellationToken) { //使用了对象池复用空间 byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize); try { int bytesRead; while ((bytesRead = await source.ReadAsync(new Memory<byte>(buffer), cancellationToken).ConfigureAwait(false)) != 0) { //最终也是调用的目标流的WriteAsync方法 await destination.WriteAsync(new ReadOnlyMemory<byte>(buffer, 0, bytesRead), cancellationToken).ConfigureAwait(false); } } finally { ArrayPool<byte>.Shared.Return(buffer); } } }
总结
本文主要讲解了如何读取ResponseBody
,默认情况下是不可以读取的,需要我们使用了中间件结合MemoryStream
自行处理一下,同时我们结合和Http日志记录中间件里的处理方式对比了一下,最终答疑了为了要把替换的结果还得继续体现在原始的ResponseBody
上面去,整体来说这方面还是相对容易理解的,只是找起来可能比较麻烦。大致总结一下
- ResponseBody默认不可读取,因为它的实例是
HttpResponseStream
这个类重写了Stream的Read相关的方法,但是实现是抛出异常的,所以我们需要可读的类来替换默认的操作,MemoryStream
可以辅助实现。 UseHttpLogging
中间件也可以读取ResponseBody里的结果,但是它是使用的重写Stream的Write相关的方法,在Write方法里使用Buffer记录了写过的数据,然后通过GetString()
方法读取Buffer里的内容实现记录要输出的值。MemoryStream
解决的是我们在写代码过程中对ResponseBody的读取或写入操作,但是程序处理完之后要把MemoryStream
的结果在体现到HttpResponseStream
中去,否则虽然程序中读取写入Body没问题,但是输出的结果会出问题。
说句题外话,ChatGTP
的发布对人们心里的冲击还是挺大的,因为它表现出来的强大效果让人眼前一亮,很多博主和企业也借此风口寻找新的出路,甚至有人会担心会不会被替代失业。个人以为新的技术大行其道必然会带来新的产业,新的产业的新的岗位同时也是需要更多的人参与进来。所以保持对新事物的好奇心多多参与。工具不会替代人,能替代人的是会使用工具的人。