服务端只有抛出异常才能被正常地序列化成Fault消息,并实现向客户端传播。对于一般的异常(比如执行Divide操作抛出的),在默认的情况下,异常信息无法实现向客户端传递。但是,倘若为某个服务应用了这么一个服务行为,并开启了IncludeExceptionDetailInFaults开关,异常信息将会原封不动地传播到客户端。WCF内部是如何处理抛出的非异常的呢?
实际上,WCF对非异常的处理并不复杂,我们现在就来简单介绍一下相关的流程:在执行服务操作过程中,如果抛出一个非异常,WCF会先判断IncludeExceptionDetailInFaults开发是否开启,如果没有,WCF会手工创建一个对象,并根据当前线程的语言文化从资源文件中获取一段固定的文本作为的(就是我们在《》的例子中看到的那段文字)。此外,固定的被创建出来作为该的Code。最终,WCF将该转换成一个Fault消息,并采用固定的Action作为该消息的Action报头。所以,无论服务端抛出怎样的异常,客户端捕获的总是具有相同信息的异常。
注:客户端的错误信息总是这么一段文字:“由于内部错误,服务器无法处理该请求。有关该错误的详细信息,请打开服务器上的 IncludeExceptionDetailInFaults (从 ServiceBehaviorAttribute 或从 <serviceDebug> 配置行为)以便将异常信息发送回客户端,或在打开每个 Microsoft .NET Framework 3.0 SDK 文档的跟踪的同时检查服务器跟踪日志。”
上面说的是IncludeExceptionDetailInFaults开关关闭的情况。如果IncludeExceptionDetailInFaults开启,WCF则会基于该异常对象创建对象,并将该对象作为明细对象创建(采用固定FaultCode)。最终,将此转换生成Fault消息,当然Action也是采用固定的预定值。因此,在这种情况下,服务端抛出的信息总是能够原封不动地传递到客户端。而客户端捕获的总是一个泛型的FaultException<ExceptionDetail>异常。
一、ExceptionDetail对象为何能被反序列化?
对于异常对象的序列化和反序列化工作,最终都回落在FaultFormatter这么一个对象上(具体原理,可以参考《》)。无论是虚列化还是反序列化,都具有一个根本的前提:确定对象的类型。FaultFormatter依赖创建时指定的一个列表来获知具体的类型,而该列表最初来源于应用在操作方法上特性的定义。
那么,对于应用了服务行为,并开启了IncludeExceptionDetailInFaults的场景,客户端如何能够把承载与Fault消息中的表示错误明细的XML片段通过反序列化生成对象的呢?由于我们不曾通过特性将类型应用在相应的操作方法上面,FaultFormatter无法确定反序列化对象的类型,照理说反序列化是无法成功的,这是为何呢?
其实原因很简单,WCF在初始化FaultFormatter的时候会基于类型创建对象,并将其添加到属于自己的列表中。相应的实现基本上可以通过下面的伪代码体现:
1: internal class FaultFormatter : IClientFaultFormatter, IDispatchFaultFormatter
2: {
3: //其他成员
4: private FaultContractInfo[] faultContractInfos;
5: internal FaultFormatter(SynchronizedCollectionfaultContractInfoCollection)
6: {
7: Listlist= new List (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能够自动实现基于对象的序列化和反序列化,那么就意味着我们可以在具体的服务操作中直接抛出FaultException<ExceptionDetail>异常,而无须再将作为错误契约类型通过特性应用到相应的服务操作上面了。同样以我们的计算服务为例,在Divide方法中我们直接用封装在运算过程中抛出的异常,最终抛出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(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 (ChannelFactorychannelFactory = new ChannelFactory (
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 (FaultExceptionex)
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