如何在ASP.NET Core自定义中间件中读取Request.Body和Response.B
背景最近在徒手造轮子,编写一个ASP.NET Core的日志监控器,其中用到了自定义中间件读取Request.Body和Response.Body的内容,但是编写过程,并不像想象中的一帆风顺,ASP.NET Core针对Request.Body和Response.Body的几个特殊设计,导致了完成以上功能需要绕一些弯路。 原始代码为了读取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属性的
这里看似的无解,但是我们可以转换一下思路,既然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; } }
重新启动程序,请求/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; } }
至此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对象中的
下面我们修改代码 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请求被正确的处理了。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
- Linux从创建到部署ASP.NET Core项目-----使用阿里云(Centos
- 如何在asp.net Web应用程序(C#)中制作倒数计时器?
- asp.net-mvc – 如何在asp mvc中阻止访问Internet Explorer
- asp.net mvc – 如何实现面包屑助手在asp.net mvc?
- asp.net-mvc – Ninject和连接字符串
- asp.net-mvc – 如何将OpenId与ASP.Net成员集成在MVC中
- asp.net-mvc-3 – 淘汰赛和全球化
- 在F#中开发ASP.NET和ASP.NET MVC应用程序的缺点?
- asp.net – 将样式应用于CheckBoxList中的ListItems
- asp.net – WebMethod以JSON格式返回值
- asp.net-core – 如何编写包含其他标记帮助程序的
- asp.net – 通过MSMQ分离Web和数据库层是必要的还
- asp.net-mvc – asp.net mvc区域的默认页面
- asp.net-mvc-3 – SelectList不显示所选项目
- asp.net-mvc-5 – 在MVC5中启用和使用角色管理
- ASP.NET MVC通过ActionLink传递模型
- asp.net-mvc – ASP.NET MVC动态生成的图像URL
- asp.net-mvc – 使用带有IEnumerable的Html.Edit
- .net – 你用你的单元测试测试什么?
- castle-windsor – 如何在ASP.NET 4 RC WebAPI中