WCF技术剖析之二十二: 深入剖析WCF底层异常处理框架实现原理[中篇]

时间:2022-04-22
本文章向大家介绍WCF技术剖析之二十二: 深入剖析WCF底层异常处理框架实现原理[中篇],主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

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

一、FaultException异常和Fault消息之间的纽带:MessageFault

对于WCF的异常处理框架,其本身并不直接进行FaultException异常和Fault消息之间的转换,而是通过另外一个作为中介的对象来完成的,这个对象就是这一小节我们讲述的重点:MessageFault。Message(Fault)、MessageFault和FaultException通过如图1描述的“三角”关系实现了相互之间的转化。

图1 Message(Fault)、Message和FaultException“三角”转换关系

在消息介绍MessageFault之前,我们先来看看MessageFault的定义。MessageFault定义在命名空间System.ServiceModel.Channels下,下面的代码是MessageFault的定义。

   1: public abstract class MessageFault
   2: {
   3:    //其他成员
   4:     public static MessageFault CreateFault(Message message, int maxBufferSize);
   5:     public static MessageFault CreateFault(FaultCode code, FaultReason reason);
   6:     public static MessageFault CreateFault(FaultCode code, string reason);
   7:     public static MessageFault CreateFault(FaultCode code, FaultReason reason, object detail);
   8:     public static MessageFault CreateFault(FaultCode code, FaultReason reason, object detail, XmlObjectSerializer serializer);
   9:     public static MessageFault CreateFault(FaultCode code, FaultReason reason, object detail, XmlObjectSerializer serializer, string actor);
  10:     public static MessageFault CreateFault(FaultCode code, FaultReason reason, object detail, XmlObjectSerializer serializer, string actor, string node);
  11:     
  12:     public T GetDetail<T>();
  13:     public T GetDetail<T>(XmlObjectSerializer serializer);
  14:     public XmlDictionaryReader GetReaderAtDetailContents();
  15:  
  16:     public void WriteTo(XmlDictionaryWriter writer, EnvelopeVersion version);
  17:     public void WriteTo(XmlWriter writer, EnvelopeVersion version);
  18:  
  19:     public virtual string Actor { get; }
  20:     public abstract FaultCode Code { get; }
  21:     public abstract bool HasDetail { get; }
  22:     public bool IsMustUnderstandFault { get; }
  23:     public virtual string Node { get; }
  24:     public abstract FaultReason Reason { get; }
  25: }

从上面给出的对MessageFault并不复杂的定义可以看出,它的属性成员和FaultException,以及SOAP Fault的5个子元素是想匹配的:Code、Reason、Node、Actor(对于SOAP 1.2规范中SOAP Fault的Role元素,在SOAP 1.1中的名称为Actor)。而另一个元素Detail则可以通过两个泛型方法GetDetail<T>获得。由于此操作需要对错误明细对象进行反序列化,所以需要指定错误明细类型对应的序列化器,默认情况下采用的是DataContractSerializer。而属性IsMustUnderstandFault表述此错误是否是由于识别 SOAP 标头失败而造成的,实际上,它和FaultCode的IsPredefinedFault向对应,主要具有预定义的Code,IsMustUnderstandFault就返回True。

通过MessageFault众多的CreateFault静态方法,我们可以以不同的组合方式指定构成SOAP Fault的5个元素。如果指定了错误明细对象,需要指定与之匹配的序列化器以实现对其的序列化和反序列化。两个重载的WirteTo方法实行对MessageFault进行序列化,并将序列化后的XML通过XmlDictionaryWriter或者XmlWriter写入掉相应的“流”中。

