原文:如何在ASP.NET Core自定义中间件中读取Request.Body和Response.Body的内容?

文章名称: 如何在ASP.NET Core自定义中间件读取Request.Body和Response.Body的内容?
作者: Lamond Lu
地址: https://www.cnblogs.com/lwqlun/p/10954936.html
源代码: https://github.com/lamondlu/webapi-logger

背景

最近在徒手造轮子,编写一个ASP.NET Core的日志监控器,其中用到了自定义中间件读取Request.Body和Response.Body的内容,但是编写过程,并不像想象中的一帆风顺,ASP.NET Core针对Request.Body和Response.Body的几个特殊设计,导致了完成以上功能需要绕一些弯路。

原始代码

为了读取Request.Body和Response.Body的内容,我的实现思路如下:

  • 创建一个LoggerMiddleware的中间件,将它放置在项目中间件管道的头部。因为根据ASP.NET Core的中间件管道设计,只有第一个中间件才能获取到原始的请求信息和最终的响应信息。

  • Request.Body和Response.Body属性都是Steram类型, 在LoggerMiddleware中间件的InvokeAsync方法中,我们可以分别使用StreamReader读取Request.Body和Response.Body的内容。

根据以上思路,我编写了以下代码。

LoggerMiddleware.cs

    public class LoggerMiddleware
{
private readonly RequestDelegate _next; public LoggerMiddleware(RequestDelegate next)
{
_next = next;
} public async Task InvokeAsync(HttpContext context)
{
var requestReader = new StreamReader(context.Request.Body); var requestContent = requestReader.ReadToEnd();
Console.WriteLine($"Request Body: {requestContent}"); await _next(context); var responseReader = new StreamReader(context.Response.Body);
var responseContent = responseReader.ReadToEnd();
Console.WriteLine($"Response Body: {responseContent}");
}
}

Startup.cs

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseMiddleware<LoggerMiddleware>();
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
} app.UseHttpsRedirection();
app.UseMvc();
}

问题1:Response.Body的Stream不可读

这里为了测试我创建了一个默认的ASP.NET Core WebApi项目。当运行程序,使用GET方式调用/api/values之后,控制台会返回第一个需要处理的错误。

System.ArgumentException: Stream was not readable.

即ASP.NET Core默认创建的Response.Body属性是不可读的。

这一点我们可以通过打断点看到Response.Body属性的CanRead值是false。

这就很糟糕了,ASP.NET Core默认并不想让我们在中间件中直接读取Response.Body中的信息。

这里看似的无解,但是我们可以转换一下思路,既然ASP.NET Core默认将Response.Body是不可读的,那么我们就使用一个可读可写的Stream对象将其替换掉。这样当所有中间件都依次执行完之后,我们就可以读取Response.Body的内容了。

public async Task InvokeAsync(HttpContext context)
{
var requestReader = new StreamReader(context.Request.Body); var requestContent = requestReader.ReadToEnd();
Console.WriteLine($"Request Body: {requestContent}"); using (var ms = new MemoryStream())
{
context.Response.Body = ms;
await _next(context); context.Response.Body.Position = 0; var responseReader = new StreamReader(context.Response.Body); var responseContent = responseReader.ReadToEnd();
Console.WriteLine($"Response Body: {responseContent}"); context.Response.Body.Position = 0;
}
}

注意:

  • 读取Response.Body的时候,需要设置Position = 0, 这样是为了重置指针,如果不这样做的话,会导致读取的流不正确。

  • 这里千万不要用using包裹StreamReader, 因为StreamReader会在读取完Stream内容之后,将Stream关闭,导致后续由于Stream关闭,而不能再次读取Stream中的内容。如果必须使用,请使用StreamReader的以下重载,将leaveOpen参数设置为true, 确保StreamReader对象被销毁的时候不会自动关闭读取的Stream.

    public StreamReader(Stream stream, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize, bool leaveOpen);

重新启动程序,请求/api/values, 我们就得到的正确的结果。

进一步完善代码

以上代码实现,看似已经能够读取Response.Body的内容了,但是其实还是有问题的。

回想一下,我们做出以上方案的前提是,当前LoggerMiddleware中间件必须位于中间件管道的头部。

