文章名称: 如何在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

Copy
	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

Copy
    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之后,控制台会返回第一个需要处理的错误。

Copy
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的内容了。

Copy
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.

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

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

进一步完善代码#

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

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

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

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

Copy
    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错误, 错误信息

Copy
A non-empty request body is required.

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

Copy
    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指针重置

Copy
    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之后,你会得到以下结果。

错误原因:

Copy
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方法。

下面我们修改代码

Copy
    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请求被正确的处理了。

作者:Lamond Lu

出处:https://www.cnblogs.com/lwqlun/p/10954936.html

版权:本站使用「署名 4.0 国际」创作共享协议,转载请在文章明显位置注明作者及出处。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

随机推荐

  1. 韦东山freeRTOS系列教程之【第十章】软件定时器(software timer)

    目录 系列教程总目录 概述 10.1 软件定时器的特性 10.2 软件定时器的上下文 10.2.1 守护任务 10.2.2 守护任务的调度 10.2.3 回调函数 10.3 软件定时器的函数 10.3 ...

  2. debian12 笔记

    前言 最近在win10通过wsl安装了debian linux子系统(wsl2安装报错了..所以改成了wsl),没想到安装的还是最新的debian12 (Bookworm).的确和ubuntu有些不一 ...

  3. 通过vscode写博客

    通过Vscode写博客到博客园 前言 在以前的写作方式都是通过博客园内置的markdown进行工作,但是在实际使用过程中,感觉不是很方便,所以找到了用VSCode插件写作的方法. 所需插件 博客园Cn ...

  4. yb课堂 VSCODE编译器和开发环境搭建 《二十五》

    前端编辑器 vscode:免费开源的现代化轻量级代码编辑器,支持大部分主流的开发语言的语法高亮.智能代码补全.自定义热键.代码对比DIFF.GIT等特性,支持插件扩展,软件跨平台支持Win.Mac以及 ...

  5. 【原创软件】第2期:CAD文字快速批量替换工具CFR(CAD_FastReplace_V4)

    01 背景 由于工作需要,开发了一套CAD文字快速批量替换软件CFR.主要目的是:实现dwg文件一次性完成单对/多对词组快速批量替换. 02 主要功能特色 (1)无需打开CAD,快速实现文字批量替换. ...

  6. 精品 IDEA 插件大汇总!值得收藏

    轻松提高 Java 开发效率 俗话说,工欲善其事,必先利其器.想要提升编程开发效率,必须选择一款顺手的开发工具. 对于 Java 开发者,JetBrains IDEA 无疑是目前最主流的开发工具,既简 ...

  7. UE中返回值为数组的时候,无法传递Reference的问题

    我如果要修改一个类或者结构体的成员变量, 那么我需要通过函数返回 也就是说Struct目前不能传递引用,只能传递备份

  8. 自己在本地搭建 git 版本仓库服务器

    请确保你安装了 git 的图形化工具和 git 软件 首先先创建一个目录作为你的项目工程目录,比如 e:/gitTest 其次右键 git init. 然后指定一个 git 服务器目录,例如:e:/g ...

  9. 写写Redis十大类型bitmap的常用命令

    其实这些命令官方上都有,而且可读性很强,还有汉化组翻译的http://redis.cn/commands.html,不过光是练习还是容易忘,写一写博客记录一下 bitmap 位图,是由0和1状态表现的 ...

  10. 【Layui】14 代码修饰器 CodeDecorator

    文档地址: https://www.layui.com/demo/code.html 基本案例: <pre class="layui-code">//在里面存放任意的代 ...