只有累積,沒有奇蹟

2021年6月13日 星期日

[NETCore] ASP.NET Core 中的例外處理方式

前言
錯誤處理一直是開發中的重要環節之一,如果在程式發生異常錯誤的當下有效的將錯誤訊息完整的記錄下來,可以大大的節省 debug 的時間與效率,反過來如果在開發時沒有考慮到異常處理的機制,可能在發生問題時要找到錯誤的原因難度就會提高,因此在開發時必須要考慮到異常處理的機制,過去公司都會將 exception 訊息透過推(push)或是拉(pull)的方式傳送到 Logstash 再搭配 Elasticsearch 與 Kibana 搜尋到相對應的錯誤訊息或是 Log。這篇就簡單分享在 ASP.NET Core 中如何使用 ExceptionFilter 以及 Middleware 捕捉異常紀錄的方式,內容若有問題歡迎提出來一起討論。

開個 Error 給他
首先在 Visual Studio 2019 建立 ASP.NET Core API 專案
為了模擬例外狀況的發生,在預設提供的 WeatherForecastController.cs 中直接加上 throw 一個訊息為 "我是醬爆,我要爆了 !" 的 exception,代碼如下
[HttpGet]
public IEnumerable Get()
{
    throw new Exception("我是醬爆,我要爆了!!!");

    var rng = new Random();
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
    })
    .ToArray();
}
接著在執行應用程式,可以看到應用程式開啟後即噴出異常訊息

使用 Exception Filter
首先為了更模擬一般開發情境,建立 API 共用回傳的 Model 與自訂客製的例外 CustomerException 等兩個 model,代碼如下
public class ApiResponse
{
    public DateTimeOffset Timestamp { get; set; }
    public string Message { get; set; }
    public string Result { get; set; }
}

public class CustomerException: Exception
{
    private readonly Exception _exception;
}
在 ASP.NET Core 中如果要自訂錯誤機制 exception Filter 必須透過實作 IExceptionFilter 或是 IAsyncExceptionFilter 介面,差異只是 IAsyncExceptionFilter 實現非同步等待(看需求),接著建立例外發生時要用到的類別名為 BombExceptionFilter ,類別中實作 IExceptionFilter 中的 OnException 方法裡面定義當例外發生時要做的處理機制
public class BombExceptionFilter : IExceptionFilter
{
    private ILogger _log;

    public BombExceptionFilter(ILogger log)
    {
        _log = log;
    }

    public void OnException(ExceptionContext context)
    {
        var status = HttpStatusCode.InternalServerError;
        var message = string.Empty;

        var exceptionType = context.Exception.GetType();
        if (exceptionType == typeof(CustomerException))
        {
            message = context.Exception.Message;
        }
        else
        {
            message = "something wrong";
        }

        // log 
        _log.LogError(context.Exception, $"Ex massage: {message}, StackTrace: {context.Exception.StackTrace}", context.Exception);

        // 設定 exception 已處理完畢
        context.ExceptionHandled = true;
        var response = context.HttpContext.Response;
        response.StatusCode = (int)status;
        response.ContentType = "application/json";
        
        context.Result = new ObjectResult(new ApiResponse { Timestamp = DateTimeOffset.UtcNow, Message = message, Result = "我出包了,請給我一點時間" });
    }
}
OnException 中邏輯為當發生例外時,會根據發生例外的種類 exception Type 來取得回傳的錯誤訊息並定義再回傳的 message 屬性中;另外也在第38行將錯誤紀錄在 log 中,可搭配習慣的 log 解決方案,像是 log4net、serilog 看團隊習慣決定;使用 apiResponse 類別作為回傳的格式,其中定義了 timestamp、Message 及 result 等屬性,當發生錯誤時在 result 會定義 "我出包了,請再給我一點時間" 訊息告知呼叫 client 端。

