1. 程式人生 > 其它 >.NET Core:處理全域性異常

.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,可能出現異常處理結果被覆蓋的情況。