在本文中,我將講解如何通過自定義ExceptionHandlerMiddleware,以便在中間件管道中發生錯誤時創建自定義回應,而不是提供一個“重新執行”管道的路徑,
作者:依樂祝
譯文:https://www.cnblogs.com/yilezhu/p/12497937.html
原文:https://andrewlock.net/creating-a-custom-error-handler-middleware-function/
Razor頁面中的例外處理
所有的.NET應用程式都有可能會產生錯誤,并且不幸地引發例外,因此在ASP.NET中間件管道中處理這些例外顯得非常重要,服務器端呈現的應用程式(如Razor Pages)通常希望捕獲這些例外并重定向到一個錯誤頁面,
例如,如果您創建一個使用Razor Pages(dotnet new webapp)的新Web應用程式,您將在Startup.Configure中看到如下的中間件配置:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
// .. other middleware not shown
}
在Development環境中運行時,應用程式將捕獲處理請求時引發的所有例外,并使用一個非常有用的DeveloperExceptionMiddleware方法將其以網頁的形式進行顯示:

這在本地開發期間非常有用,因為它使您可以快速檢查堆疊跟蹤,請求標頭,路由詳細資訊以及其他內容,
當然,這些都是您不想在生產中公開的敏感資訊,因此,當不在開發階段時,我們將使用其他例外處理程式ExceptionHandlerMiddleware,此中間件允許您提供一個請求路徑,默認情況下是"/Error",并使用它“重新執行”中間件管道,以生成最終回應:

Razor Pages應用程式的最終結果是,每當生產中發生例外時,就會回傳這個Error.cshtml 的Razor 頁面:

這涵蓋了razor 頁面的例外處理,但是Web API呢?
Web API的例外處理
Web API模板(dotnet new webapi)中的默認例外處理類似于Razor Pages使用的例外處理,但有一個重要的區別:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// .. other middleware not shown
}
如您所見DeveloperExceptionMiddleware,在Development環境中仍會添加,但是在生產中根本沒有添加錯誤處理!這沒有聽起來那么糟糕:即使沒有例外處理中間件,ASP.NET Core也會在其底層架構中捕獲該例外,將其記錄下來,并向客戶端回傳一個空白的500回應:

