前言
錯誤處理一直是開發中的重要環節之一,如果在程式發生異常錯誤的當下有效的將錯誤訊息完整的記錄下來,可以大大的節省 debug 的時間與效率,反過來如果在開發時沒有考慮到異常處理的機制,可能在發生問題時要找到錯誤的原因難度就會提高,因此在開發時必須要考慮到異常處理的機制,過去公司都會將 exception 訊息透過推(push)或是拉(pull)的方式傳送到 Logstash 再搭配 Elasticsearch 與 Kibana 搜尋到相對應的錯誤訊息或是 Log。這篇就簡單分享在 ASP.NET Core 中如何使用 ExceptionFilter 以及 Middleware 捕捉異常紀錄的方式,內容若有問題歡迎提出來一起討論。
首先在 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 ILoggerOnException 中邏輯為當發生例外時,會根據發生例外的種類 exception Type 來取得回傳的錯誤訊息並定義再回傳的 message 屬性中;另外也在第38行將錯誤紀錄在 log 中,可搭配習慣的 log 解決方案,像是 log4net、serilog 看團隊習慣決定;使用 apiResponse 類別作為回傳的格式,其中定義了 timestamp、Message 及 result 等屬性,當發生錯誤時在 result 會定義 "我出包了,請再給我一點時間" 訊息告知呼叫 client 端。_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 = "我出包了,請給我一點時間" }); } }
接著在 startUp.cs 的 ConfigureServices 區塊加上 BombExceptionFilter 下列代碼
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddScoped<BombExceptionFilter>(); }最後一步在 controller 中的 action 加上 BombExceptionFilter attribute
[ServiceFilter(typeof(BombExceptionFilter))] [HttpGet] public IEnumerable接著我們 run 應用程式看當發生錯誤時自定義的 BombExceptionFilter exception Filter 是否有生效,可以看到結果如下設定成功 有關在 ASP.NET Core 中更詳細的 exception filters 可以參考 MSDN 文件。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(); }
使用 Middleware
Middleware 與 exception 相比就簡單許多,在 如何在 ASP.NET Core Middleware 加上單元測試 Unititest 文章中有詳細介紹關於 middleware 基本使用方式,這裡就不在多加說明,新增 CustomerExceptionMiddleware 的中介層來處理 exception 的邏輯,代碼如下
public class CustomerExceptionMiddleware { private readonly RequestDelegate _next; private readonly ILogger在代碼中加上 try/catch 將發生錯誤的 exception 捕捉起來,並設定回傳 statusCode 為 IIS Server Error 以及與上述 exception filter 相同將錯誤記錄在 log 中,另外將錯誤的 result 設定為 "我出包了(Middleware)" 區別是 middleware catch 到的錯誤,接著新增擴充方法 UseCustomerExceptionMiddleware 方便在 startup 使用_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); } }
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