由于不同的SOAP规范的版本(SOAP 1.1和SOAP 1.2)对Message Fault的结构进行了不同的规定,所有在调用WirteTo的时候需要显式地指定基于那个版本进行写入(SOAP的版本通过EnvelopeVersion表示)。下面的示例代码中,我们创建了一个MessageFault对象,分别针对SOAP 1.1和SOAP 1.2写到两个不同的XML文件中。读者可以仔细辨别最终生成的Message Fault到底有多大的差别。

   1: using System.Collections.Generic;
   2: using System.Diagnostics;
   3: using System.IO;
   4: using System.Runtime.Serialization;
   5: using System.ServiceModel;
   6: using System.ServiceModel.Channels;
   7: using System.Text;
   8: using System.Xml;
   9: namespace MessageFaultDemos
  10: {
  11:     class Program
  12:     {
  13:         static void Main(string[] args)
  14:         {
  15:             FaultCode code = FaultCode.CreateSenderFaultCode(new FaultCode("CalculationError", "http://www.artech.com/"));
  16:             IList<FaultReasonText> reasonTexts = new List<FaultReasonText>();
  17:             reasonTexts.Add(new FaultReasonText("The input parameter is invalid!","en-US"));
  18:             reasonTexts.Add(new FaultReasonText("输入参数不合法!", "zh-CN"));
  19:             FaultReason reason = new FaultReason(reasonTexts);
  20:  
  21:             CalculationError detail = new CalculationError("Divide", "被除数y不能为零!");
  22:             MessageFault fault = MessageFault.CreateFault(code, reason, detail, new DataContractSerializer(typeof(CalculationError)), "http://http://www.artech.com/calculatorservice", "http://http://www.artech.com/calculationcenter");
  23:  
  24:             string fileName1 = @"fault.soap11.xml";
  25:             string fileName2 = @"fault.soap12.xml";
  26:             WriteFault(fault, fileName1, EnvelopeVersion.Soap11);
  27:             WriteFault(fault, fileName2, EnvelopeVersion.Soap12);           
  28:         }
  29:  
  30:         static void WriteFault(MessageFault fault, string fileName, EnvelopeVersion version)
  31:         { 
  32:              using (FileStream stream = new FileStream(fileName, FileMode.Create, FileAccess.Write))
  33:             {
  34:                 using (XmlDictionaryWriter writer = XmlDictionaryWriter.CreateTextWriter(stream, Encoding.UTF8, false))
  35:                 {
  36:                     fault.WriteTo(writer, version);
  37:                     Process.Start(fileName);
  38:                 }
  39:             }
  40:         }
  41:     }
  42: }

基于SOAP 1.1(fault.soap11.xml):

   1: <Fault xmlns="http://schemas.xmlsoap.org/soap/envelope/">
   2:   <faultcode xmlns="" xmlns:a="http://www.artech.com/">a:CalculationError</faultcode>
   3:   <faultstring xml:lang="en-US" xmlns="">The input parameter is invalid!</faultstring>
   4:   <faultactor xmlns="">http://http://www.artech.com/calculatorservice</faultactor>
   5:   <detail xmlns="">
   6:     <CalculationError xmlns="http://www.artech.com/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
   7:       <Message>被除数y不能为零!</Message>
   8:       <Operation>Divide</Operation>
   9:     </CalculationError>
  10:   </detail>
  11: </Fault>

基于SOAP 1.2(fault.soap12.xml):

   1: <Fault xmlns="http://www.w3.org/2003/05/soap-envelope">
   2:   <Code>
   3:     <Value>Sender</Value>
   4:     <Subcode>
   5:       <Value xmlns:a="http://www.artech.com/">a:CalculationError</Value>
   6:     </Subcode>
   7:   </Code>
   8:   <Reason>
   9:     <Text xml:lang="en-US">The input parameter is invalid!</Text>
  10:     <Text xml:lang="zh-CN">输入参数不合法!</Text>
  11:   </Reason>
  12:   <Node>http://http://www.artech.com/calculationcenter</Node>
  13:   <Role>http://http://www.artech.com/calculatorservice</Role>
  14:   <Detail>
  15:     <CalculationError xmlns="http://www.artech.com/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  16:       <Message>被除数y不能为零!</Message>
  17:       <Operation>Divide</Operation>
  18:     </CalculationError>
  19:   </Detail>
  20: </Fault>

二、 如何实现Message(Fault)和MessageFault之间的转换

MessageFault可以作为Message(Fault)和FaultException异常之间进行转换的中介,而且WCF定义个相应的API实现Message和MessageFault,以及MessageFault和FaultException异常之间的转化。我们先来关注一下如果实现Message和MessageFault两种之间的转化。

由于MessageFault定义与Fault消息中主体部分的Fault元素,即SOAP Fault,所以对于一个给定的表示Fault消息的Message对象,我们可以通过提取SOAP Fault对应,从而创建相应的MessageFault对象。MessageFault提供了下面一个CreateFault静态方法,使我们能过传入一个Message对象创建MessageFault(参数maxBufferSize为做大消息缓冲区最大缓冲区大小)。

   1: public abstract class MessageFault
   2: {
   3:    //其他成员
   4:     public static MessageFault CreateFault(Message message, int maxBufferSize);
   5: }