如果您正在使用該[ApiController]屬性(你可能應該這樣使用),并且該錯誤來自您的Web API控制器,那么ProblemDetails默認情況下會得到一個結果,或者您可以進一步對其進行自定義,
對于Web API客戶端來說,這實際上還不錯,您的API使用者應能夠處理錯誤回應,因此最終用戶將不會看到上面的“中斷”頁面,但是,它通常不是那么簡單,
例如,也許您使用的是錯誤的標準格式,例如ProblemDetails格式,如果您的客戶期望所有錯誤都具有該格式,那么在某些情況下生成的空回應很可能導致客戶端中斷,同樣,在Development環境中,當客戶端期望回傳JSON時而你回傳一個HTML開發人員例外頁面,這可能會導致問題!
官方檔案中描述了一種解決方案,建議您創建ErrorController并具有兩個終結點的:
[ApiController]
public class ErrorController : ControllerBase
{
[Route("/error-local-development")]
public IActionResult ErrorLocalDevelopment() => Problem(); // Add extra details here
[Route("/error")]
public IActionResult Error() => Problem();
}
然后使用Razor Pages應用程式中使用的相同“重新執行”功能來生成回應:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseExceptionHandler("/error-local-development");
}
else
{
app.UseExceptionHandler("/error");
}
// .. other middleware
}
這可以正常作業,但是對于使用生成例外的同一基礎結構(例如Razor Pages或MVC)來生成例外訊息,總有一些困擾我,由于被第二次拋出例外,我多次被失敗的錯誤回應所困擾!因此,我喜歡采取稍微不同的方法,
使用ExceptionHandler代替ExceptionHandlingPath
當我第一次開始使用ASP.NET Core時,解決此問題的方法是撰寫自己的自定義ExceptionHandler中間件來直接生成回應,“處理例外不是那么難,對吧”?
事實證明,這要復雜得多(我知道,令人震驚),您需要處理各種邊緣情況,例如:
- 如果在發生例外時回應已經開始發送,則您將無法攔截它,
- 如果在
EndpointMiddleware發生例外時已執行,則需要對選定的端點進行一些處理 - 您不想快取錯誤回應
ExceptionHandlerMiddleware處理所有這些情況,所以重新寫你自己的版本不是一條要走的路,幸運的是,盡管通常顯示的方法是為中間件提供重新執行的路徑,但還有另一種選擇-直接提供處理函式,
在ExceptionHandlerMiddleware中有一個ExceptionHandlerOptions引數,該選項物件具有兩個屬性:
public class ExceptionHandlerOptions
{
public PathString ExceptionHandlingPath { get; set; }
public RequestDelegate ExceptionHandler { get; set; }
}
當你向UseExceptionHandler(path)方法提供重新執行的路徑時,實際上是在options物件上設定ExceptionHandlingPath,同樣的,如果需要的話,您可以設定ExceptionHandler屬性,并使用UseExceptionHandler()將ExceptionHandlerOptions的實體直接傳遞給中間件:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseExceptionHandler(new ExceptionHandlerOptions
{
ExceptionHandler = // .. to implement
});
// .. othe middleware
}
另外,您可以使用UseExceptionHandler()的另一個多載方法并配置一個迷你中間件管道來生成回應:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseExceptionHandler(err => err.UseCustomErrors(env)); // .. to implement
// .. othe middleware
}
兩種方法都是等效的,因此更多是關于喜好的問題,在本文中,我將使用第二種方法并實作該UseCustomErrors()功能,
創建自定義例外處理函式
對于此示例,我將假設我們在中間件管道中遇到例外時需要生成一個ProblemDetails的物件,我還要假設我們的API僅支持JSON,這就避免了我們不必擔心XML內容協商等問題,在開發環境中,ProblemDetails回應將包含完整的例外堆疊跟蹤,而在生產環境中,它將僅顯示一般錯誤訊息,
ProblemDetails是回傳HTTP回應中錯誤的機器可讀詳細資訊的行業標準方法,這是從ASP.NET Core 3.x(在某種程度上在2.2版中)的Web API回傳錯誤訊息的普遍支持的方法,
我們將從在靜態幫助器類中定義UseCustomErrors函式開始,該幫助類將一個生成回應的中間件添加到IApplicationBuilder方法擴展中,在開發環境中,它最侄訓呼叫WriteResponse方法,并且設定includeDetails: true,在其他環境中,includeDetails`設定為false,
using System;
using System.Diagnostics;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
public static class CustomErrorHandlerHelper
{
public static void UseCustomErrors(this IApplicationBuilder app, IHostEnvironment environment)
{
if (environment.IsDevelopment())
{
app.Use(WriteDevelopmentResponse);
}
else
{
app.Use(WriteProductionResponse);
}
}
private static Task WriteDevelopmentResponse(HttpContext httpContext, Func<Task> next)
=> WriteResponse(httpContext, includeDetails: true);
private static Task WriteProductionResponse(HttpContext httpContext, Func<Task> next)
=> WriteResponse(httpContext, includeDetails: false);
private static async Task WriteResponse(HttpContext httpContext, bool includeDetails)
{
// .. to implement
}
}
剩下的就是實作WriteResponse方法來生成我們的回應的功能,這將從ExceptionHandlerMiddleware(通過IExceptionHandlerFeature)中檢索例外,并構建一個包含要顯示的詳細資訊的ProblemDetails物件,然后,它使用System.Text.Json序列化程式將物件寫入Response流,
private static async Task WriteResponse(HttpContext httpContext, bool includeDetails)
{
// Try and retrieve the error from the ExceptionHandler middleware
var exceptionDetails = httpContext.Features.Get<IExceptionHandlerFeature>();
var ex = exceptionDetails?.Error;
// Should always exist, but best to be safe!
if (ex != null)
{
// ProblemDetails has it's own content type
httpContext.Response.ContentType = "application/problem+json";
// Get the details to display, depending on whether we want to expose the raw exception
var title = includeDetails ? "An error occured: " + ex.Message : "An error occured";
var details = includeDetails ? ex.ToString() : null;
var problem = new ProblemDetails
{
Status = 500,
Title = title,
Detail = details
};
// This is often very handy information for tracing the specific request
var traceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier;
if (traceId != null)
{
problem.Extensions["traceId"] = traceId;
}
//Serialize the problem details object to the Response as JSON (using System.Text.Json)
var stream = httpContext.Response.Body;
await JsonSerializer.SerializeAsync(stream, problem);
}
}
您可以在序列化ProblemDetails之前記錄從HttpContext中檢索的自己喜歡的任何其他值,
請注意,在呼叫例外處理程式方法之前,
ExceptionHandlerMiddleware會 清除路由值,以使這些值不可用,
如果您的應用程式現在在Development環境中引發例外,則您將在回應中獲取作為JSON回傳的完整例外:

在生產環境中,您仍然會得到ProblemDetails回應,但是省略了詳細資訊:

與MVC /重新執行路徑方法相比,此方法顯然具有一些局限性,即您不容易獲得模型系結,內容協商,簡單的序列化或本地化(取決于您的方法),
如果您需要其中任何一個(例如,也許您使用PascalCase而不是camelCase從MVC進行序列化),那么使用此方法可能比其價值更麻煩,如果是這樣,那么所描述的Controller方法可能是明智的選擇,
如果您不關心這些,那么本文中顯示的簡單處理程式方法可能是更好的選擇,無論哪種方式,都不要嘗試實作自己的版本ExceptionHandlerMiddleware-使用可用的擴展點!??
總結
在這篇文章中,我描述了Razor Pages和Web API的默認例外處理中間件方法,我著重指出了默認Web API模板配置的問題,尤其是在客戶端期望有效JSON的情況下,即使出現錯誤也是如此,
然后,我從官方檔案中展示了建議的方法,該方法使用MVC控制器為API 生成ProblemDetails回應,這種方法效果很好,除非問題出在您的MVC配置本身上,否則嘗試執行ErrorController將會失敗,
作為替代方案,我展示了如何使用ExceptionHandlerMiddleware為生成回應提供定制的例外處理功能,我最后展示了一個示例處理程式,該處理程式將ProblemDetails物件序列化為JSON,包括Development環境中的詳細資訊,并在其他環境中將其排除在外,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/56752.html
標籤:.NET Core