接著在 startUp.cs 的 ConfigureServices 區塊加上 BombExceptionFilter 下列代碼
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddScoped<BombExceptionFilter>();
}
最後一步在 controller 中的 action 加上 BombExceptionFilter attribute
[ServiceFilter(typeof(BombExceptionFilter))]
[HttpGet]
public IEnumerable Get()
{
    throw new Exception("我是醬爆,我要爆了!!!");

    var rng = new Random();
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
    })
    .ToArray();
}
接著我們 run 應用程式看當發生錯誤時自定義的 BombExceptionFilter exception Filter 是否有生效,可以看到結果如下設定成功
有關在 ASP.NET Core 中更詳細的 exception filters 可以參考 MSDN 文件

使用 Middleware
Middleware 與 exception 相比就簡單許多,在 如何在 ASP.NET Core Middleware 加上單元測試 Unititest 文章中有詳細介紹關於 middleware 基本使用方式,這裡就不在多加說明,新增 CustomerExceptionMiddleware 的中介層來處理 exception 的邏輯,代碼如下
public class CustomerExceptionMiddleware 
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    public CustomerExceptionMiddleware(RequestDelegate next, ILogger logger)
    {
        _next = next;
        _logger = logger;
    }

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

    private Task HandleException(HttpContext context, Exception ex)
    {
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        context.Response.ContentType = "application/json";
        var message = ex.GetType() == typeof(CustomerException) ? ex.Message : "something wrong";

        // log 
        _logger.LogError(ex, $"Ex massage: {message}, StackTrace: {ex.StackTrace}", ex);

        var result = JsonConvert.SerializeObject(new ApiResponse
            {Timestamp = DateTimeOffset.UtcNow, Message = message, Result = "我出包了(Middleware)"});
        return context.Response.WriteAsync(result);
    }
}
在代碼中加上 try/catch 將發生錯誤的 exception 捕捉起來,並設定回傳 statusCode 為 IIS Server Error 以及與上述 exception filter 相同將錯誤記錄在 log 中,另外將錯誤的 result 設定為 "我出包了(Middleware)" 區別是 middleware catch 到的錯誤,接著新增擴充方法 UseCustomerExceptionMiddleware 方便在 startup 使用
public static class ApplicationBuilderExtension
{
    public static IApplicationBuilder UseCustomerExceptionMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware();
    }
}
修改 startup.cs 將稍早的 BombExceptionFilter 註解,Configure 代碼加上 UseCustomerExceptionMiddleware 方法
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    ...
    app.UseCustomerExceptionMiddleware();
    ...
}
重新執行應用程式,可以發現當 application 發生 error 的時候有 catch 到錯誤訊息
宣告使用 middleware 成功 !

感想
以上針對 exception 在 ASP.NET Core 的處理方式,分別是使用 exception filters 與 middleware 兩種方法,另外還可以使用內建的 UseExceptionHandler 方法達到類似的效果,詳細可以參考 Cash 大大 ASP.NET Core 使用內建的 ExceptionHandler Middleware 實作全站 Exception 處理的文章說明,這裡就不再班門弄斧的多加說明;還有在開發中有需要注意順序性的問題,可以透過 MSDN 的圖片了解到 middleware 與 filter 的執行順序,避免認知錯誤造成自己在開發上時間的浪費,希望這篇文章可以幫助到需要的朋友 :)

參考
ASP.NET Core Web API exception handling
ASP.NET Core 中的篩選條件
Asp.NetCore依赖注入和管道方式的异常处理及日志记录
ASP.NET Core 使用內建的 ExceptionHandler Middleware 實作全站 Exception 處理
[鐵人賽 Day17] ASP.NET Core 2 系列 - 例外處理 (Exception Handler)
Using ExceptionFilter for exception handling in AspNet Core Web API

0 意見:

張貼留言

Copyright © m@rcus 學習筆記 | Powered by Blogger

Design by Anders Noren | Blogger Theme by NewBloggerThemes.com