由ASP.NET Core读取Response.Body引发的思考

打印 上一主题 下一主题

主题 750|帖子 750|积分 2250

前言

    前几天有群友在群里问如何在我之前的文章《ASP.NET Core WebApi返回结果统一包装实践》的时候有点疑问,主要的疑问点就是关于Respouse的读取的问题。在之前的文章《深入探究ASP.NET Core读取Request.Body的正确方式》曾分析过关于Request的读取问题,需要读取Response的场景同样经常遇到,比如读取输出信息或者包装一下输出结果等。无独有偶Response的读取同样存在类似的问题,本文我们便来分析一下如何进行Response的Body读取。
使用方式

我们在日常的使用中是如何读取流呢?很简单,直接使用StreamReader去读取,方式如下
  1. public override void OnResultExecuted(ResultExecutedContext context)
  2. {
  3.     //操作流之前恢复一下操作位
  4.     context.HttpContext.Response.Body.Position = 0;
  5.     StreamReader stream = new StreamReader(context.HttpContext.Response.Body);
  6.     string body = stream.ReadToEnd();
  7.     _logger.LogInformation("body content:" + body);
  8.     context.HttpContext.Response.Body.Position = 0;
  9.     base.OnResultExecuted(context);
  10. }
复制代码
代码很简单,直接读取即可,可是这样读取是有问题的会抛出异常System.ArgumentException:“Stream was not readable.”异常信息就是的意思是当前Stream不可读,也就是Respouse的Body是不可以被读取的。关于StreamReader到底和Stream有啥关联,我们在之前的文章深入探究ASP.NET Core读取Request.Body的正确方式一文中有过源码分析,这里就不在赘述了,有兴趣的同学可以自行翻阅,强烈建议在阅读本文之前可以看一下那篇文章,方便更容易了解。
如何解决上面的问题呢?方式也很简单,比如你想在你的程序中保证Response的Body都是可读的,你可以定义一个中间件解决这个问题。
  1. public static IApplicationBuilder UseResponseBodyRead(this IApplicationBuilder app)
  2. {
  3.     return app.Use(async (context, next) =>
  4.     {
  5.         //获取原始的Response Body
  6.         var originalResponseBody = context.Response.Body;
  7.         try
  8.         {
  9.             //声明一个MemoryStream替换Response Body
  10.             using var swapStream = new MemoryStream();
  11.             context.Response.Body = swapStream;
  12.             await next(context);
  13.             //重置标识位
  14.             context.Response.Body.Seek(0, SeekOrigin.Begin);
  15.             //把替换后的Response Body复制到原始的Response Body
  16.             await swapStream.CopyToAsync(originalResponseBody);
  17.         }
  18.         finally
  19.         {
  20.             //无论异常与否都要把原始的Body给切换回来
  21.             context.Response.Body = originalResponseBody;
  22.         }
  23.     });
  24. }
复制代码
本质就是先用一个可操作的Stream比如咱们这里的MemoryStream替换默认的ResponseBody,让后续对ResponseBody的操作都是针对新的ResponseBody进行操作,完成之后把替换后的ResponseBody复制到原始的ResponseBody。最终无论异常与否都要把原始的Body给切换回来。需要注意的是,这个中间件的位置尽量要放在比较靠前的位置注册,至少也要保证在你所有要操作ResponseBody之前的位置注册。如下所示
  1. var app = builder.Build();
  2. app.UseResponseBodyRead();
复制代码
源码探究

通过上面我们了解到了ResponseBody是不可以被读取的,至于为什么呢,这个我们需要通过相关源码了解一下。通过HttpContext类的源码我们可以看到相关定义
  1. public abstract class HttpContext
  2. {
  3.     public abstract HttpResponse Response { get; }
  4. }
复制代码
这里看到HttpContext本身是个抽象类,看一下它的属性HttpResponse类的定义也是一个抽象类
  1. public abstract class HttpResponse
  2. {
  3. }
复制代码
由上面可知Response属性是抽象的,所以抽象类HttpResponse必然包含一个子类去实现它,否则没办法直接操作相关方法。这里我们介绍一个网站https://source.dot.net用它可以更轻松的阅读微软类库的源码,比如CLR、ASP.NET Core、EF Core等等,双击一个类或者属性方法可以查找引用和定义它们的地方,非常方便,它的源码都是最新版本的,来源就是GitHub上的相关仓库。找到实例化HttpResponse的为位置在HttpContext的子类DefaultHttpContext类中[<a href="https://github.com/dotnet/aspnetcore/blob/v7.0.4/src/Http/Http/src/DefaultHttpContext.cs#L62" target="_blank" rel="noopener">点击查看源码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

何小豆儿在此

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表