需求
根據項目需要,要為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
