WCF技术剖析之二十二: 深入剖析WCF底层异常处理框架实现原理[上篇]_WCF对于上一篇文章 (WCF基本异常处理模式:[上篇]、[中篇]、[下篇]),主要是站在最终开发者的角度对WCF关于异常处理编程模式进行了介绍,接下来,我们需要将我们的目光转移到WCF框架内部,深入剖析整个WCF异常处理流程。

 

一、 从SOAP Fault说起(基于SOAP 1.2)

服务调用的最终实现通过消息交换完成,WCF本质上可以看成是一个消息处理的框架。消息,不但承载着正常服务调用的请求和回复,在出现异常时,消息依然是错误信息的载体。今年来,尽管随着REST的迅速发展,基于POX(Plain of XML)消息交换大行其道;AJAX的持续升温,又是的基于非XML(JSON)的消息开始火热,但是不可否认,在今后不短的一段时间内SOAP依然是主流。看看现在有多少WS-*规范或者标准是建立在SOAP之上,就应该对这个论断不会有怀疑。

W3C先后出台了两个关于SOAP的规范:SOAP 1.1和SOAP 1.2。在《WCF技术剖析(卷1)》的第5章,我曾经按照SOAP 1.2规范对SOAP作了全面的介绍,这其中就包括对SOAP Fault。由于异常在消息交换中通过Fault消息承载,所以很多必要对SOAP Fault的相关规范作一下重申,至于SOAP的其他相关的内容,在这里就不再作重复的介绍了。

要了解SOAP 1.2下关于SOAP 1.2对SOAP Fault的规范,我们首先来看看一下一个具有完整结构的Fault SOAP。


1: <s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
2:   <s:Header>
3:     <a:Action s:mustUnderstand="1">http://www.artech.com/ICalculator/DivideCalculationErrorFault</a:Action>
4:   </s:Header>
5:   <s:Body>
6:     <s:Fault>
7:       <s:Code>
8:         <s:Value>s:Sender</s:Value>
9:         <s:Subcode>
10:           <s:Value xmlns:a="http://www.artech.com/">a:CalculationError</s:Value>
11:         </s:Subcode>
12:       </s:Code>
13:       <s:Reason>
14:         <s:Text xml:lang="zh-CN">被除数y不能为零!</s:Text>
15:       </s:Reason>
16:       <s:Node>http://http://www.artech.com/calculationcenter</s:Node>
17:       <s:Role>http://http://www.artech.com/calculatorservice</s:Role>
18:       <s:Detail>
19:         <CalculationError xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.artech.com/">
20:           <Message>被除数y不能为零!</Message>
21:           <Operation>Divide</Operation>
22:         </CalculationError>
23:       </s:Detail>
24:     </s:Fault>
25:   </s:Body>
26: </s:Envelope>


上面一个SOAP是非常完整的Fault消息,它的主体(Body)部分包含了构成SOAP Fault所有类型的元素(必需的和可选的),接下来,我们就在这个Fault消息的基础上介绍SOAP 1.2规范下对SOAP Fault的相关规定。

在SOAP 1.2规范中规定,SOAP Fault作为Fault SOAP消息的主体,用于承载错误相关的信息。具体地,对于SOAP Fault元素,有作了如下几点规范:


1、一个必须的Code元素表示错误代码;

2、一个比如的Reason元素表示出错的原因;

3、一个可选的Node元素表示导致出错的SOAP节点(SOAP Node);

4、一个可选的Role元素表示SOAP节点对应的角色;

5、一个可选的Detail表述对错误的详细描述。

接下来,我们对组成SOAP Fault的五个子元素进行简单的介绍。

1、Fault Code元素

SOAP Fault的Code元素,是一个用以表示错误类型的代码,该错误代码可以大致看作对错误的一种分类。SOAP 1.2对Code元素的格式作了如下的规范:

1、一个必须的Value元素用以定义错误代码;

2、一个可选的SubCode元素用以定义错误子代码。

而对于Value元素的格式,又具有如下的规范:



枚举值



含义



VersionMismatch



命名空间或者名称和规定的SOAP规范不匹配



MustUnderstand



目标SOAP结点不能理解并处理mustUnderstand属性为“true”或者“1”的SOAP报头



DataEncodingUnknown



SOAP报头或者主体的数据编码方式不被目标SOAP结点支持



Sender



消息格式合法或者缺少必要的数据



Receiver



SOAP结点处理消息出现错误


而SubCode元素相关的规范定义如下:

1、必须Value元素:名称为“Value”,命名空间名称为“​​http://www.w3.org/2003/05/soap-envelope​​”,类型为“xs:QName”,一般将具体应用定义错误代码用作该元素的值

2、可选的Subcode元素

可见,SOAP Code是一种具有层级关系的(Hierarchical)的结构(Code的具有一个Code结构的SubCode)。在上面给出的Fault消息,就具有一个具有两层结构的SOAP Code:


1: <s:Code>
2:   <s:Value>s:Sender</s:Value>
3:   <s:Subcode>
4:     <s:Value xmlns:a="http://www.artech.com/">a:CalculationError</s:Value>
5:   </s:Subcode>
6: </s:Code>


2、Fault Reason元素

对于一个SOAP Fault消息,除了必须有一个表示错误代码的Code元素之外,还需要具有一个Reason元素用以表导致错误的原因。SOAP 1.2对Reason元素的格式作了如下的规范:

对于上面给出的SOAP Fault消息,具有如下一个SOAP Reason元素。Text元素中的lang属性表示想相应的语言文化,也就是说,你可通过该属性指定基于不同语言文化的文字用于描绘苏错误的原因。


1: <s:Reason>
2:   <s:Text xml:lang="zh-CN">被除数y不能为零!</s:Text>
3: </s:Reason>


3、Fault Node元素

由于在整个SOAP消息的路由过程中,错误可能发生在最终接收结点,也可能发生在中间结点。为了使SOAP Fault消息的接收者能够判断导致错误的SOAP结点类型,在生成Fault消息的时候,可以通过Node元素指定结点的类型。SOAP 1.2对Node元素作如下的规范:

在上面给出的Fault消息中,我将Fault Node指定为http://http://www.artech.com/calculationcenter。

4、Fault Role元素

SOAP结点处理SOAP消息时候担当着不同的角色。SOAP Fault的Role元素即用以表述导致错误的SOAP结点对应的角色。SOAP 1.2对Node元素的格式作了如下的规范:


  • 元素名称必须为“Role”,命名空间名称为“​​http://www.w3.org/2003/05/soap-envelope​​”;
  • 元素值得类型为“xs:anyURI”,即通过URI表示的SOAP结点对于得角色(参考SOAP报头的Role属性)。

 

5、Fault Detail元素

在很多基于SOAP通信的应用中,SOAP Fault消息的接收者处理需要了解通过上面介绍的基本错误元素表示的错误信息之外,往往还需要一些对错误信息更加详尽的描述。这样的描述就可以通过Detail元素来表示。SOAP 1.2对Detail元素作了如下的规范:

通过上面给出的Fault消息,我们可以看出该元素对应着我们在第一节介绍的错误明细对象,既FaultException<TDetail>异常最终序列化生成Fault消息的时候,其Detail属性表示的错误明细对象被序列化成Fault Detail元素。


1: <s:Detail>
2:   <CalculationError xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.artech.com/">
3:     <Message>被除数y不能为零!</Message>
4:     <Operation>Divide</Operation>
5:   </CalculationError>
6: </s:Detail>


二、 WCF下的异常:FaultException

