任何程式都離不開對異常的處理,良好的異常處理方式可加快尋找出異常的根源,同時也需要避免暴露敏感資訊到異常中。WCF這種典型的服務端和用戶端互動的程式,服務端的異常更需要適當的處理。下面以一個簡單的服務為例,說明WCF中處理異常的方式。
WCF服務定義如下,很明顯方法Divide在divisor為0的時候將會拋出異常
View Code
public class CalculateService : ICalculateService
{
public int Divide(int dividend, int divisor)
{
return dividend / divisor;
}
public int Add(int a, int b)
{
return a + b;
}
}
用戶端調用如下:
View Code
using (var client = new CalculateServiceClient())
{
try
{
Console.WriteLine(client.Divide(20, 0));
}
catch (FaultException ex)
{
Console.WriteLine(ex.Reason);
}
}
首先需要知道的是,WCF的異常資訊預設是以FaultException的形式返回到用戶端,FaultException的關鍵屬性Reason是對用戶端反饋的最重要訊息之一。以上用戶端代碼調用之後,預設的FaultException返回的Message資訊如下:
由於內部錯誤,伺服器無法處理該請求。有關該錯誤的詳細資料,請開啟伺服器上的 IncludeExceptionDetailInFaults (從 ServiceBehaviorAttribute 或從 <serviceDebug> 配置行為)以便將異常資訊發送回用戶端,或在開啟每個 Microsoft .NET Framework 3.0 SDK 文檔的跟蹤的同時檢查伺服器追蹤記錄檔。
根據異常的提示,意思說如果要在用戶端看到詳細的Exception資訊,那麼請將ServiceBehavior對應的IncludeExceptionDetailInFaults屬性設定為True,通常在配置中表現為如下設定:
View Code
1 <serviceBehaviors>
2 <behavior>
3 <serviceMetadata httpGetEnabled="True" httpGetUrl="http://localhost:8733/CalculateService/"/>
4 <serviceDebug includeExceptionDetailInFaults="True" />
5 </behavior>
6 </serviceBehaviors>
通過以上設定之後,用戶端輸出的內容為“嘗試除以零”,這個提示資訊跟原始的異常資訊是一致,即返回的FaultException中的Reason包含原始異常的Message的值,但是這樣處理之後服務端所報出的異常資訊直接傳到了用戶端,比如一些保密資訊也可能輸出到了用戶端,因此對於異常資訊必須進行一個封裝。最直接的形式莫過於在服務端就把異常給捕獲了,並重新throw一個FaultException
服務端的代碼改進如下,經過以下改進,那麼用戶端得到的資訊僅僅是"操作失敗",同時服務端也記錄了異常資訊(這時IncludeExceptionDetailInFaults是設定為False的)。
View Code
1 try
2 {
3 return dividend / divisor;
4 }
5 catch (Exception ex)
6 {
7 Console.WriteLine(ex.Message);
8 throw new FaultException("操作失敗");
9 }
當然這是FaultException的預設用法,FaultException還支援強型別的異常錯誤資訊,返回更加豐富和精確的錯誤提示。假設定義如下通用的一個FaultContract類型,將出錯時的使用者名稱和線程名字記錄到異常資訊中,因為異常資訊也是通過SOAP格式傳輸的,因此跟定義其他DataContract的方式一樣。
CommonFaultContract
1 [DataContract]
2 public class CommonFaultContract
3 {
4 [DataMember]
5 public string UserName { get; set; }
6 [DataMember]
7 public string ThreadName { get; set; }
8 }
那麼服務方法的介面需要增加如下標記,如果不這樣標記,那麼用戶端得到的異常類型依然是FaultException,而不是強型別的異常資訊。
[FaultContract(typeof(CommonFaultContract))]
int Divide(int dividend, int divisor)
實現方法中拋出異常的部分代碼改成如下:
異常處理
1 catch (Exception ex)
2 {
3 Console.WriteLine(ex.Message);
4 throw new FaultException<CommonFaultContract>(new CommonFaultContract
5 {
6 UserName = Environment.UserName,
7 ThreadName = System.Threading.Thread.CurrentThread.Name
8 }, "操作失敗");
9 }
這時候重建用戶端的代理類,然後更新用戶端的代碼如下,紅色部分即擷取強型別的異常錯誤資訊。
View Code
1 try
2 {
3 Console.WriteLine(client.Divide(20, 0));
4 }
5 catch (FaultException<CommonFaultContract> ex)
6 {
7 Console.WriteLine(ex.Detail.ThreadName);
8 Console.WriteLine(ex.Detail.UserName);
9 Console.WriteLine(ex.Reason);
10 }
當然在具體應用中還需要根據需求,返回不同的資訊,構建不同的FaultContract。
以上服務端捕獲的異常方法,適用於方法比較少的情況,如果有十多個方法,一個個去寫try catch然後做標記等,那麼工作量會很大,而且代碼也不利於重用。嘗試尋找像MVC Controller那樣的統一處理Exception的方式,將異常處理都放在基類中,那麼只要繼承與這個基類的方法都不需要去寫try catch去捕獲異常。但WCF中似乎沒有這樣的機制,放棄了這種做法。
最近在研究Enterprise Lib中對WCF的支援時,發現Exception Block中還特地有針對WCF程式異常處理的解決方案,而且滿足以上說道的需求,即可記錄異常,又可對異常資訊進行封裝。更重要的時,自動處理運行時的異常資訊,不需要挨個方法的去寫Try catch。秉承企業庫的優秀傳統,大部分工作還是通過配置就可以完成了,非常好的解決方案。下面介紹具體的使用步驟。
步驟一:
引用以下dll
Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.dll
Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.WCF.dll
Microsoft.Practices.EnterpriseLibrary.Common.dll
Microsoft.Practices.ObjectBuilder2.dll
步驟2:
在具體的實作類別中,增加如下屬性標記,其中WcfException為企業庫中Exception Block中的一個異常處理策略,具體如何配置異常處理策略,請參考企業庫的協助文檔。
[ExceptionShielding("WcfException")]
public class CalculateService : ICalculateService
那麼只要增加了[ExceptionShielding("WcfException")]這個屬性標記之後,所有運行時的異常都將交給策略名為WcfException的異常處理block來處理,在這裡就可以執行一些異常記錄以及異常封裝的操作。
步驟3:
將異常資訊封裝為FaultException,這個動作也是通過配置來完成。在Exception節點中添加一個Fault Contract Exception Handler。
Fault Contract Exception Handler需要設定以下兩個屬性值
exceptionMessage:所有異常封裝後的錯誤資訊
faultContractType:即返回異常的faltContract類型,這個類型必須指定一個,哪怕方法中沒有用到也要,如果方法中有用到,那麼用戶端那邊就能得到強型別FaultException,否則就是普通的FaultException。這裡指定為之前定義的CommonFaultContract。
對於faultContract類型的值,還可以通過PropertyMappings來自訂需要從原始異常資訊中映射到faultContract的屬性中,這個屬性可選。
經過以上步驟配置之後,服務端的程式就具備了自動處理異常的功能。用戶端還是跟往常那樣調用,不過具體是用FaultException捕獲異常還是FaultException<T>去捕獲異常,還得根據定義方法中是否標記了FaultContract。之後若定義了其他服務介面,同樣也僅僅需要在實作類別上加上[ExceptionShielding("WcfException")]標記即可。
(圖片後續補上)