需求 根据项目需要,要为WebApi实现一个ExceptionFilter,不仅要将WebApi执行过程中产生的异常信息进行收集,还要把WebApi的参数信息进行收集,以方便未来定位问题。
问题描述 对于WepApi的参数,一部分是通过URL获取,例如Get请求。对于Post或Put请求,表单数据是保存在Http请求的Body中的。基于此,我们可以在ExceptionFilter中,通过ExceptionContext参数,获取当前Http请求的Body数据。考虑到Body是Stream类型,读取方法如下:
public override async Task OnExceptionAsync(ExceptionContext context){ var httpContext = context.HttpContext; var request = httpContext.Request; StreamReader sr = new StreamReader(request.Body); string body = await sr.ReadToEndAsync(); } } 1 2 3 4 5 6 7 很遗憾,上面的代码读取到的Body数据为空。后来将代码移到ActionFilter,读取到的Body数据依然为空。最后将代码移到Middleware中,读取到的Body数据还是空。
问题解决 解决方案 结合Github和Stackflow类似问题的分析,得到解决方案如下,具体原因集分析请参看问题分析章节。
在Startup.cs中定义Middleware,设置缓存Http请求的Body数据。代码如下。自定义Middleware请放到Configure方法的最前面。 app.Use(next => new RequestDelegate( async context => { context.Request.EnableBuffering(); await next(context); } )); 1 2 3 4 5 6 在Filter或Middleware中,读取Body关键代码如下。 public override async Task OnExceptionAsync(ExceptionContext context){ var httpContext = context.HttpContext; var request = httpContext.Request; request.Body.Position = 0; StreamReader sr = new StreamReader(request.Body); string body = await sr.ReadToEndAsync(); request.Body.Position = 0; } }
1 2 3 4 5 6 7 8 9 10 注意事项 Body在ASP.NET Core 的Http请求中是以Stream的形式存在。 首行Request.Position = 0,表示设定从Body流起始位置开始,读取整个Htttp请求的Body数据。 最后一行Request.Position = 0, 表示在读取到Body后,重新设置Stream到起始位置,方便后面的Filter或Middleware使用Body的数据。 在读取Body的时候,请尽量使用异步方式读取。ASP.NET Core默认是不支持同步读取的,会抛出异常,解决方法如下: Startup.cs文件中的ConfigureServices方法中添加以下代码 services.Configure<KestrelServerOptions>(options => { options.AllowSynchronousIO = true; }); 1 2 3 4 Startup.cs文件中,增加Using引用。 using Microsoft.AspNetCore.Server.Kestrel.Core; 1 异步处理(async/await)本来就是ASP.NET Core的重要特性,因此我也是推荐使用异步方式读取Body的Stream流中的数据。
问题分析 当前的解决方案,相比于最初始的代码,增加了两点:
EnableBuffering(HttpRequest)方法调用,该方法会将当前请求的Body数据缓存下来。 在读取Http请求的Body流时候,设置从起始位置开始读取数据。 下面我们通过如下实验,来验证上述解决方案。我们的准备工作如下:
准备一个Middleware,放到所有Middleware之前执行,读取Http Post请求的body。 准备一个ActionFiler(异步),读取Http Post请求的body。 准备一个ExceptionFilter(异步),读取Http Post请求的body。 准备一个含有分母为0的异常的Action,该Action对应一个Post请求,含有一个Club类型参数,Club是一个对足球俱乐部的描述类。 实验1 我们在代码中,不调用EnableBuffering(HttpRequest)方法。因为不调用该扩展方法,Request.Position = 0这句会抛出异常如下,因此将该句也略去,完整代码以及Action参数设定请见附录实验1。
System.NotSupportedException: Specified method is not supported. at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpRequestStream.set_Position(Int64 value) at SportsNews.Web.Middlewares.ExceptionMiddleware.InvokeAsync(HttpContext httpContext) in D:\project\SportsNews\SportsNews.Web\Middlewares\ExceptionMiddleware.cs:line 33 at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context) 1 2 3 4 实验结果: 控制台执行结果:
Postman返回结果:
实验结果分析 理论上代码执行路线应该是
Middleware -> Model Binding -> ActionExecuting Filter -> Action -> Exception Filter -> ActionExecuted Filter
从控制台的显示结果来看,Action Filter和Exception Filter的代码并没有被执行。分母为0的异常也并未抛出。
根据MS提供的ASP.NET Core Http 请求的流程和Postman的请求相应,显然,异常是在数据绑定阶段(Model Binding)抛出的。
原因就是在不执行EnableBuffering(HttpRequest)来缓存Body的情况下,Body只能被读取一次。
而这一次在我们定义的Middleware中已经使用了,所以在后面的数据绑定阶段(Model Binding),MVC的应用程序在从Body中读取数据,反序列化成具体的对象,作为Action的参数时候,读取失败了。因为此时Body中读取到数据为空,Postman显示解析的表单JSON数据失败。
实验2 在实验1的middleware中增加EnableBuffering(HttpRequest)的调用,但是在所有代码中读取Http请求的Body后,不重置Body流到起始位置,即不增加Request.Position = 0这句。
其他代码准备同实验1,完整代码以及Action参数设定请见附录实验2。
实验2的执行结果和实验1相同,控制台和Postman的返回结果同实验1完全相同,不再赘述。
实验结果分析 虽然我们缓存了Http请求中的Body,但是没有正确使用Body流,没有在代码中将Body流设置到起始位置,再进行读取。所以实验结果表现出来的还是Body只能读一次。
实验3 在实验2的基础上,每次读取完Http请求的Body后,增加Body流重置到初始位置的代码,具体代码参见附录实验3代码。
实验3基本符合我们的预期,除了ActionExecuting Filter没有读取到Body,其他Filter, Action和Middleware全部获取到Body数据,分母为0的异常已经抛出,具体如下:
控制台:
Postman:
为什么ActionExecuting Filter没有读取到Body没有读取到Body,根据MS提供的ASP.NET Core Http 请求的流程,我们的代码执行顺序应该是这样:
Middleware -> Model Binding -> ActionExecuting Filter -> Action -> Exception Filter -> ActionExecuted Filter
在我们自定义的Middleware中,我们使用完Body,进行了重置操作,所以Model Binding阶段没有出现实验1和2中出现的异常。但是Model Binding阶段MVC应用程序会读取请求的Body,但是读取完后,没有执行重置操作。所以 在ActionExecuting Filter中没有读到Body。
但是我们在ActionExecuting Filter中进行了重置操作,所以后面的Filter可以获取到Body。
基于此,所以我们文中开始时候的解决方案,重置操作时在读取Body前和读取Body后都做的。
对于在哪缓存Http请求的Body的问题,根据MS提供的如下Http请求流程图,我建议是放到所有的Middleware之前自定义Middleware并调用EnableBuffering(HttpRequest)方法,以保证后面的Middleware, Action或Filter都可以读取到Body。
附录 实验1代码 Action代码 [CustomerActionFilter] [CustomerExceptionFilterAttribute] [HttpPost("checkerror/{Id:int}")] public IActionResult GetError2 ([FromBody] Club club) { var a = 1; var b = 2; var c = 3; var d = c / (b-a*2); return Ok (d); } 1 2 3 4 5 6 7 8 9 10 参数Club的定义: public class Club { public int Id { get; set; } public string Name { get; set; } public string City { get; set; }
[Column (TypeName = "date")] public DateTime DateOfEstablishment { get; set; } public string History { get; set; } public League League { get; set; } public int LeagueId { get; set; } } 1 2 3 4 5 6 7 8 9 10 11 Postman请求参数:
{ "Id" : 10, "Name" : "Real Madrid", "City" : "Madrid", "History" : "Real Madrid has long history", "DateOfEstablishment" : "1902-03-06", "LeagueId":13 } 1 2 3 4 5 6 7 8 9 Middleware 代码:
public class ExceptionMiddleware { public RequestDelegate _next { get; } public string body { get; private set; } public ExceptionMiddleware(RequestDelegate next) { this._next = next; } public async Task InvokeAsync(HttpContext httpContext){ var request = httpContext.Request; using (StreamReader reader = new StreamReader (request.Body, Encoding.UTF8, true, 1024, true)) { body = await reader.ReadToEndAsync(); System.Console.WriteLine("This is ExceptionMiddleware. Body is " + body); } await _next(httpContext); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Exception Filter的代码: public class CustomerExceptionFilter: ExceptionFilterAttribute { public CustomerExceptionService _exceptionService { get; }
public CustomerExceptionFilter( CustomerExceptionService exceptionService, IHttpContextAccessor accessor){ this._exceptionService = exceptionService ?? throw new ArgumentNullException(nameof(exceptionService)); } public override async Task OnExceptionAsync(ExceptionContext context){ var httpContext = context.HttpContext; var request = httpContext.Request; StreamReader sr = new StreamReader(request.Body); string body = await sr.ReadToEndAsync(); System.Console.WriteLine("This is OnExceptionAsync."); System.Console.WriteLine("Request body is " + body); if (!context.ExceptionHandled) { context.Result = new JsonResult(new { Code = 501, Msg = "Please contract Administrator." }); } } } public class CustomerExceptionFilterAttribute : TypeFilterAttribute{ public CustomerExceptionFilterAttribute (): base(typeof(CustomerExceptionFilter)){ } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 Action Filter的代码:
public class CustomerActionFilterAttribute: ActionFilterAttribute { public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next){ // before Action var httpContext = context.HttpContext; var request = httpContext.Request; StreamReader sr = new StreamReader(request.Body); string body = await sr.ReadToEndAsync(); System.Console.WriteLine("This is OnActionExecuting."); System.Console.WriteLine("Request body is " + body);
//Action await next();
// after Action //request.Body.Position = 0; StreamReader sr2 = new StreamReader(request.Body); body = await sr2.ReadToEndAsync(); System.Console.WriteLine("This is OnActionExecuted."); System.Console.WriteLine("Request body is " + body); // request.Body.Position = 0; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 实验2代码 Middleware代码:
public class ExceptionMiddleware { public RequestDelegate _next { get; } public string body { get; private set; } public ExceptionMiddleware(RequestDelegate next) { this._next = next; } public async Task InvokeAsync(HttpContext httpContext){ var request = httpContext.Request; request.EnableBuffering(); StreamReader reader = new StreamReader (request.Body) ; string body = await reader.ReadToEndAsync(); System.Console.WriteLine("This is ExceptionMiddleware. Body is " + body); await _next(httpContext); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 实验3代码 Middleware 代码:
public class ExceptionMiddleware { public RequestDelegate _next { get; } public string body { get; private set; } public ExceptionMiddleware(RequestDelegate next) { this._next = next; } public async Task InvokeAsync(HttpContext httpContext){ var request = httpContext.Request; request.EnableBuffering(); StreamReader reader = new StreamReader (request.Body) ; string body = await reader.ReadToEndAsync(); request.Body.Position = 0; System.Console.WriteLine("This is ExceptionMiddleware. Body is " + body); await _next(httpContext); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Exception Filter的代码: public class CustomerExceptionFilter: ExceptionFilterAttribute { public CustomerExceptionService _exceptionService { get; }
public CustomerExceptionFilter( CustomerExceptionService exceptionService, IHttpContextAccessor accessor){ this._exceptionService = exceptionService ?? throw new ArgumentNullException(nameof(exceptionService)); } public override async Task OnExceptionAsync(ExceptionContext context){ var httpContext = context.HttpContext; var request = httpContext.Request; StreamReader sr = new StreamReader(request.Body); string body = await sr.ReadToEndAsync(); request.Body.Position = 0; System.Console.WriteLine("This is OnExceptionAsync."); System.Console.WriteLine("Request body is " + body); if (!context.ExceptionHandled) { context.Result = new JsonResult(new { Code = 501, Msg = "Please contract Administrator." }); } } } public class CustomerExceptionFilterAttribute : TypeFilterAttribute{ public CustomerExceptionFilterAttribute (): base(typeof(CustomerExceptionFilter)){ } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 Action Filter的代码:
public class CustomerActionFilterAttribute: ActionFilterAttribute { public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next){ // before Action var httpContext = context.HttpContext; var request = httpContext.Request; StreamReader sr = new StreamReader(request.Body); string body = await sr.ReadToEndAsync(); request.Body.Position = 0; System.Console.WriteLine("This is OnActionExecuting."); System.Console.WriteLine("Request body is " + body);
//Action await next();
// after Action //request.Body.Position = 0; StreamReader sr2 = new StreamReader(request.Body); body = await sr2.ReadToEndAsync(); request.Body.Position = 0; System.Console.WriteLine("This is OnActionExecuted."); System.Console.WriteLine("Request body is " + body); // request.Body.Position = 0; } }
原文链接:https://blog.csdn.net/weixin_43263355/article/details/107980799
|
请发表评论