︿
Top

2024年2月23日 星期五

ASP.NET MVC 及 ASP.NET Core MVC 的錯誤處理

Error Handling in ASP.NET MVC and ASP.NET Core MVC

前言

有效的錯誤處理, 對於確保應用程式的穩定性和用戶體驗, 是非常重要的; 若系統沒有作好錯誤處理, 而呈現程式錯誤細節給使用者, 代表系統不穩定, 且用戶體驗會非常糟糕.

若您覺得這篇文章對您有幫助, 請不吝於在筆者的 GitHub Repository (可在 Repository 下載範例程式) 上按星星, 謝謝.

如果只關心 ASP.NET Core MVC 的實作程序及程式碼, 可以直接看筆者的另一篇文章: ASP.NET Core MVC Error Handling 摘要版 .

(一) 名詞定義

1.. 錯誤(Error): 係指框架 (ASP.NET 或 ASP.NET Core), 或應用程式自定義的例外 (Exception). 本文會說明如何將未通過資料檢核的錯誤, 包裝為自定義用戶端例外拋出. 以提供一致性的錯誤訊息規格.

2.. 錯誤處理(Error Handling): 係指錯誤取得 (Catch), 訊息規格 (Specification), 呈現 (Presentation), 及記錄 (Logging). 本文主要著重在錯誤取得及一致性錯誤訊息規格制訂.

(二) 錯誤取得機制概要

ASP.NET Core 提供了中介軟體 (Middleware) 的機制, 讓開發人員可以在整個 HTTP Request / Response 的過程中, 進行例外的攔截.

MVC 框架本身 (含 ASP.NET MVC 及 ASP.NET Core MVC) 提供了例外過濾器 (Exception Filter) 的機制, 讓開發人員在 HTTP 訊息進入 MVC 的生命週後, 進行例外的攔截.

理解 (Microsoft Learn) Filters in ASP.NET Core 的 2 張圖, 對開發人員而言, 是非常重要的.

若要完全處理整個 Web 應用程式的例外, 實作方面的相關配套如下:

1.. ASP.NET MVC: 要搭配 Web.config 或 Global.asax + 自定義的 ErrorController.

2.. ASP.NET Core MVC: 可以自行撰寫 ASP.NET Core 的例外處理中介軟體 (Middleware).
說明: Middleware 是屬於 ASP.NET Core, 不是 ASP.NET Core MVC; ASP.NET Core MVC 是建置在 ASP.NET Core 上的一個框架.

3.. ASP.NET Core 8 以後提供了實作 IExceptionHandler 介面的方式, 簡化例外處理 Middleware 的撰寫, 內建的 app.UseExceptionHandler(), 其實也是一佪 Middleware, 會使用前述的類別, 攔截例外. 只是筆者能力有限, 目前遇到無法取得 routeData (ex: controller, action 名稱) 的問題, 雖然本文還是有提供範例, 但暫不推薦此方式.

(三) 一致性的錯誤訊息規格概要

回傳一致性的錯誤訊息, 對於系統的開發及對用戶體驗, 是非常有幫助的.

1.. ASP.NET MVC: 只能自定義錯誤訊息類別.

2.. ASP.NET Core MVC: 可採用自定義錯誤訊息類別, 或內建的 ProblemDetails 類別.
說明: ASP.NET Core 2.1 起, 有一個依 IETF >RFC7807 規範 設計的 ProblemDetails 類別, 用以統一回傳結果. 但也可視需求, 自行定義統一的回傳類別.
說明: 本文採自定義的 ErrorViewModel 類別作為回傳格式, 作為說明之用. 但最後仍補上採用 ProblemDetails 類別的範例.

伺服端程式必須進行資料檢核, 才能去存取資料庫. 若資料檢核發現有誤, 代表用戶端沒有作好資料檢核的工作, 或者被瀏覽器的 F12 破解, 因此屬於用戶端的問題 (HTTP 4XX).

這類的錯誤, 需設計繼承自 Exception 類別的自定義例外類別, 當發生未通過檢核時, 打包為自訂例外拋出, 由前述的 Middleware 或 Exception Filter 攔截及處理.

上述的文字內容的思路, 可參考結論的流程圖.

(四) 章節內容

本文將探討 ASP.NET MVC 及 ASP.NET Core MVC 中一些錯誤處理實作技巧.

一. 預設的錯誤處理方式
二. 自定義統一的回傳的錯誤訊息規格
三. 自定義 Middleware, 用以產生貫穿 Request 軌跡的 TraceId
四. 撰寫 自定義例外 (Custom Exception), 供後續 Middleware 或 Filter 使用
五. 自定義 Middleware 攔截例外 (ASP.NET Core)
六. 建立 Exception Filter (ASP.NET MVC or ASP.NET Core MVC)
七. 實作 IExceptionHandler (ASP.NET Core 8 and later)
八. 建立另一個 MVC 專案, 參考前述的例外處理專案

目前 GitHub 上的範例版本為最後一版, 請下載參考. 輸出結果, 若發現回傳值有 JSON 欄位名稱字首大小寫的差異 (例如: TraceId vs. traceId), 或者有增加欄位 (例如: ControllerName, ActionName), 請再自行參酌程式碼. 前者是因為在輸出 JSON 時, 有對 System.Text.Json 作了一些選項的設定; 後者是因為要寫 Log 時, 需要該欄位.

2024年2月1日 星期四

如何在 ASP.NET Core 6 MVC 撰寫具有 相依注入(DI) 的自訂屬性 (Custom Attribute)

