閱讀807 返回首頁    go 阿裏雲 go 技術社區[雲棲]


WCF技術剖析之二十一:WCF基本異常處理模式[下篇]

從FaultContractAttribute的定義我們可以看出,該特性可以在同一個目標對象上麵多次應用(AllowMultiple = true)。這也很好理解:對於同一個服務操作,可能具有不同的異常場景,在不同的情況下,需要拋出不同的異常。

   1: [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
   2: public sealed class FaultContractAttribute : Attribute
   3: {    
   4:     //省略成員
   5: }

但是,如果你在同一個操作方法上麵應用了多了FaultContractAttribute特性的時候,需要遵循一係列的規則,我們現在就來逐條介紹它們。

一、多次聲明相同的錯誤明細類型

比如在下麵的代碼中,對於操作Divide,通過FaultContractAttribute特性對同一個錯誤明細類型CalculationError進行了兩次設置。

   1: using System.ServiceModel;
   2: namespace Artech.WcfServices.Contracts
   3: {    
   4:     [ServiceContract(Namespace = "https://www.artech.com/")]
   5:     public interface ICalculator
   6:     {
   7:         [OperationContract]
   8:         [FaultContract(typeof(CalculationError))]
   9:         [FaultContract(typeof(CalculationError))]
  10:         int Divide(int x, int y);
  11:     }   
  12: }

WCF服務端框架在初始化ServiceHost,並創建服務表述的時候(關於服務描述,以及在服務寄宿過程中對服務描述的創建,《WCF技術剖析(卷1)》的第7章有詳細的介紹),會拋出如圖1所示的InvalidOperationException異常。

clip_image002

圖1 多次聲明相同的錯誤明細類型導致的異常

但是,如果你在應用FaultContractAttribute特性指定相同錯誤明細類型的同時,指定不同的Name或者Namespace,這是允許的。比如下麵的代碼中,在兩個FaultContractAttribute特性中,同樣是指定的相同的錯誤明細類型CalculationError,由於我們為之指定了不同的Name,在寄宿服務的時候將不會有上述異常的發生。

   1: using System.ServiceModel;
   2: namespace Artech.WcfServices.Contracts
   3: {    
   4:     [ServiceContract(Namespace = "https://www.artech.com/")]
   5:     public interface ICalculator
   6:     {
   7:         [OperationContract]
   8:         [FaultContract(typeof(CalculationError), Name = "CalculationError")]
   9:         [FaultContract(typeof(CalculationError), Name = "CalculationException")]
  10:         int Divide(int x, int y);
  11:     }   
  12: }

二、多次聲明不同的具有相同有效名稱錯誤明細類型

多次聲明的錯誤類型的類型雖然不同,但是如果我們為其指定相同的Name和Namespace我們可以將Name和Namespace的組合稱為有效名稱QN:Qualified Name),這依然是不允許的。比如下麵的代碼中,通過FaultContractAttribute特性為Divide操作指定了兩個不同的錯誤明細類型(CalculationError和CalculationException),但是設置的名稱卻是相同的(CalculationError)。

   1: using System.ServiceModel;
   2: namespace Artech.WcfServices.Contracts
   3: {    
   4:     [ServiceContract(Namespace = "https://www.artech.com/")]
   5:     public interface ICalculator
   6:     {
   7:         [OperationContract]
   8:         [FaultContract(typeof(CalculationError), 
   9: Name = "CalculationError", Namespace = "https://www.artech.com/")]
  10:         [FaultContract(typeof(CalculationException), 
  11: Name = "CalculationError", Namespace = "https://www.artech.com/")]
  12:         int Divide(int x, int y);
  13:     }   
  14: }

對於這種情況,在服務寄宿的時候,依然會和上麵一樣拋出一個InvalidOperationExcepiton異常,如圖2所示:

clip_image004

圖2 多次申明具有相同有效名稱導致的異常

三、多次聲明不同的具有相同數據契約有效名稱的錯誤明細類型

