背景#

最近在徒手造轮子,编写一个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的内容的文章就介绍到这了,更多相关asp.net core自定义中间件读取request.body与response.body内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!