在下面的代码中,借助于Message的静态方法CreateMessage,通过逐个指定FaultCode、FaultReason、Detail和Action的方式创建了一个Fault消息。然后将其传入上述的CreateFault静态方法,从而创建出相应的MessageFault对象。最后通过MessageFault的GetDetail<T>方法得到错误明细对象,通过输出的信息可以证实该MessageFault中的错误明信息和创建消息指定指定的是一致的。

   1: using System;
   2: using System.Collections.Generic;
   3: using System.ServiceModel;
   4: using System.ServiceModel.Channels;
   5: namespace MessageFaultDemos
   6: {
   7:     class Program
   8:     {
   9:         static void Main(string[] args)
  10:         {
  11:             CalculationError detail = new CalculationError("Divide","被除数y不能为零!");
  12:             FaultCode code = FaultCode.CreateSenderFaultCode(new FaultCode("CalculationError", "http://www.artech.com/"));
  13:             IList<FaultReasonText> reasonTexts = new List<FaultReasonText>();
  14:             Message message = Message.CreateMessage(MessageVersion.Soap12WSAddressing10, code, "被除数y不能为零!",
  15:                detail, "http://www.artech.com/calculatorservice/dividecalculationerrorfault");
  16:             MessageFault messageFault = MessageFault.CreateFault(message, int.MaxValue);
  17:             detail = messageFault.GetDetail<CalculationError>();
  18:             Console.WriteLine("Operation: {0}", detail.Operation);
  19:             Console.WriteLine("Message: {0}", detail.Message);
  20:         }
  21:     }
  22: }

输出的结果:

Operation:Devide
Message:被除数y不能为零!

既然我们可以通过提取Fault消息的SOAP Fault进而创建相应的MessageFault,我们同样可以通过给定的MessageFault对象,基于某种消息版本和Action报头,创建一个Fault消息。Message类型中定义的下面一个静态的CreateMessage方法可以帮我们实现这样的操作。

   1: public abstract class Message : IDisposable
   2: {    
   3:     //其他成员
   4:     public static Message CreateMessage(MessageVersion version, MessageFault fault, string action);
   5: }

下面的例子中,我通过MessageFault的CreateFault方法创建了一个MessageFault对象。然后将其传入上述的CreateMessage静态方法,并指定不同的MessageVersion(MessageVersion.Soap11WSAddressingAugust2004和MessageVersion.Soap12WSAddressing10),创建了不同的Fault消息。有兴趣的读者可以仔细分析一下:基于不同的消息版本,针对同一个MessageFault对象创建的Fault消息都有哪些差异(最后能够针对SOAP 1.1、SOAP 1.2、WS-Addressing 2004和WS-Addressing 1.0规范进行比较)。

   1: using System.Diagnostics;
   2: using System.ServiceModel;
   3: using System.ServiceModel.Channels;
   4: using System.Text;
   5: using System.Xml;
   6: namespace MessageFaultDemos
   7: {
   8:     class Program
   9:     {
  10:         static void Main(string[] args)
  11:         {
  12:             MessageFault messageFault = MessageFault.CreateFault(FaultCode.CreateSenderFaultCode("Infrastructure", "http://www.artech.com/"), "Message Timeout");
  13:             Message messageSoap11 = Message.CreateMessage(MessageVersion.Soap11WSAddressingAugust2004, messageFault, "http://www.artech.com/calculatefault");
  14:             Message messageSoap12 = Message.CreateMessage(MessageVersion.Soap12WSAddressing10, messageFault, "http://www.artech.com/calculatefault");
  15:             using (XmlWriter writer1 = new XmlTextWriter("faultmessage.soap11.addressing2004.xml", Encoding.UTF8))
  16:             using (XmlWriter writer2 = new XmlTextWriter("faultmessage.soap12.addressing10.xml", Encoding.UTF8))
  17:             {
  18:                 messageSoap11.WriteMessage(writer1);
  19:                 messageSoap12.WriteMessage(writer2);
  20:             }
  21:             Process.Start("faultmessage.soap11.addressing2004.xml");
  22:             Process.Start("faultmessage.soap12.addressing10.xml");            
  23:         }
  24:        
  25:     }
  26: }