還有另一種情況:雖然是多次申明的是不同的錯誤明細類型,但是通過DataContractAttribute特性定義它們的時候,指定了相同的名稱和命名空間。如果我們將它們通過FaultContractAttribute特性應用到同一個操作上麵,又會出現怎樣的問題了。比如,在下麵的代碼中,我們定義了兩個不同錯誤明細類型(CalculationError和CalculationFault),它們具有相同的數據契約名稱(CalculationError)和命名空間(https://www.artech.com/)。

   1: using System;
   2: using System.Runtime.Serialization;
   3: namespace Artech.WcfServices.Contracts
   4: {
   5:     [DataContractAttribute(Namespace="https://www.artech.com/")]
   6:     public class CalculationError
   7:     {       
   8:         [DataMember]
   9:         public string Operation
  10:         { get; set; }
  11:         [DataMember]
  12:         public string Message
  13:         { get; set; }
  14:     }
  15:  
  16: [DataContractAttribute(Namespace = "https://www.artech.com/", Name = "CalculationError")]
  17:     public class CalculationFault
  18:     {
  19:         [DataMember]
  20:         public string Fault
  21:         { get; set; }
  22:     }
  23: }

如果我們通過下麵的方式通過FaultContractAttribute特性將這兩個類型應用到同一個服務操作上麵,服務寄宿不會出什麼問題,客戶端的方法調用也能正常運行。

   1: using System.ServiceModel;
   2: namespace Artech.WcfServices.Contracts
   3: {    
   4:     [ServiceContract(Namespace = "https://www.artech.com/")]
   5:     public interface ICalculator
   6:     {
   7:         [OperationContract]
   8:         [FaultContract(typeof(CalculationError))]
   9:         [FaultContract(typeof(CalculationFault))]
  10:         int Divide(int x, int y);
  11:     }   
  12: }

但是,當我們試圖通過HTTP-GET或者標準的MEX終結點獲取以WSDL表示的服務元數據(Metadata)的時候就會出現問題。至於為什麼會導致這樣的問題,你大體可以這樣來理解:當WCF為某個操作的錯誤描述(Fault Description)的時候,會創建一個字典來存儲通過FaultContractAttribute特性指定的基於錯誤明細類型的數據契約。對於這個字典來說,它的Key為數據契約的有效名稱(QN:Qualified Name),即名稱和命名空間組合。由於CalculationError和CalculationFault具有相同的名稱和命名空間,這無疑會造成Key的衝突。

由於數據契約是使對數據結構的一種描述,如果兩個數據契約時等效的,不管其具體的托管類型是什麼,WCF在遇到上述情況的時候,會自動識別並忽略其中一個,從而保證元數據能夠正確產生。比如說,如果我們將CalculationFault進行如下的改寫,服務的元數據就能夠被正常地獲得了。

   1: [DataContractAttribute(Namespace = "https://www.artech.com/", Name = "CalculationError")]
   2: public class CalculationFault
   3: {
   4:     [DataMember(Name = "Operation")]
   5:     public string OperationName
   6:     { get; set; }
   7:     [DataMember(Name = "Message")]
   8:     public string Fault
   9:     { get; set; }
  10: }

四、通過XmlSerializer對錯誤明細對象進行序列化

對於任何分別是框架來說,序列化和反序列化都是其功能體係中重要的一環。WCF通過一個重要的對象實現對托管對象的序列化和反序列化:序列化器(Serializer)。具體來說,所有序列化和反序列化的功能又最終落實到兩個具體的序列化器上:DataContractSerializer和XmlSerializer。關於這兩種序列化器,在《WCF技術剖析(卷1)》第5章中已經有過深入的探討,在這裏就需要在畫蛇添足了。

WCF采用的默認序列化器是DataContractSerializer,但是有的時候,我們需要顯示地控製某個服務或者服務的某個操作的序列化行為,通過XmlSerializer來序列化和反序列化操作的參數對象和返回值。舉個例子,一個服務的絕大部分操作的參數類型都是通過數據契約的方式定義,但是對於個別的操作參數類型依然沿用的是傳統XML的定義方式。在這種情況下,我們希望的是專門對這幾個操作進行定製,讓它們采用XmlSerializer作為它們的序列化器。

我們可以通過對自定義特性System.ServiceModel.XmlSerializerFormatAttribute的應用幫助我們是相上麵的功能。從先麵對XmlSerializerFormatAttribute的定義我們可以看出:應用特性的目標元素的類型包括接口、類和方法。也就是說,XmlSerializerFormatAttribute既可以應用於服務契約接口上,也可以應用於服務類型上,甚至可以應用於服務接口和服務類型的方法上。

   1: [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Method | AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
   2: public sealed class XmlSerializerFormatAttribute : Attribute
   3: {   
   4:     public XmlSerializerFormatAttribute();  
   5:     public OperationFormatStyle Style { get; set; }
   6:     public bool SupportFaults { get; set; }
   7:     public OperationFormatUse Use { get; set; }
   8: }

在默認的情況下,XmlSerializerFormatAttribute特性僅僅控製操作的參數和返回值的序列化行為,而不能控製錯誤明細對象的序列化行為。也就是說,基於在某個操作方法上應用了XmlSerializerFormatAttribute特性,WCF會采用XmlSerializer作為所有參數和返回值的序列化器,對於出現異常指定的錯誤明細對象,依然采用默認的DataContractSerializer進行序列化和反序列化。我們可以通過SupportFaults屬性來顯式地選擇XmlSerializer作為錯誤明細對象的序列化器。在下麵的代碼中,我們將XmlSerializerFormatAttribute特性應用在服務契約的Divide操作上麵,並將SupportFaults屬性設為true。

   1: using System.ServiceModel;
   2: namespace Artech.WcfServices.Contracts
   3: {    
   4:     [ServiceContract(Namespace = "https://www.artech.com/")]
   5:     public interface ICalculator
   6:     {
   7:         [OperationContract]
   8:         [FaultContract(typeof(CalculationError), Name = "CalculationError")]
   9:         [XmlSerializerFormat(SupportFaults = true)]
  10:         int Divide(int x, int y);
  11:     }
  12: }

那麼對於Divide操作,WCF將會采用XmlSerializer同時作為參數、返回值和錯誤明細對象的序列化器。比如在這個時候,我們采用下麵的形式對CalculationError進行重新定義:

   1: using System;
   2: using System.Runtime.Serialization;
   3: using System.Xml;
   4: using System.Xml.Serialization;
   5: namespace Artech.WcfServices.Contracts
   6: {
   7:     [Serializable]
   8:     public class CalculationError
   9:     {       
  10:         [XmlAttributeAttribute("op")]
  11:         public string Operation
  12:         { get; set; }
  13:         [XmlElement("Error")]
  14:         public string Message
  15:         { get; set; }
  16:     }
  17: }

在被零除而拋出異常的情況下,WCF將會生成如下一個Fault SOAP,其中s:Body>/<s:Fault>/ <s:Detail>節點中的XML為CalculationError對象序列化所的。

   1: <s:Envelope xmlns:s="https://www.w3.org/2003/05/soap-envelope" xmlns:a="https://www.w3.org/2005/08/addressing">
   2:   <s:Header>
   3:     <a:Action s:mustUnderstand="1">https://www.artech.com/ICalculator/DivideCalculationError</a:Action>
   4:     <a:RelatesTo>urn:uuid:7b01995b-9f81-4a08-9fa2-c5ef8c7cacc</a:RelatesTo>
   5:   </s:Header>
   6:   <s:Body>
   7:     <s:Fault>
   8:       <s:Code>
   9:         <s:Value>s:Sender</s:Value>
  10:       </s:Code>
  11:       <s:Reason>
  12:         <s:Text xml:lang="zh-CN">被除數y不能為零!!</s:Text>
  13:       </s:Reason>
  14:       <s:Detail>
  15:         <CalculationError op="Divide" xmlns="https://www.artech.com/" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="https://www.w3.org/2001/XMLSchema">
  16:           <Error>被除數y不能為零!!</Error>
  17:         </CalculationError>
  18:       </s:Detail>
  19:     </s:Fault>
  20:   </s:Body>
  21: </s:Envelope>   

作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-30 11:34:36

  上一篇:go  WCF技術剖析之二十一:WCF基本異常處理模式[中篇]
  下一篇:go  WCF技術剖析之二十二: 深入剖析WCF底層異常處理框架實現原理[上篇]