如果不能保证这个约定, 就会出现问题,因为我们在LoggerMiddleware中间件中将Response.Body属性指向了一个新的可读可写的Stream对象。如果LoggerMiddleware中间件之前的某个中间件中设置过Response.Body, 就会导致这部分设置丢失。

因此正确的设置方式应该是这样的:

    public async Task InvokeAsync(HttpContext context)
{
var originalResponseStream = context.Response.Body; var requestReader = new StreamReader(context.Request.Body); var requestContent = requestReader.ReadToEnd();
Console.WriteLine($"Request Body: {requestContent}"); using (var ms = new MemoryStream())
{
context.Response.Body = ms;
await _next(context); ms.Position = 0;
var responseReader = new StreamReader(ms); var responseContent = responseReader.ReadToEnd();
Console.WriteLine($"Response Body: {responseContent}"); ms.Position = 0; await ms.CopyToAsync(originalResponseStream);
context.Response.Body = originalResponseStream;
}
}

代码解释:

  • 这里当进入LoggerMiddleware中间件时,我们将之前中间件操作完成之后的Response.Body对象对应的原始Stream, 保存在一个临时变量中
  • 当LoggerMiddelware中间件的任务完成之后,我们需要将后续产生的Response.Body流追加到原始Stream中,然后将Response.Body对象重置为这个新的Stream。

至此Repsonse.Body的问题都解决,下面我们再来看一下Request.Body的问题。

问题2:Request.Body的内容可以正确的显示,但是后续的ModelBinding都失败了

下面我们来请求POST /api/values, Request.Body里面的内容是字符串"123123"

服务器端返回了400错误, 错误信息

A non-empty request body is required.

这里就很奇怪,为啥请求体是空呢?我们回到中间件部分代码,这里我们在读取完Request.Body中的Stream之后,没有将Stream的指针重置,当前指针已经是Stream的尾部,所以后续ModelBinding的时候,读取不到Stream的内容了。

    public async Task InvokeAsync(HttpContext context)
{
...
var requestReader = new StreamReader(context.Request.Body); var requestContent = requestReader.ReadToEnd();
Console.WriteLine($"Request Body: {requestContent}");
...
}

于是,这里我们需要采取和Response.Body相同的处理方式,在读取完Request.Body之后,我们需要将Request.Body的Stream指针重置

    public async Task InvokeAsync(HttpContext context)
{
...
var requestReader = new StreamReader(context.Request.Body); var requestContent = requestReader.ReadToEnd();
Console.WriteLine($"Request Body: {requestContent}");
context.Request.Body.Position = 0;
...
}

你一定觉着至此问题就解决了,不过ASP.NET Core和你又开了一个玩笑

当你重新请求POST /api/values之后,你会得到以下结果。

错误原因:

System.NotSupportedException: Specified method is not supported.

翻译过来就是指定方法不支持。到底不支持啥呢?在代码上打上断点,你会发现Request.Body的CanSeek属性是false, 即Request.Body的Stream, 你是不能随便移动指针的,只能按顺序读取一次,默认不支持反复读取。

那么如何解决这个问题呢?

你可以在使用Request对象中的EnableRewind或者EnableBuffering。 这2个方法的作用都是在内存中创建缓冲区存放Request.Body的内容,从而允许反复读取Request.Body的Stream。

说明: 其实EnableBuffering方法内部就只直接调用的EnableRewind方法。

下面我们修改代码

    public async Task InvokeAsync(HttpContext context)
{
context.Request.EnableBuffering();
var requestReader = new StreamReader(context.Request.Body); var requestContent = requestReader.ReadToEnd();
Console.WriteLine($"Request Body: {requestContent}");
context.Request.Body.Position = 0; using (var ms = new MemoryStream())
{
context.Response.Body = ms;
await _next(context); ms.Position = 0;
var responseReader = new StreamReader(ms); var responseContent = responseReader.ReadToEnd();
Console.WriteLine($"Response Body: {responseContent}"); ms.Position = 0;
}
}

再次请求POST /api/values, api请求被正确的处理了。