在《WCF技术剖析(卷1)》中,我曾经提到过,在整个WCF体系下,数据存在的形态大体可以分为两种:XML和托管对象(Managed Object)。WCF建立在.NET平台下,利用托管语言(C#和VB.NET)开发人员提供了一个面向对象的编程模型,所以,在WCF体系最顶层的数据形态表现为.NET托管对象。而最终服务调用体现在消息的交换上,消息时基于XML的(除了少部分非XML的消息,比如JSON)。从数据转化的角度上讲,WCF起到了一个将数据从这两种形态数据进行转化和适配的作用。

在WCF异常处理体系中,对于异常或者错误,在XML的世界里最终通过Fault消息体现;而在托管对象的世界中,即使相应的Exception对象。上面以小节,我们在消息交换的角度对SOAP Fault进行了讲解,接下来我们介绍的对象就是它在托管世界的对立体:FaultException。

通过千篇 一片文章的内容,我们知道了基于WCF异常处理的编程只要围绕着FaultException这个类型来完成的,所以我们很有必要重新深入地认识这个对象。先通过下面的代码片段来看看FaultException的基本定义:


1: [Serializable]
2: public class FaultException : CommunicationException
3: {
4:    //其他成员
5:     public FaultException();
6:     public FaultException(FaultReason reason);
7:     public FaultException(string reason);
8:     public FaultException(FaultReason reason, FaultCode code);
9:     public FaultException(string reason, FaultCode code);
10:     public FaultException(FaultReason reason, FaultCode code, string action);
11:     public FaultException(string reason, FaultCode code, string action);
12:
13:     public string Action { get; }
14:     public FaultCode Code { get; }
15:     public override string Message { get; }
16:     public FaultReason Reason { get; }
17: }


上面列出了FaultException的一些构造函数和公共属性。Action表述最终生成到Fault消息中WS-Addressing报头Action的值。而Code和Reason则对应着SOAP Fault中的Code和Reason元素,它们的类型分别为System.ServiceModel.FaultCode和System.ServiceModel.FaultReason,我们先来详细地了解这两种类型。

1、 System.ServiceModel.FaultCode

FaultCode表述错误的代码,该代码大体上可以看成是一种对错误的分类。在序列化FaultException对象生成Fault消息的时候,该对象最终会生成SOAP Fault的Code节点。在介绍SOAP Fault的时候,我们提到SOAP Fault中的Code是一种具有层级关系(Hierarchical)的结构,这也体现在FaultCode的定义上:从下面对FaultCode的定义代码中 ,我们可以看到属性SubCode的属性是FaultCode本身。


1: public class FaultCode
2: {
3:     public FaultCode(string name);
4:     public FaultCode(string name, FaultCode subCode);
5:     public FaultCode(string name, string ns);
6:     public FaultCode(string name, string ns, FaultCode subCode);
7:
8:     public static FaultCode CreateReceiverFaultCode(FaultCode subCode);
9:     public static FaultCode CreateReceiverFaultCode(string name, string ns);
10:     public static FaultCode CreateSenderFaultCode(FaultCode subCode);
11:     public static FaultCode CreateSenderFaultCode(string name, string ns);
12:
13:     public bool IsPredefinedFault { get; }
14:     public bool IsReceiverFault { get; }
15:     public bool IsSenderFault { get; }
16:     public string Name { get; }
17:     public string Namespace { get; }
18:     public FaultCode SubCode { get; }
19: }


FaultCode的Name和NameSpace属性表述SOAP Code中Value元素的值,而SubCode属性则自然对应着同名的SubCode元素。IsPredefinedFault属性表示该Fault Code是否属于预定义的。WCF通过命名空间确定其是否是预定义的Fault Code,具体来讲,只有具有以下三个命名空间的才属于预定义的Fault Code:http://schemas.xmlsoap.org/soap/envelope/(SOAP​ 1.1)、http://www.w3.org/2003/05/soap-envelope(SOAP​ 1.2)和http://schemas.microsoft.com/ws/2005/05/envelope/none。对于名称为Sender和Receiver的预定义Fault Code,它们的IsSenderFault和IsReceiverFault分别返回true。

FaultCode还定义了两组静态方法(CreateSenderFaultCode和CreateReceiverFaultCode)帮助我们方便地创建Sender和Receiver预定义FaultCode。比如,下面我们调用静态方法CreateSenderFaultCode创建了一个FaultCode,该FaultCode的内容和我们前面给定的Fault消息中的Fault Code是一致的:


1: FaultCode code = FaultCode.CreateSenderFaultCode(new FaultCode("CalculationError", "http://www.artech.com/"));


对应在Fault消息中的Fault Code元素:


1: <s:Code>
2:   <s:Value>s:Sender</s:Value>
3:   <s:Subcode>
4:     <s:Value xmlns:a="http://www.artech.com/">a:CalculationError</s:Value>
5:   </s:Subcode>
6: </s:Code>


接下来,我们来介绍另一个类型System.ServiceModel.FaultReason。

2、 System.ServiceModel.FaultReason

FaultReason用于定于错误的原因,在SOAP Fault对应的元素为Reason,下面是其定义代码:


1: public class FaultReason
2: {
3:     public FaultReason(IEnumerable<FaultReasonText> translations);
4:     public FaultReason(FaultReasonText translation);
5:     public FaultReason(string text);
6:
7:     public FaultReasonText GetMatchingTranslation();
8:     public FaultReasonText GetMatchingTranslation(CultureInfo cultureInfo);
9:     public override string ToString();
10:
11:     public SynchronizedReadOnlyCollection<FaultReasonText> Translations { get; }
12: }


虽然SOAP Fault的Reason的值仅仅是一个字符文本,但是处于本地化(Localization)的支持,允许我们基于不同语言文化定义不同的内容。基于语言文化的Reason文本通过一个特殊的类型表示:FaultReasonText。


1: public class FaultReasonText
2: {
3:     public FaultReasonText(string text);
4:     public FaultReasonText(string text, CultureInfo cultureInfo);
5:     public FaultReasonText(string text, string xmlLang);
6:     public bool Matches(CultureInfo cultureInfo);
7:
8:     public string Text { get; }
9:     public string XmlLang { get; }
10: }


从上面对FaultReasonText的定义中可以看到,我们可以通过指定CultureInfo和String对象指定基于某种语言文化的Fault Reason。如果没有显式指定CultureInfo,默认采用的是当前线程的语言文化。

对于对FaultReason对象的构建,既可以通过指定一个FaultReasonText集合创建支持多语言文化的Fault Reason,也可以通过指定单个FaultReasonText创建基于某个单一语言文化的Fault Reason。最简单的莫过于直接指定一个字符串表述的Reason文本,这是默认采用当前线程的语言文化。而属性Translations返回一个FaultReasonText的集合。

下面的代码中,我们创建了一个FaultReason对象对两种语言文化提供支持(简体中文和美式英语)


1: IList<FaultReasonText> reasonTexts = new List<FaultReasonText>();
2: reasonTexts.Add(new FaultReasonText("The input parameter is invalid!","en-US"));
3: reasonTexts.Add(new FaultReasonText("输入参数不合法!", "zh-CN"));
4: FaultReason reason = new FaultReason(reasonTexts);


对应在SOAP Fault的Reason元素的内容为:


1: <s:Reason>
2:   <s:Text xml:lang="en-US">The input parameter is invalid!</s:Text>
3:   <s:Text xml:lang="zh-CN">输入参数不合法!</s:Text>
4: </s:Reason>


FaultException进行提供过Code和Reason两个基本SOAP Fault元素的定义,而不能提供对Detail元素的定义,因为该元素是通过另一个继承自FaultException的异常类型定义的:FaultException<TDetail>。

3、 FaultException<TDetail>

当从服务端抛出异常时,如果需要通过一个对象用于描述错误的消息信息,不管该对的类型是基元类型(比如String,Int等)还是自定义类型(比如自定义数据契约),就不得不使用泛型的FaultException<TDetail>异常对象了。下面给出了FaultException<TDetail>的定义:

[Serializable]

public class FaultException


1: [Serializable]
2: public class FaultException<TDetail> : FaultException
3: {
4:    //其他成员
5:     public FaultException(TDetail detail);
6:     public FaultException(TDetail detail, FaultReason reason);
7:     public FaultException(TDetail detail, string reason);
8:     protected FaultException(SerializationInfo info, StreamingContext context);
9:     public FaultException(TDetail detail, FaultReason reason, FaultCode code);
10:     public FaultException(TDetail detail, string reason, FaultCode code);
11:     public FaultException(TDetail detail, FaultReason reason, FaultCode code, string action);
12:     public FaultException(TDetail detail, string reason, FaultCode code, string action);
13:
14:     public TDetail Detail { get; }
15: }


FaultException<TDetail>直接继承自FaullException,泛型类型参数表示错误明细类型。通过相应的构造函数在创建FaultException<TDetail>对象的时候指定类型为TDetail的错误明细对象,该对象通过只读属性Detail获取。错误明细类型必须是可序列化的,一般地,我们通将其定义成数据契约的形式。该类型通过FaultContractAttribute特性应用在服务契约相应的操作上面。

当WCF的服务端框架在进行错误提供过程中,将整个FaultException<TDetail>进行序列化并据此生成一个Fault消息,其Detail属性表示的错误明细对象被序列化后的XML作为SOAP Fault的Detail元素。

到此为止,我们分别站在消息交换和编程的角度介绍了SOAP Fault和FaultException异常。在服务执行过程中,我们手工抛出FaultException异常,WCF服务端框架会对该异常对象进行序列化病最终生成Fault消息。当WCF客户端框架介绍到该Fault消息之后,会做一项相反的操作:对Fault消息中进行解析和反序列化,重新生成并抛出FaultException异常。WCF框架自动为我们作了这么多“幕后”工作,使得开发人员可以完全采用编写一般的.NET应用程序的模式进行异常的处理:在错误的地方抛出相应异常,对于潜在出错的方法调用进行相应的异常捕获和处理。

所以,WCF的异常处理框架的核心功能就是实现FaultException异常和Fault消息之间的转换,在[中篇]中,我们着重来讨论这个话题。

作者:​​Artech