How to write Custom Attribute with DI in ASP.NET Core 6 MVC

前言

由於 ASP.NET Core 6 MVC 專案採用 DI (Dependency Injection) 的方式, 有一些相關的 自訂屬性 (Custom Attribute) 也想採用 DI.

原本程式碼是在 MyLogAttribute 裡面自行 new Logger(), 在套用屬性時, 是用 [MyLogAttribute]; 改採 DI 後, 變成建構子要傳入 ILogger 的參數, 在 HomeController 的 Index() method, 要改成以下的方式, 但編譯會出現 CS0181 及 CS0120 的編譯錯誤.

[MyLogAttribute(_logger)]
public IActionResult Index() {
    return View();
}

參考文件1.. 為 CS0181 的說明, 它提到自訂屬性傳入的參數, 不可以是物件變數, 必須為常數值.
本文主要採 參考文件2.. 的方法2 (ServiceFilter) 進行演練及實作.

完整範例可由 GitHub 下載.

2024年1月24日 星期三

無回傳值方法 (void method) 的單元測試, 以 Serilog 套件為例

Unit Test for void method (Serilog package) in ASP.NET Core 6 MVC

前言

接續前一篇 樂透開獎(含日期限制)(含主辦人宣佈啟動開獎) 的例子, 假設有一個新的需求: "需要將開獎的各次號碼及結果寫入Log記錄檔, 以供後續稽核".

雖然 ASP.NET Core 有提供一個 ILogger 的實作, 但功能有限. (參考文件4..)
一般會採用 Serilog 套件, 但其相關的 Log method (例如: LogTrace, LogDebug ... 等), 都沒有回傳值 (void), 無法以回傳值模擬其結果, 那應該要如何建立測試呢?

關於 Serilog 的部份, 主要採 參考文件1..及2.. 方式進行演練及實作.

完整範例可由 GitHub 下載.

2024年1月22日 星期一

靜態元素 (Static Elements) 的單元測試, 以 System.IO.File.ReadAllText 為例

Unit Test for Static Elements (System.IO.File.ReadAllText) in ASP.NET Core 6 MVC

前言

接續前一篇 樂透開獎(含日期限制) 的例子, 假設有一個新的需求: "樂透開奬有一個前置作業, 必須由主辦人員按下[開始]按鈕, 才能開獎".

本文假設主辦人員按下[開始]按鈕, 會在主機端產生一個檔案 (Extras/startup.txt), 內含主辦人員的姓名.

因此, 程式要增加一個讀取檔案內容的動作.

  • 若可讀到 Extras/startup.txt, 才可開獎, 並回傳開獎的結果, 要再加上主辦人員的姓名.
  • 若讀不到 Extras/startup.txt, 則不可開獎, 並回傳警告訊息.
    • "", -2, "主辦人員尚未按下[開始]按鈕". // 第1個空字串, 代表主辦人員的姓名

這裡很單純的想法, 是用 File.ReadAllText() 的方法, 因為是 static class + static method, 應該要如何建立測試呢?

以下係採 參考文件1..及2.. 方式進行演練及實作.

完整範例可由 GitHub 下載.

靜態元素 (Static Elements) 的單元測試, 以 DateTime.Now 為例

Unit Test for Static Elements (DateTime.Now) in ASP.NET Core 6 MVC

前言

接續前一篇 樂透開獎 的例子, 假設有一個新的需求: "樂透開奬不是每天開, 而是固定每個月的 5 日才會開獎".

因此, 程式要加一個當前日期時間的判斷.

  • 若為 5 日才可開獎, 並回傳開獎的結果.
  • 若為其它日期, 就不可開獎, 並回傳警告訊息.
    這個邏輯要如何測試呢? 總不能等到每個月 5 日, 或者調整筆電的系統時間, 才來測試吧.

以下係採 參考文件2.. "An interface that wraps the DateTime.Now" 方式進行演練及實作.

完整範例可由 GitHub 下載.

2024年1月19日 星期五

非決定性元素 (Non-Deterministic Elements) 的單元測試, 以亂數 (Random) 為例

Unit Test for Non-Deterministic Elements (Random class) in ASP.NET Core 6 MVC

前言

非決定性元素 (Non-Deterministic Elements) 是指在軟體開發中, 其行為或輸出無法預先確定或不會每次都產生相同結果的元素.
這種不確定性通常是由於外部因素, 隨機性, 系統狀態或執行環境的變化所引起.
非決定性元素在測試時可能帶來挑戰, 因為相同的輸入不一定導致相同的輸出.

以下是一些常見的非決定性元素範例:

  • 隨機數生成 (如 Random 類別): 隨機數生成器產生的數字是不可預測的, 每次調用可能得到不同的結果.
  • GUID 生成 (如 Guid.NewGuid()) : 全局唯一標識符 (GUID) 生成器每次調用時都會產生一個唯一的值.
  • 當前日期和時間 (如 DateTime.Now) : 當前的日期和時間在每次調用時都會不同, 這取決於當前的系統時間.

在單元測試中處理非決定性元素時, 一個常用的策略是通過封裝 (Wrapping) 這些元素於可控的介面後, 使用模擬或替身物件 (Test Doubles) 來提供可預測的輸出. 這樣可以減少測試的不確定性, 並提高測試的可靠性和重複性.

以下茲以一個 ASP.NET Core 6 MVC 的專案, 以 Random 類別為例, 進行演練.

完整範例可由 GitHub 下載.