服务端只有抛出FaultException异常才能被正常地序列化成Fault消息,并实现向客户端传播。对于一般的异常(比如执行Divide操作抛出的DivideByZeroException),在默认的情况下,异常信息无法实现向客户端传递。但是,倘若为某个服务应用了ServiceDebugBehavior这么一个服务行为,并开启了IncludeExceptionDetailInFaults开关,异常信息将会原封不动地传播到客户端。WCF内部是如何处理抛出的非FaultException异常的呢?
实际上,WCF对非FaultException异常的处理并不复杂,我们现在就来简单介绍一下相关的流程:在执行服务操作过程中,如果抛出一个非FaultException异常,WCF会先判断IncludeExceptionDetailInFaults开发是否开启,如果没有,WCF会手工创建一个MessageFault对象,并根据当前线程的语言文化从资源文件中获取一段固定的文本作为MessageFault的FaultReason(就是我们在《WCF基本的异常处理模式[上篇]》的例子中看到的那段文字)。此外,固定的FaultCode被创建出来作为该MessageFault的Code。最终,WCF将该MessageFault转换成一个Fault消息,并采用固定的Action作为该消息的Action报头。所以,无论服务端抛出怎样的异常,客户端捕获的总是具有相同信息的FaultException异常。
注:客户端的错误信息总是这么一段文字:“由于内部错误,服务器无法处理该请求。有关该错误的详细信息,请打开服务器上的 IncludeExceptionDetailInFaults (从 ServiceBehaviorAttribute 或从 <serviceDebug> 配置行为)以便将异常信息发送回客户端,或在打开每个 Microsoft .NET Framework 3.0 SDK 文档的跟踪的同时检查服务器跟踪日志。”
上面说的是IncludeExceptionDetailInFaults开关关闭的情况。如果IncludeExceptionDetailInFaults开启,WCF则会基于该异常对象创建ExceptionDetail对象,并将该对象作为明细对象创建MessageFault(采用固定FaultCode)。最终,将此MessageFault转换生成Fault消息,当然Action也是采用固定的预定值。因此,在这种情况下,服务端抛出的信息总是能够原封不动地传递到客户端。而客户端捕获的总是一个泛型的FaultException<ExceptionDetail>异常。
一、ExceptionDetail对象为何能被反序列化?
对于异常对象的序列化和反序列化工作,最终都回落在FaultFormatter这么一个对象上(具体原理,可以参考《深入剖析WCF底层异常处理框架实现原理[中篇]》)。无论是虚列化还是反序列化,都具有一个根本的前提:确定对象的类型。FaultFormatter依赖创建时指定的一个FaultContractInfo列表来获知具体的类型,而该列表最初来源于应用在操作方法上FaultContractAttribute特性的定义。
那么,对于应用了ServiceDebugBehavior服务行为,并开启了IncludeExceptionDetailInFaults的场景,客户端如何能够把承载与Fault消息中的表示错误明细的XML片段通过反序列化生成ExceptionDetail对象的呢?由于我们不曾通过FaultContractAttribute特性将ExceptionDetail类型应用在相应的操作方法上面,FaultFormatter无法确定反序列化对象的类型,照理说反序列化是无法成功的,这是为何呢?
其实原因很简单,WCF在初始化FaultFormatter的时候会基于ExceptionDetail类型创建FaultContractInfo对象,并将其添加到属于自己的FaultContractInfo列表中。相应的实现基本上可以通过下面的伪代码体现:
1: internal class FaultFormatter : IClientFaultFormatter, IDispatchFaultFormatter
2: {
3: //其他成员
4: private FaultContractInfo[] faultContractInfos;
5: internal FaultFormatter(SynchronizedCollection<FaultContractInfo> faultContractInfoCollection)
6: {
7: List<FaultContractInfo> list= new List<FaultContractInfo>(faultContractInfoCollection);
8: faultContractInfoCollection.Add(new FaultContractInfo("http://schemas.microsoft.com/net/2005/12/windowscommunicationfoundation/dispatcher/fault", typeof(ExceptionDetail)));
9: this.faultContractInfos = list.ToArray();
10: }
11: }
二、如果直接抛出FaultException<ExceptionDetail>异常呢?
既然FaultFormatter能够自动实现基于ExceptionDetail对象的序列化和反序列化,那么就意味着我们可以在具体的服务操作中直接抛出FaultException<ExceptionDetail>异常,而无须再将ExceptionDetail作为错误契约类型通过FaultContractAttribute特性应用到相应的服务操作上面了。同样以我们的计算服务为例,在Divide方法中我们直接用ExceptionDetail封装在运算过程中抛出的异常,最终抛出FaultException<ExceptionDetail>异常。
1: using System.ServiceModel;
2: using Artech.WcfServices.Contracts;
3: using System;
4: namespace Artech.WcfServices.Services
5: {
6: [ServiceBehavior(Namespace="http://www.artech.com/")]
7: public class CalculatorService : ICalculator
8: {
9: public int Divide(int x, int y)
10: {
11: try
12: {
13: return x / y;
14: }
15: catch (Exception ex)
16: {
17: throw new FaultException<ExceptionDetail>(new ExceptionDetail(ex), ex.Message);
18: }
19: }
20: }
21: }
在客户端,我们就可以直接捕获FaultException<ExceptionDetail>异常了。下面的代码中,我们将捕获的FaultException<ExceptionDetail>异常相关的信息打印出来:
1: using System;
2: using System.ServiceModel;
3: using Artech.WcfServices.Contracts;
4: namespace Artech.WcfServices.Clients
5: {
6: class Program
7: {
8: static void Main(string[] args)
9: {
10: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>(
11: "calculatorservice"))
12: {
13: ICalculator calculator = channelFactory.CreateChannel();
14: using (calculator as IDisposable)
15: {
16: try
17: {
18: int result = calculator.Divide(1, 0);
19: }
20: catch (FaultException<ExceptionDetail> ex)
21: {
22: Console.WriteLine("Action: {0}", ex.Action);
23: Console.WriteLine("Code: {0}:{1}", ex.Code.Namespace,ex.Code.Name);
24: Console.WriteLine("Detail");
25: Console.WriteLine("\tMessage: {0}", ex.Detail.Message);
26: Console.WriteLine("\tType: {0}", ex.Detail.Type);
27: }
28: }
29: }
30: Console.Read();
31: }
32: }
33: }
输出结果(注意Action正好和在FaultFormatter中指定的值是一致的):
1: Action: http://schemas.microsoft.com/net/2005/12/windowscommunicationfoundation/dispatcher/fault
2: Code:http://www.w3.org/2003/05/soap-envelope:Sender
3: Detail:
4: Message:试图除以零。
5: Type:System.DivideByZeroException
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。