前兩篇建立了「sys.messages 統一錯誤碼 → RAISERROR 拋出 → TRY/CATCH 捕捉 → ADO.NET 捕獲 SqlException」的完整鏈路。本文深入剖析 SqlException 物件屬性映射關係,並介紹 SqlConnection.InfoMessage 事件處理非錯誤訊息 (PRINT、RAISERVERITY ≤ 10) 的機制。
| 屬性 | 型別 | 來源映射 | 說明 |
|---|---|---|---|
Errors | SqlErrorCollection | T-SQL RAISERROR 產生的每一條錯誤記錄 | 包含 Number、Message、Severity、State、Procedure、LineNumber、Server 等詳細資訊,可能多條 (批次多條 RAISERROR) |
Number | Int32 | sys.messages.message_id / @@ERROR | 錯誤號,**關鍵識別碼**。若 RAISERROR 直接傳字串而非 message_id,預設為 50000 |
Message | String | sysmessages.text / RAISERROR 字串 | 完整錯誤訊息文字 |
Severity (Class) | Byte | RAISERROR severity 參數 | 11-16 可被 TRY/CATCH 捕捉;17-19 系統級;20-25 致命 (斷開連線) |
State | Byte | RAISERROR state 參數 | 同一錯誤號不同狀態碼,用於區分拋出位置 |
Procedure | String | 預存程序/觸發器/函數名稱 | 錯誤發生的模組名稱 |
LineNumber | Int32 | 批次/模組內行號 | 錯誤發生的行號 (從 1 起算) |
Server | String | 連線字串 Data Source | 錯誤來源伺服器名稱 |
Source | String | .NET 提供者名稱 | 固定為 .Net SqlClient Data Provider |
ErrorCode | Int32 | HRESULT (0x80131904) | COM 互通性錯誤碼,固定值 |
-- T-SQL
RAISERROR(50001, 16, 1); -- 50001 為 sys.messages 中自訂 message_id
-- ADO.NET 捕獲
ex.Number → 50001 ✅ 精確識別業務錯誤類型
-- T-SQL
DECLARE @msg NVARCHAR(200) = '使用者已存在';
RAISERROR(@msg, 16, 1); -- 無 message_id
-- ADO.NET 捕獲
ex.Number → 50000 ❌ 所有自訂字串錯誤號相同,無法區分業務類型
RAISERROR(@msg, 16, 1) WITH SETERROR; -- 強制將當前 @@ERROR 值設為錯誤號
-- 若前一條語句產生錯誤 (如 PK 違反 2627),則 Number = 2627
-- 否則 Number = 50000
public static bool CreateUser(string userName)
{
const string proc = "P_USERS_I";
var parameters = new Dictionary
{
["user_id"] = Guid.NewGuid().ToString(),
["user_name"] = userName
};
try
{
ExecuteCommand(proc, parameters); // 內部使用 SqlCommand + Parameters
return true;
}
catch (SqlException ex)
{
// 結構化日誌記錄 (Serilog/NLog/ELK)
Log.Error(ex, "CreateUser 失敗 | UserName={UserName} | ErrorNumber={Number} | Severity={Class} | Procedure={Procedure} | Line={LineNumber} | Server={Server} | State={State}",
userName, ex.Number, ex.Class, ex.Procedure, ex.LineNumber, ex.Server, ex.State);
// 逐條 SqlError 詳細檢查 (批次可能多條)
foreach (SqlError err in ex.Errors)
{
Log.Error("SqlError Detail | Number={Number} | Message={Message} | Severity={Severity} | State={State} | Procedure={Procedure} | Line={Line}",
err.Number, err.Message, err.Class, err.State, err.Procedure, err.LineNumber);
}
// 業務邏輯分流:依錯誤號決定重試/拒絕/告警
switch (ex.Number)
{
case 2627: // PK/Unique 違反
case 50001: // 自訂:使用者已存在
return false; // 友善提示用戶
case 1205: // 死結
throw new RetryableException("資料庫忙碌,請稍後重試", ex);
default:
throw; // 未知嚴重錯誤向上拋出
}
}
}
RAISERROR severity ≤ 10 或 PRINT 陳述式**不會拋出例外**,而是觸發 SqlConnection.InfoMessage 事件。常用於:長時間作業進度回報、ETL 步驟日誌、除錯資訊傳遞。
CREATE PROC P_CLEAR_DATA
AS
DELETE FROM T_USERS_IN_ROLES;
DELETE FROM T_USERS;
DELETE FROM T_ROLES;
PRINT N'所有資料已清除完成!'; -- Severity 0,觸發 InfoMessage
GO
using (var conn = new SqlConnection(connStr))
{
conn.InfoMessage += (sender, e) =>
{
// e.Errors 為 SqlErrorCollection,同 SqlException.Errors
foreach (SqlError err in e.Errors)
{
Console.WriteLine($"[INFO] {err.Message} | Proc={err.Procedure} | Line={err.LineNumber}");
// 可寫入應用日誌、推送 SignalR 進度至前端、更新作業狀態表
}
};
// 必須設定 FireInfoMessageEventOnUserErrors = true 才能接收 severity 1-10 的訊息
conn.FireInfoMessageEventOnUserErrors = true;
using (var cmd = new SqlCommand("P_CLEAR_DATA", conn) { CommandType = CommandType.StoredProcedure })
{
conn.Open();
cmd.ExecuteNonQuery();
}
}
// 輸出:[INFO] 所有資料已清除完成! | Proc=P_CLEAR_DATA | Line=5
| 場景 | T-SQL 寫法 | .NET 處理 | 關鍵點 |
|---|---|---|---|
| 業務錯誤 (可預期、可處理) | RAISERROR(message_id, 16, 1) | Catch SqlException, 依 ex.Number 分流 | 使用 sys.messages 管理訊息,Number 精確映射 |
| 系統/不可預期錯誤 | TRY/CATCH + THROW / RAISERROR(@msg, 16, 1) | Catch SqlException, 記錄完整堆疊,向上拋出 | THROW 保留原始堆疊 (SQL 2012+) |
| 進度/資訊/除錯訊息 | PRINT / RAISERROR('msg', 0, 1) / RAISERROR('msg', 10, 1) | 訂閱 conn.InfoMessage, 設定 FireInfoMessageEventOnUserErrors=true | 不中斷執行流程,非同步接收 |
| 批次多錯誤 | 多條 RAISERROR | 遍歷 ex.Errors / e.Errors 集合 | 單一 SqlException 包含多條 SqlError |
掌握 SqlException 屬性映射與 InfoMessage 事件機制,能構建「錯誤精確識別、資訊即時傳遞、日誌結構化、業務優雅降級」的完整異常處理體系。核心原則:**業務錯誤用自訂 message_id (Number 精確)、系統錯誤用 THROW 保留堆疊、資訊流用 InfoMessage 非阻塞傳遞**。