前言
WCF 服務端拋出的 CLR 例外 (Exception) 無法直接跨越服務邊界傳遞給用戶端,預設會被轉換為通用的 FaultException 並導致通道進入 Faulted 狀態。為了實現型別安全、跨平台相容的錯誤傳遞,WCF 提供了 FaultException、FaultException<TDetail> (錯誤契約) 以及 IErrorHandler 全域錯誤攔截機制。本篇完整示範四種錯誤處理模式的實作與用戶端捕獲差異。
四種錯誤處理模式比較
| 模式 | 服務端拋出 | 用戶端捕獲 | 型別安全 | 通道狀態 | 適用場景 |
| 原始 Exception | throw new Exception() | CommunicationException / FaultException | ❌ | Faulted (需重建 Proxy) | 除錯期、非預期錯誤 |
| FaultException | throw new FaultException() | FaultException | ❌ (僅字串) | 正常 (可繼續使用) | 簡單錯誤通知 |
| FaultException<T> | throw new FaultException<TDetail>() | FaultException<TDetail> | ✅ (強型別) | 正常 | 正式環境、跨語言互通 |
| IErrorHandler | 統一攔截未處理例外 | 依 ProvideFault 邏輯決定 | 可控制 | 可控制 | 全域日誌、統一錯誤格式、防止通道 Faulted |
完整程式碼範例
1. 定義錯誤資料契約 (供 FaultException<T> 使用)
using System.Runtime.Serialization;
namespace WCF.ServiceLib.Exception
{
[DataContract]
public class FaultMessage
{
[DataMember] public string Message { get; set; }
[DataMember] public int ErrorCode { get; set; }
}
}
2. 服務契約 — 宣告 FaultContract
using System.ServiceModel;
namespace WCF.ServiceLib.Exception
{
[ServiceContract]
public interface IHello
{
[OperationContract]
void HelloException(); // 原始 Exception
[OperationContract]
void HelloFaultException(); // 非泛型 FaultException
[OperationContract]
[FaultContract(typeof(FaultMessage))] // 關鍵:宣告錯誤契約
void HelloFaultExceptionGeneric(); // 強型別 FaultException
[OperationContract]
void HelloIErrorHandler(); // 由 IErrorHandler 處理
}
}
3. 服務實作 — 多種拋出方式
using System;
using System.IO;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
namespace WCF.ServiceLib.Exception
{
public class Hello : IHello, IServiceBehavior, IDisposable
{
// 1. 原始 Exception - 會導致通道 Faulted
public void HelloException() => throw new Exception("拋出 Exception 異常");
// 2. 非泛型 FaultException - 可攜帶 FaultCode、Reason
public void HelloFaultException()
=> throw new FaultException("拋出 FaultException 異常", new FaultCode("服務"));
// 3. 強型別 FaultException - 需搭配 [FaultContract]
public void HelloFaultExceptionGeneric()
=> throw new FaultException(
new FaultMessage { Message = "拋出 FaultException 異常", ErrorCode = -1 },
new FaultReason("為了測試 FaultException 用的"));
// 4. 觸發 IErrorHandler 攔截
public void HelloIErrorHandler() => throw new IOException("拋出異常,由 IErrorHandler 處理");
// --- IServiceBehavior 實作:註冊 IErrorHandler ---
public void AddBindingParameters(ServiceDescription sd, ServiceHostBase shb,
System.Collections.ObjectModel.Collection ep,
BindingParameterCollection bp) { }
public void ApplyDispatchBehavior(ServiceDescription sd, ServiceHostBase shb)
{
var handler = new FaultErrorHandler();
foreach (ChannelDispatcher cd in shb.ChannelDispatchers)
cd.ErrorHandlers.Add(handler);
}
public void Validate(ServiceDescription sd, ServiceHostBase shb) { }
public void Dispose() { }
}
}
4. 自訂 IErrorHandler — 統一錯誤處理與日誌
using System;
using System.IO;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
namespace WCF.ServiceLib.Exception
{
public class FaultErrorHandler : IErrorHandler
{
// 例外返回給用戶端「之前」被呼叫:可轉換為 FaultException 避免通道 Faulted
public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
if (error is IOException)
{
var fe = new FaultException("IErrorHandler - ProvideFault 測試");
var mf = fe.CreateMessageFault();
fault = Message.CreateMessage(version, mf, fe.Action);
}
// 其他例外類型可依需求轉換
}
// 例外返回給用戶端「之後」被呼叫:適合寫入日誌、發送警告
public bool HandleError(Exception error)
{
File.AppendAllText(@"C:\WCF_Log.txt",
$"[{DateTime.Now}] IErrorHandler - HandleError: {error.GetType().Name} - {error.Message}{Environment.NewLine}");
return true; // true = 已處理,不再傳遞
}
}
}
5. 宿主 Web.config (IIS 裝載)
<service name="WCF.ServiceLib.Exception.Hello" behaviorConfiguration="ExceptionBehavior">
<endpoint address="" binding="wsHttpBinding" contract="WCF.ServiceLib.Exception.IHello" />
</service>
<behaviors>
<serviceBehaviors>
<behavior name="ExceptionBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true" /> <!-- 開發期開啟,正式環境建議 false -->
</behavior>
</serviceBehaviors>
</behaviors>
6. 用戶端捕獲差異示範 (ASP.NET Web Form)
public partial class Exception_Hello : System.Web.UI.Page
{
// 1. 原始 Exception - 捕獲到 CommunicationException,通道 Faulted
protected void btnHelloException_Click(object sender, EventArgs e)
{
var proxy = new ExceptionService.HelloClient();
try { proxy.HelloException(); }
catch (Exception ex) { lblMsg.Text = ex.Message; }
finally { SafeClose(proxy); }
}
// 2. FaultException - 捕獲 FaultException,通道正常
protected void btnHelloFaultException_Click(object sender, EventArgs e)
{
var proxy = new ExceptionService.HelloClient();
try { proxy.HelloFaultException(); }
catch (FaultException fe)
{
lblMsg.Text = $"錯誤編碼:{fe.Code.Name};錯誤原因:{fe.Reason}";
}
finally { proxy.Close(); }
}
// 3. FaultException - 強型別捕獲,取得 Detail 物件
protected void btnHelloFaultExceptionGeneric_Click(object sender, EventArgs e)
{
var proxy = new ExceptionService.HelloClient();
try { proxy.HelloFaultExceptionGeneric(); }
catch (FaultException fe)
{
lblMsg.Text = $"錯誤代碼:{fe.Detail.ErrorCode};錯誤訊息:{fe.Detail.Message};原因:{fe.Reason}";
}
finally { proxy.Close(); }
}
// 4. IErrorHandler 轉換後的 FaultException
protected void btnHelloIErrorHandler_Click(object sender, EventArgs e)
{
var proxy = new ExceptionService.HelloClient();
try { proxy.HelloIErrorHandler(); }
catch (FaultException fe)
{
lblMsg.Text = $"錯誤訊息:{fe.Message}";
}
catch (Exception ex)
{
lblMsg.Text = ex.ToString();
}
finally { proxy.Close(); }
}
private void SafeClose(ICommunicationObject proxy)
{
try { proxy.Close(); }
catch { proxy.Abort(); } // Faulted 狀態只能 Abort
}
}
執行結果對照
| 按鈕 | 用戶端顯示 | 通道狀態 |
| HelloException | 拋出 Exception 異常 + CommunicationObjectFaulted | Faulted (需 Abort) |
| HelloFaultException | 錯誤編碼:服務;錯誤原因:拋出 FaultException 異常 | Opened (可繼續用) |
| HelloFaultExceptionGeneric | 錯誤代碼:-1;錯誤訊息:...;原因:... | Opened |
| HelloIErrorHandler | 錯誤訊息:IErrorHandler - ProvideFault 測試 | Opened |
最佳實務總結
- 正式環境務必使用 FaultException<T> + FaultContract:提供強型別錯誤資訊,避免用戶端解析字串。
- 實作 IErrorHandler 統一處理未預期例外:記錄完整堆疊、轉換為友善 Fault、防止通道 Faulted、整合監控系統 (如 ELK、AppInsights)。
- 服務端設定
includeExceptionDetailInFaults="false":避免洩露堆疊細節,僅在開發/測試環境開啟。
- 用戶端永遠使用 try/catch/finally + SafeClose 模式:確保通道正確關閉或中止,避免連線洩漏。
- 定義共用錯誤碼 Enum/常數類別:服務端與用戶端共用,避免魔術數字。