.NET Core:處理全局異常

一、前言

在程序設計中,我們會遇到各種各樣的異常問題,一個好的異常處理解決方案能夠幫助開發者快速的定位問題,也能夠給用戶更好的用戶體驗。那麼我們在AspNetCore中該如何捕獲和處理異常呢?我們以一個WebApi項目爲例,講解如何捕獲和處理異常。

二、異常處理

1、異常處理

開發過ASP.NET程序的人都知道:IExceptionFilter。這個過濾器同樣在AspNetCore中也可以用來捕獲異常。不過,對於使用IExceptionFilter,更建議使用它的異步版本:IAsyncExceptionFilter。那麼該如何使用過濾器呢?下面以IAsyncExceptionFilter爲例,對於同步版本其實也是一樣的。

我們在項目中添加一個Model文件夾,存放返回結果實體類,這裏定義一個泛型類:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ExceptionDemo.Model
{
    public class ResultModel<T>
    {
        /// <summary>
        /// 返回結果編碼 0:失敗 1:成功
        /// </summary>
        public int ResultCode { get; set; }

        /// <summary>
        /// 返回結果內容 成功:Success  失敗:異常內容
        /// </summary>
        public string ResultMsg { get; set; }

        /// <summary>
        /// 返回結果 成功:返回T類型數據 失敗:默認null
        /// </summary>
        public T ResultData { get; set; }
    }
}

 

我們在項目中添加一個Filter文件夾,所有的過濾器都放在該文件夾下面。然後添加一個類:CustomerExceptionFilter,並使該類繼承自IAsyncExceptionFilter。代碼如下:

using ExceptionDemo.Model;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Newtonsoft.Json;
using System.Threading.Tasks;

namespace ExceptionDemo.Filter
{
    /// <summary>
    /// 自定義異常過濾器
    /// </summary>
    public class CustomerExceptionFilter : IAsyncExceptionFilter
    {
        /// <summary>
        /// 重寫OnExceptionAsync方法,定義自己的處理邏輯
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public Task OnExceptionAsync(ExceptionContext context)
        {
            // 如果異常沒有被處理則進行處理
            if(context.ExceptionHandled==false)
            {
                // 定義返回類型
                var result = new ResultModel<string>
                {
                    ResultCode = 0,
                    ResultMsg = context.Exception.Message
                };
                context.Result = new ContentResult
                {
                    // 返回狀態碼設置爲200,表示成功
                      StatusCode = StatusCodes.Status200OK,
                      // 設置返回格式
                      ContentType="application/json;charset=utf-8",
                      Content=JsonConvert.SerializeObject(result)
                };
            }
            // 設置爲true,表示異常已經被處理了
            context.ExceptionHandled = true;
            return Task.CompletedTask;
        }
    }
}

上面的代碼很簡單,我們新建了一個自定義的異常過濾器,然後在OnExceptionAsync方法中定義自己的處理邏輯,報錯之後依然讓http返回狀態碼爲200,並且將錯誤信息返回到客戶端。

然後添加一個控制器,命名爲ExceptionFilter,在控制器中模擬發生異常的情況:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ExceptionDemo.Model;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace ExceptionDemo.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ExceptionFilterController : ControllerBase
    {
        [HttpGet]
        public async Task<ResultModel<int>> Get()
        {
            int i = 0;
            int k = 10;
            // 這裏會發生異常
            int j = await Task.Run<int>(() => 
            {
                return k / i;
            });


            return new ResultModel<int>()
            {
                ResultCode=1,
                ResultMsg="Success",
                ResultData=j
            };
        }
    }
}

最後我們需要把自定義的異常過濾器進行注入,這裏選擇使用全局注入的方式,在Startup類的ConfigureServices方法中進行注入:

services.AddControllers(options => 
{
      options.Filters.Add(new CustomerExceptionFilter());
});

然後運行程序,查看結果:

如何我們沒有使用過濾器捕獲和處理異常,我們將得到Http狀態碼爲500的內部錯誤,這種錯誤不方便定位問題,而且給客戶端返回的信息也不夠友好。使用了過濾器處理異常,進行特殊處理之後就會顯得很友好了。

在上面自定義過濾器的代碼中,有下面的一行代碼:

context.ExceptionHandled = true;

注意:這句代碼很關鍵,當你處理完異常之後,一定要將此屬性更改爲true,表示異常已經處理過了,這樣其他地方就不會在處理這個異常了。 

2、使用中間件處理異常

我們知道,AspNetCore的管道模型具有層層傳遞的特點,那麼我們就可以在管道中實現全局異常捕獲。我們新創建一個自定義的異常中間件:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Text.Json;
using System.Threading.Tasks;