基于SOAP 1.1 + WS-Addressing 2004的Fault消息(faultmessage.soap11.addressing2004.xml):

   1: <s:Envelope xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
   2:   <s:Header>
   3:     <a:Action s:mustUnderstand="1">http://www.artech.com/calculatefault</a:Action>
   4:   </s:Header>
   5:   <s:Body>
   6:     <s:Fault>
   7:       <faultcode xmlns:a="http://www.artech.com/">a:Infrastructure</faultcode>
   8:       <faultstring xml:lang="en-US">Message Timeout</faultstring>
   9:     </s:Fault>
  10:   </s:Body>
  11: </s:Envelope>

基于SOAP 1.2 + WS-Addressing 1.0的Fault消息(faultmessage.soap12.addressing10.xml):

   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/calculatefault</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:Infrastructure</s:Value>
  11:         </s:Subcode>
  12:       </s:Code>
  13:       <s:Reason>
  14:         <s:Text xml:lang="en-US">Message Timeout</s:Text>
  15:       </s:Reason>
  16:     </s:Fault>
  17:   </s:Body>
  18: </s:Envelope>

三、如何实现MessageFault和FaultException之间的转换

上面介绍的是MessageFault和Message(Fault)之间的转化关系,现在我们来介绍Message、Message和FaultException“三角”关系中的另一组转换关系:MessageFault和FaultException之间的转换关系。

WCF将实现MessageFault和FaultException之间的转化的API定义在FaultException类中。其中两个静态CreateFault方法实现将MessageFault向FaultException的转换,而实例方法CreateMessageFault则将FaultException对象转化成相应的MessageFault对象。三个方法定义如下,其中faultDetailTypes代表错误明细类型列表,这是为对FaultException<TDetail>对象的反序列化服务的。

   1: [Serializable]
   2: public class FaultException : CommunicationException
   3: {
   4:     //其他成员
   5:     public static FaultException CreateFault(MessageFault messageFault, params Type[] faultDetailTypes);
   6:     public static FaultException CreateFault(MessageFault messageFault, string action, params Type[] faultDetailTypes);
   7:  
   8:     public virtual MessageFault CreateMessageFault();
   9: }

在下面的实例代码中,先通过调用MessageFault的静态方法CreateFault方法,传入组成一个完整MessageFault相关的参数,创建了一个MesageFault对象。然后调用上面介绍的静态方法CreateFault,创建FaultException对象。由于我们构建MessageFault的时候查传入一个CalculationError作为错误明细,所以返回的异常类型应该是FaultException<CalculationError>对象。最后,我们将该异常对象的相关信息在控制台上输出。

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Runtime.Serialization;
   4: using System.ServiceModel;
   5: using System.ServiceModel.Channels;
   6: namespace MessageFaultDemos
   7: {
   8:     class Program
   9:     {
  10:         static void Main(string[] args)
  11:         {
  12:             FaultCode code = FaultCode.CreateSenderFaultCode(new FaultCode("CalculationError", "http://www.artech.com/"));
  13:             IList<FaultReasonText> reasonTexts = new List<FaultReasonText>();
  14:             reasonTexts.Add(new FaultReasonText("The input parameter is invalid!", "en-US"));
  15:             reasonTexts.Add(new FaultReasonText("输入参数不合法!", "zh-CN"));
  16:             FaultReason reason = new FaultReason(reasonTexts);
  17:             CalculationError detail = new CalculationError("Divide", "被除数y不能为零!");
  18:             MessageFault fault = MessageFault.CreateFault(code, reason, detail, new DataContractSerializer(typeof(CalculationError)), "http://http://www.artech.com/calculatorservice", "http://http://www.artech.com/calculationcenter");
  19:  
  20:             FaultException<CalculationError> exception = FaultException.CreateFault(fault, typeof(CalculationError)) as FaultException<CalculationError>;
  21:             Console.WriteLine("Fault Code: {0}",exception.Code.Name);
  22:             Console.WriteLine("tSubCode: {0}:{1}", exception.Code.SubCode.Namespace,exception.Code.SubCode.Name);
  23:             Console.WriteLine("Fault Reason:");
  24:             foreach (var reasonText in exception.Reason.Translations)
  25:             {
  26:                 Console.WriteLine("t{0}:{1}", reasonText.XmlLang, reasonText.Text);
  27:             }
  28:             Console.WriteLine("Detail:");
  29:             Console.WriteLine("tOperation:{0}", exception.Detail.Operation);
  30:             Console.WriteLine("tMessage:{0}", exception.Detail.Message);
  31:         }
  32:     }
  33: }