如何在ASP.NET Core自定义中间件中读取Request.Body和Response.Body的内容?的更多相关文章

  1. ASP.NET Core 5.0 中读取Request中Body信息

    ASP.NET Core 5.0 中读取Request中Body信息 记录一下如何读取Request中Body信息 public class ValuesController : Controller ...

  2. ASP.NET Core 2.0 中读取 Request.Body 的正确姿势

    原文:ASP.NET Core 中读取 Request.Body 的正确姿势 ASP.NET Core 中的 Request.Body 虽然是一个 Stream ,但它是一个与众不同的 Stream ...

  3. 如何在 asp.net core 的中间件中返回具体的页面

    前言 在 asp.net core 中,存在着中间件这一概念,在中间件中,我们可以比过滤器更早的介入到 http 请求管道,从而实现对每一次的 http 请求.响应做切面处理,从而实现一些特殊的功能 ...

  4. 如何在ASP.NET Core 2.0中使用Razor页面

    如何在ASP.NET Core 2.0中使用Razor页面  DotNetCore2017-11-22 14:49 问题 如何在ASP.NET Core 2.0中使用Razor页面 解 创建一个空的项 ...

  5. 如何在ASP.NET Core Web API中使用Mini Profiler

    原文如何在ASP.NET Core Web API中使用Mini Profiler 由Anuraj发表于2019年11月25日星期一阅读时间:1分钟 ASPNETCoreMiniProfiler 这篇 ...

  6. ASP.NET Core自定义中间件的方式

    ASP.NET Core应用本质上,其实就是由若干个中间件构建成的请求处理管道.管道相当于一个故事的框架,而中间件就相当于故事中的某些情节.同一个故事框架采用不同的情节拼凑,最终会体现出不同风格的故事 ...

  7. asp.net core 自定义中间件

    官方文档:https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-2.1 中间件的定 ...

  8. asp.net core 自定义中间件【以dapper为例】

    在asp.net core开发中.按照国际案例开始.都是先在Nuget安装XXX包.比如我们今天要用到的Dapper nuget里面安装Dapper 1.然后新建一个类文件DapperExtensio ...

  9. asp.net core 自定义中间件和service

    首先新建项目看下main方法: public static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel ...

随机推荐

  1. 【贪心 堆】luoguP2672 推销员

    堆维护,贪心做法 题目描述 阿明是一名推销员,他奉命到螺丝街推销他们公司的产品.螺丝街是一条死胡同,出口与入口是同一个,街道的一侧是围墙,另一侧是住户.螺丝街一共有N家住户,第i家住户到入口的距离为S ...

  2. 【NOIP2017提高A组冲刺11.8】购物

    这个范围对DP不友好,和CF的一道C题非常像,贪心+后悔. 先使用k个优惠券购买k个q最小的(钱不购买则退出),同时把这k个p[i]-q[i]放入小根堆,然后将剩下的n-k个按p升序排序,记小根堆堆顶 ...

  3. windows显示文件扩展名

    搜索打开windows的文件资源管理选项,如下去掉“隐藏已知文件类型的扩展名”即可 打开之后显示如下:

  4. 【php】运算符优先级界定

    <?php $i = 1; $array[$i] = $i++; print_r($array);die; //输出 Array([2] => 1) $a = 1; echo $a + $ ...

  5. nodemailer发送邮件遇到的一些问题

    使用nodemailer发送邮件一直困惑了我好几天,百度谷歌都没有找到的几个问题,方便大家排查. 我使用的是0.7.1版本,其他的版本好像报错,就没用. 错误信息:Mail from command ...

  6. Method for Estimating the Number of Concurrent Users

    1. Formula for Estimating the Average Number of Concurrent users We begin by defining what the numbe ...

  7. BZOJ 2005 [Noi2010]能量采集 ——Dirichlet积

    [题目分析] 卷积一卷. 然后分块去一段一段的求. O(n)即可. [代码] #include <cstdio> #include <cstring> #include < ...

  8. [POJ1664] 放苹果 (动态规划,组合数学)

    题目描述 把M个同样的苹果放在N个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分发(5,1,1和1,1,5是同一种方法) 输入输出格式 输入格式: 第一行是测试数据的数目t(0 <= ...

  9. 【BZOJ3991】寻宝游戏(虚树,DFS序,splay)

    题意:求在树中从任意点开始,经过若干个关键点回到原点的最小距离 要求支持在线将某个点设置(取消)为关键点,以及询问答案 n,m<=100000 len[i]<=10^9 思路:显然是一个虚 ...

  10. C++练习,计算间隔天数

    time_t time(time_t *time) 从1970年1月1日到到现在的秒数,如果系统时间不存在,返回1char *ctime(const time_t *time) 返回以:day mon ...