【5min+】AspNet Core中的全局異常處理

系列介紹

【五分鐘的dotnet】是一個利用您的碎片化時間來學習和豐富.net知識的博文系列。它所包含了.net體系中可能會涉及到的方方面面,比如C#的小細節,AspnetCore,微服務中的.net知識等等。
5min+不是超過5分鐘的意思,"+"是知識的增加。so,它是讓您花費5分鐘以下的時間來提升您的知識儲備量。

正文

其實一說到AspNet Core裏面的全局異常,其實大家都不會陌生。因爲這玩意兒用的非常頻繁,好的異常處理方案能夠幫助開發者更快速的定位問題,也能夠給用戶更好的用戶體驗。

比如,當您訪問到一個網頁,突然,它喵的報錯了!您沒有看錯,它報錯了!!!然後顯示了這樣的一個錯誤頁面:

x

請問,此刻電腦屏幕前的您會什麼感受。(真想掏出那傳說中的95級史詩巨劍!

x

但是,假若我們稍微處理一下這個異常,比如用咱們騰訊爸爸的手段,換個皮膚:
x

用戶馬上就會想:“哎呀,錯誤就錯誤嘛,孰能無過,程序員鍋鍋也挺辛苦的。”

由此可見!!!全局異常的捕獲和處理是有多麼的重要。

AspNet Core 中的全局處理

IAsyncExceptionFilter

那麼在AspNet Core中我們該如何捕獲和處理異常呢? 可能很多同學都知道:IExceptionFilter 。 這個過濾器應該算是AspNet裏面的老牌過濾器了,從很早就延續至今,它允許咱們捕獲AspNet Core的控制器中的錯誤。不過,對於使用 IExceptionFilter,其實我更建議您考慮它的異步版本: IAsyncExceptionFilter。(別問爲什麼,問就是愛的供養)。

那麼我們來看看該過濾器是怎麼使用的呢? 下面以 IAsyncExceptionFilter 爲例,對於同步版本其實也是一樣的:

public class MyCustomerExceptionFilter : IAsyncExceptionFilter
{
    public Task OnExceptionAsync(ExceptionContext context)
    {
        if (context.ExceptionHandled == false)
        {
            string msg = context.Exception.Message;
            context.Result = new ContentResult
            {
                Content = msg,
                StatusCode = StatusCodes.Status200OK,
                ContentType = "text/html;charset=utf-8"
            };
        }
        context.ExceptionHandled = true; //異常已處理了

        return Task.CompletedTask;
    }
}

上面咱們新建了一個自定義的異常過濾器,代碼很簡單,就是報錯了之後依舊讓Http返回狀態碼爲200的結果。並且將錯誤信息返回到客戶端。

然後還需要在 Startup.cs 中,告訴 MVC 咱們新加的這個過濾器:

services.AddControllers(options => options.Filters.Add(new MyCustomerExceptionFilter()));

然後就完了,是不是so easy? 來看看結果:

 [HttpGet]
public IEnumerable<WeatherForecast> Get()
{
    throw new Exception("has error!");
}

x

如果不增加該過濾器,我們將得到Http狀態碼爲500的響應。這對於某些不致命的意外操作來說,有點殺雞用牛刀的感覺,對於前端用戶來說也不是很友好(明明輸錯了一個字符,就直接被告知網站崩潰,並且出現喬殿下)。

而咱們捕獲了異常,進行特殊處理之後就顯得很友好了。(返回200,並且告訴用戶輸錯了某字符等)。

在上面的代碼中,您會看到有一行 context.ExceptionHandled = true;注意!!! 這很關鍵,當您處理完異常之後,請記得將此屬性更改爲true,表明異常已經處理過了。如果不更改的話,嘿嘿🤪。會有什麼結果呢? 請看下面↓

中間件處理異常

由於AspNet Core管道的層層傳遞的特點,咱們就有機會在管道中實現全局異常捕獲。 新建一箇中間件來試試吧:

public class MyExceptionMiddleware
{
    private readonly RequestDelegate _next;
    public MyExceptionMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    public async Task Invoke(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (Exception ex)
        {
            httpContext.Response.ContentType = "application/problem+json";

            var title = "An error occured: " + ex.Message;
            var details = ex.ToString();

            var problem = new ProblemDetails
            {
                Status = 200,
                Title = title,
                Detail = details
            };

            //Serialize the problem details object to the Response as JSON (using System.Text.Json)
            var stream = httpContext.Response.Body;
            await JsonSerializer.SerializeAsync(stream, problem);
        }
    }
}

然後在 Startup.cs 中,註冊管道:

app.UseMiddleware<MyExceptionMiddleware>();

來看看效果:

x

還是原來的味道,還是熟悉的配方,爽歪歪!

管道的添加順序決定了它的執行順序,所以如果您想擴大異常捕獲的範圍,可以將該管道放置在 Configure 的第一行。 但是!! 您會發現,這個默認的AspNet Core項目不是已經在第一行弄了一個異常處理麼? 我*&&……&。

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
}