输出的结果:

Fault Code: Sender
        SubCode: http://www.artech.com/:CalculationError
Fault Reason:
        en-US:The input parameter is invalid!
        zh-CN:输入参数不合法!
Detail:
        Operation:Divide
        Message:被除数y不能为零!

上面给出的是如果将一个MessageFault对象转换成一个FaultException异常的例子,如果要进行相干的操作,只需要直接调用FaultException异常实例的CreateMessageFault方法即可。清楚了应该调用怎样的API进行MessageFault和FaultException之间的转换,我们现在来进一步深入了解其内部的实现原理。在自身的异常处理框架内容,WCF实际上是通过一个特殊的对象实现两者之间的转换的,这个对象就是我们下面要介绍的FaultFormatter。

四、FaultException与MessageFault转换的核心:FaultFormatter

在《WCF技术剖析(卷1)》的第5章关于序列化和数据契约的介绍中,我们谈到:WCF借助于一个特殊的对象——MessageFormatter,实现方法调用和消息之间的转换。具体来说,客户端通过ClientMessageFormatter将服务操作方法调用转换成请求消息(其中主要涉及对参数对象的序列化),以及将接收到的回复消息转换成服务操作方法对应的返回值或者输出/引用参数(其中只要涉及对返回值或者输出/引用参数的反序列化);服务端则通过DispatchMessageFormatter实现与此相反的操作。

MessageFormatter实现了在正常的服务调用过程中方法调用和消息之间的转换,但是,当异常(这里指的是FaultException异常)从服务端抛出,WCF通过需要一个相似的组件实现类似的功能:在服务端对异常对象进行序列化并生成回复消息(Fault消息),在客户端对接收到的回复消息进行反序列化重建并抛出异常。这样的一个使命由FaultFormatter担当,不过,由于MessageFault是FaultException和Fault消息进行转换的中介,所以FaultFormatter并不直接进行两者之间的转换,而是实现FaultException和MessageFault之间的转换。

严格地说来,FaultFormatter仅仅是WCF一个内部对象,但是对该对象的深刻认识将非常有助于我们有效的理解WCF整个异常处理机制。FaultFormatter在客户端和服务端所扮演的角色是不同的:客户端将通过解析回复Fault消息生成的MessageFault转换成FaultException异常,以便后续的步骤建起抛出;服务端在将抛出的FaultException异常转换成MessageFault,以便后续的步骤生成相应的Fault消息。客户端和服务端这种职责的不同可以通过下面两个接口的定义看出来:

internal interface IClientFaultFormatter
{
    FaultException Deserialize(MessageFault messageFault, string action);
}
internal interface IDispatchFaultFormatter
{
    MessageFault Serialize(FaultException faultException, out string action);
}

内部(Internal)接口IClientFaultFormatter和IDispatchFaultFormatter分别定义了FaultFormatter在客户端和服务端的职能,即它们分别实现对FaultException对象的反序列化和序列化。在对FaultException对象进行序列化需要提取Action属性作为Fault消息的Action报头;而将MessageFault进行反序列化生成FaultException对象的时候需要从外部指定Action属性的值,所以两个方法各有一个action参数。

WCF定义了一个内部类System.ServiceModel.Dispatcher.FaultFormatter实现了这两个接口,并将其作为服务端和客户端的FaultFormatter。下面是FaultFormatter类型的定义:

internal class FaultFormatter : IClientFaultFormatter, IDispatchFaultFormatter
{
    public FaultException Deserialize(MessageFault messageFault, string action);
    public MessageFault Serialize(FaultException faultException, out string action);
}

由于WCF将绝大部分序列化和反序列化的工作都交付给两个序列化器:DataContractSerializer和XmlSerializerObjectSerializer,对于FaultException异常对象的序列化自然也不例外。为此,WCF定义了两个具体的类型System.ServiceModel.Dispatcher.DataContractSerializerFaultFormatter和System.ServiceModel.Dispatcher.XmlSerializerFaultFormatter。它们直接继承自FaultFormatter,分别采用DataContractSerializer和XmlSerializerObjectSerializer作为相应的序列化器。IClientFaultFormatter、IDispatchFaultFormatter、FaultFormatter、DataContractSerializerFaultFormatter和XmlSerializerFaultFormatter之间的关系可以简单地通过图2所示的类图表示。

图2 FaultFormatter体系结构