namespace ExceptionDemo.Middleware
{
    /// <summary>
    /// 自定義異常中間件
    /// </summary>
    public class CustomerExceptionMiddleware
    {
        /// <summary>
        /// 委託
        /// </summary>
        private readonly RequestDelegate _next;

        public CustomerExceptionMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            try
            {
                await _next(context);
            }
            catch (Exception ex)
            {

                context.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
                };

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

然後在新建一個擴展方法:

using Microsoft.AspNetCore.Builder;

namespace ExceptionDemo.Middleware
{
    /// <summary>
    /// 靜態類
    /// </summary>
    public static class ExceptionMiddlewareExtension
    {
        /// <summary>
        /// 靜態方法
        /// </summary>
        /// <param name="app">要進行擴展的類型</param>
        public static void UseExceptionMiddleware(this IApplicationBuilder app)
        {
            app.UseMiddleware(typeof(CustomerExceptionMiddleware));
        }
    }
}

最後在Startup類的Configure方法中使用自定義的異常中間件:

app.UseExceptionMiddleware();

然後我們註釋掉上面註冊的異常過濾器,運行程序進行訪問:

這樣也可以捕獲到異常。

3、使用框架自帶異常中間件

 我們首先看下面一段代碼:

if (env.IsDevelopment())
{
       app.UseDeveloperExceptionPage();
}

這段代碼在我們使用AspNetCore創建一個WebApi項目時就會看到,如果是創建的MVC項目,是下面一段代碼:

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

這兩段代碼的作用就是捕獲和處理異常,是第一個被添加到管道中的中間件。

UseDeveloperExceptionPage的意思很好理解:對於開發模式,一旦報錯就跳轉到錯誤堆棧頁面。而第二個UseExceptionHandler也很有意思,從它的名字中我們大致可以猜出它肯定是個錯誤攔截程序。那麼它和上面自定義的異常處理中間件有什麼區別呢?

UseExceptionHandler其實就是默認的錯誤處理。它其實也是一箇中間件,它的原名叫做ExceptionHandlerMiddleware。在使用UseExceptionHandler方法時,我們可以選填各種參數。比如上面的第二段代碼,填入了“/Error”參數,表示當產生異常的時候,將定位到對應的路徑,這裏定位的頁面就是“http://localhost:5001/Error”。這是MVC中自帶的一個錯誤頁面,當然,你也可以指定自己定義的一個頁面。

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

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

ExceptionHandler允許我們在ExceptionHandlerMiddleware內部指定咱們自己的異常處理邏輯。而該參數的類型爲RequestDelegate類型的委託。因此,UseExceptionHandler提供了一個簡便的寫法,可以讓我們在ExceptionHandlerMiddleware中新建自定義的錯誤攔截管道來處理異常:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using ExceptionDemo.Filter;
using ExceptionDemo.Middleware;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace ExceptionDemo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            #region 註冊全局異常過濾器
            //services.AddControllers(options => 
            //{
            //    options.Filters.Add(new CustomerExceptionFilter());
            //});
            #endregion

            services.AddControllers();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler(builder => builder.Use(ExceptionHandlerDemo));
            }

            
            app.UseExceptionMiddleware();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }

        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);
            }
        }
    }
}

三、中間件和過濾器的比較

在上面的例子中,我們分別使用了中間件和過濾器的方式來處理異常,那麼中間件和過濾器有什麼區別呢?兩者的區別:攔截範圍的不同。

IExceptionFilter作爲一種過濾器,它需要在控制器發現錯誤之後將錯誤信息提交給它處理,因此它的異常處理範圍是控制器內部。如果我們想捕獲進入控制器之前的一些錯誤,IExceptionFilter是捕獲不到的。而對於ExceptionHandlerMiddleware異常中間件來說就很容易了,它作爲第一個中間件被添加到管道中,在它之後發生的任何異常都可以捕獲的到。

那麼爲什麼要有兩種異常處理的方式呢?只使用ExceptionHandlerMiddleware中間件處理異常不可以嗎?它可以捕獲任何時候發生的異常,爲什麼還要有過濾器呢?如果你想在控制器發生異常時快速捕獲和處理異常,那麼使用過濾器處理異常是非常不錯的選擇。如果是控制器內部發生了異常,首先是由過濾器捕獲到異常,最後纔是中間件捕獲到異常。

我們在自定義過濾器的時候有這樣一段代碼:context.ExceptionHandled = true;如果在自定義過濾器中將異常標記爲已經處理之後,則第一個異常處理中間件就認爲沒有錯誤了,不會進入到處理邏輯中了。所以,如果不把 ExceptionHandled屬性設置爲true,可能出現異常處理結果被覆蓋的情況。

GitHub代碼:[email protected]:jxl1024/ExceptionDemo.git

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