這行代碼大家在初始化新AspNetCore項目時就會看到,也有可能您只有上半段,這和模板有關係。不過這都沒有關係,它的作用就是捕獲和處理異常而已。關於 UseDeveloperExceptionPage 該擴展咱們就不多說了,它的意思是:對於開發模式,一旦報錯就會跳轉到錯誤堆棧頁面。 而第二個 UseExceptionHandler 就很有意思了,從它命名就可以看出,它肯定是個錯誤攔截程序。那麼它和咱們自定義的異常處理管道有什麼區別呢?

“不指定肯定有個默認吧!” 是的,它就是默認的錯誤處理。所以,它其實也是一箇中間件,它的真身叫做 ExceptionHandlerMiddleware。在使用 UseExceptionHandler 方法時,我們可以選填各種參數。比如上方的代碼,填入了 "/Error" 參數,表示當產生異常的時候,將定向到對應路徑,此處就定位的是: “http://localhost:5001/Error” 。當然您也可以隨意指定頁面,比如 漂亮的喬殿下頁面。😝

還有指定 ExceptionHandlerOptions 參數的方法,該參數是ExceptionHandlerMiddleware中間件的重要參數:

參數名 說明
ExceptionHandlingPath 重定向的路徑,比如剛纔的 “”/Error"" 實際上就是指定的該參數
ExceptionHandler 錯誤攔截處理程序

ExceptionHandler 允許我們在 ExceptionHandlerMiddleware 內部指定咱們自有的異常處理邏輯。而該參數的類型爲 RequestDelegate,是否很眼熟,沒錯,管道處理!因此UseExceptionHandler 提供了一個簡便的寫法,可以讓我們在ExceptionHandlerMiddleware 中又新建自定義的錯誤攔截管道來作爲處理程序:

//in Configure()
app.UseExceptionHandler(appbuilder => appbuilder.Use(ExceptionHandlerDemo));

//該內容會在AspNetCore的管道返回結果至ExceptionHandlerMiddleware時,如果中間件捕獲到了異常時調用
private async Task ExceptionHandlerDemo(HttpContext httpContext, Func<Task> next)
{
    //該信息由ExceptionHandlerMiddleware中間件提供,裏面包含了ExceptionHandlerMiddleware中間件捕獲到的異常信息。
    var exceptionDetails = httpContext.Features.Get<IExceptionHandlerFeature>();
    var ex = exceptionDetails?.Error;

    if (ex != null)
    {
        httpContext.Response.ContentType = "application/problem+json";

        var title = "An error occured: " + ex.Message;
        var details = ex.ToString();

        var problem = new ProblemDetails
        {
            Status = 500,
            Title = title,
            Detail = details
        };

        var stream = httpContext.Response.Body;
        await JsonSerializer.SerializeAsync(stream, problem);
    }
}

管道 VS 過濾器

那麼上面兩個方法有什麼區別呢? 回答:攔截範圍。

x

IExceptionFilter 作爲MVC中間件之間的內容,它需要MVC在發現錯誤之後將錯誤信息提交給它處理,因此它的錯誤處理範圍僅限於MVC中間件。所以,假如我們需要捕獲MVC中間件之前的一些錯誤,其實是捕獲不到的。 而對於ExceptionHandlerMiddleware中間件來說就很簡單了,它作爲第一個中間件,凡是在它之後的所有錯誤它都能夠捕獲得到。

那麼這麼看來是否IExceptionFilter就毫無用武之地了呢? 非也,假如您想在MVC發生異常時快速捕獲和處理,使用過濾器其實是您不錯得選擇,如果您僅僅關心控制器之間的異常,那麼過濾器也是很好的選擇。

還記得剛開始我們在過濾器中說過的這一行代碼嗎:context.ExceptionHandled = true;。如果在IExceptionFilter中將異常標記爲已經處理之後,則第一道異常處理中間件就認爲沒有錯誤了,不會進入到處理邏輯中。所以,如果咱們不把該屬性改爲 true,很有可能出現攔截結果被覆蓋的情況。

最後,偷偷說一句:創作不易,點個推薦吧…

x

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章