由于WCF采用.NET托管语言(C#和NET)作为其主要的编程语言,注定以了基于WCF的编程方式不可能很复杂。同时,WCF设计的一个目的就是提供基于非业务逻辑的通信实现,为编程人员提供一套简单易用的应用编程接口(API)。WCF编程模式的简单性同样体现在异常处理上面,本篇文章的主要目的就是对WCF基于异常处理的编程模式做一个简单的介绍。
一、当异常从服务端抛出
对于一个典型的WCF服务调用,我个人倾向于将潜在抛出的异常费为两种类型:应用异常(Application Exception)和基础结构(Infrastructure Exception)。前者为应用级别,主要体现为执行某个服务操作的业务逻辑抛出的异常;而后者则是业务无关的,通过WCF本身的基础架构抛出,主要体现在对象的序列化、消息的处理、消息传输和消息的分发等等。在这里我们更多地关注与应用异常。
首先,我们在不做任何异常处理相关操作的情况下,看看如果在服务端执行某个服务操作的过程中抛出异常后,客户端会得到怎样的结果。我们通过实例的形式来演示这中场景。处于简单和易于理解考虑,我们照例沿用计算服务的例子。
我们照例采用典型的四层结构(Contract、Service、Hosting和Client),具体的层次在VS解决方案的划分如图1所示:
图1 异常抛出实例解决方案结构
下面代码片断表示服务契约(ICalculator)和服务类型(CalculatorService)的定义。为了简洁,在服务契约接口中,我们仅仅定义了唯一一个用于进行两个整数触发预算的方法Divide。服务契约和服务类型类型分别定义在项目Contracts和Services中。
1: using System.ServiceModel;
2: namespace Artech.WcfServices.Contracts
3: {
4: [ServiceContract(Namespace = "http://www.artech.com/")]
5: public interface ICalculator
6: {
7: [OperationContract]
8: int Divide(int x, int y);
9: }
10: }
1: using Artech.WcfServices.Contracts;
2: namespace Artech.WcfServices.Services
3: {
4: public class CalculatorService : ICalculator
5: {
6: public int Divide(int x, int y)
7: {
8: return x / y;
9: }
10: }
11: }
接下来是通过Console应用程序(Hosting项目)对上面定义的WCF服务(CalculatorService)进行寄宿(Host)的代码和相关配置。
1: using System;
2: using System.ServiceModel;
3: using Artech.WcfServices.Services;
4: namespace Artech.WcfServices.Hosting
5: {
6: public class Program
7: {
8: static void Main(string[] args)
9: {
10: using (ServiceHost host = new ServiceHost(typeof(CalculatorService)))
11: {
12:
13: host.Open();
14: Console.Read();
15: }
16: }
17: }
18: }
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <services>
5: <service name="Artech.WcfServices.Services.CalculatorService">
6: <endpoint address="http://127.0.0.1:3721/calculatorservice" binding="wsHttpBinding" contract="Artech.WcfServices.Contracts.ICalculator" />
7: </service>
8: </services>
9: </system.serviceModel>
10: </configuration>
最后在代表客户端的Console应用程序(Client项目)中对计算服务CalculatorService进行调用。相关的服务调用代码和配置如下所示,为了让服务端在执行Divide操作的时候抛出异常,特意将第二个参数设置为0,以便服务在进行除法运算的时候抛出System.DivideByZeroException异常。
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: int result = calculator.Divide(1, 0);
17: }
18: }
19: }
20: }
21: }
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <client>
5: <endpoint address="http://127.0.0.1:3721/calculatorservice"
6: binding="wsHttpBinding" contract="Artech.WcfServices.Contracts.ICalculator" name="calculatorservice" />
7: </client>
8: </system.serviceModel>
9: </configuration>
在启动服务寄宿程序(Hosting)后执行客户端服务调用程序,在客户端将会跑出如图2所示的类型为System.ServiceModel.FaultException的异常,其错误消息为:
“由于内部错误,服务器无法处理该请求。有关该错误的详细信息,请打开服务器上的 IncludeExceptionDetailInFaults (从 ServiceBehaviorAttribute 或从 <serviceDebug> 配置行为)以便将异常信息发送回客户端,或在打开每个 Microsoft .NET Framework 3.0 SDK 文档的跟踪的同时检查服务器跟踪日志。”
图2 客户端捕获从服务端抛出的异常
从上面的实例演示中,我们可以获知WCF在默认情况下的异常处理行为:对于服务端抛出的异常(这里主要指应用异常),客户端捕获到的总一个具有相同异常消息的System.ServiceModel.FaultException异常。由于异常类型和消息固定不变,对于服务的客户端来说,直接通过捕获到的异常相关的信息是无法确定服务端在执行服务操作的时候遇到的具体的错误是什么。
WCF如此设计的一个主要的目的为了安全。原因很简单,由于我们不能保证服务端直接抛出的异常不包含任何敏感信息,所以直接将服务端原始的异常信息暴露给客户端(对于服务提供者来说,该客户端可能使一个不受信任或者部完全受信任的第三方)。
二、 异常细节的传输
通过上面的介绍,我们已经意识到了:在默认的情况下,如果异常(主要指应用异常)在执行服务操作的过程中抛出,其真正的异常信息并不能被客户端捕获。实际上,服务端具体的异常细节信息仅限于服务端可见,并不会传递到客户端。
然后,不论对于开发阶段的调试,还是维护阶段的纠错、排错,如果在客户端调用某个服务操作后能够很直接地获取到从服务端抛出异常的所有细节,这无疑是一件很有价值的事情。那么,WCF能够做到这一点呢?答案是肯定的。
实际上,对于细心的读者,看到客户端捕获的FaultException异常的消息,就能从中找到解决方案。消息中指出,如果试图得到服务端具体的错误信息,需要开启IncludeExceptionDetailInFaults这么一个开关。具体来讲,又具有两种等效的方式:配置的方式和应用自定义特性(Custom Attribute)的方式。
通过在服务端的配置中,为寄宿的服务定义相应的服务行为(Service Behavior),并把serviceDebug配置项的includeExceptionDetailInFaults属性设为True。具体配置如下所示:
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <behaviors>
5: <serviceBehaviors>
6: <behavior name="serviceDebuBehavior">
7: <serviceDebug includeExceptionDetailInFaults="true" />
8: </behavior>
9: </serviceBehaviors>
10: </behaviors>
11: <services>
12: <service behaviorConfiguration="serviceDebuBehavior" name="Artech.WcfServices.Services.CalculatorService">
13: <endpoint address="http://127.0.0.1:3721/calculatorservice" binding="wsHttpBinding" contract="Artech.WcfServices.Contracts.ICalculator" />
14: </service>
15: </services>
16: </system.serviceModel>
17: </configuration>
大部分系统自定义服务行为都可以直接通过在服务类型上应用System.ServiceModel.ServiceBehaviorAttribute这么一个自定义特性一样,includeExceptionDetailInFaults服务调试(ServiceDebug)行为也不另外。在ServiceBehaviorAttribute中定义了一个IncludeExceptionDetailInFaults属性,当我们将ServiceBehaviorAttribute特性应用到具体的服务类型上的时候,只需将此属性设为true即可。
1: [AttributeUsage(AttributeTargets.Class)]
2: public sealed class ServiceBehaviorAttribute : Attribute, IServiceBehavior
3: {
4: //其他成员
5: public bool IncludeExceptionDetailInFaults { get; set; }
6: }
所以如果不采用上面的配置,在服务类型CalculatorService上面应用ServiceBehaviorAttribute特性,并进行如下的设置,也可以到达相同的效果。
1: using Artech.WcfServices.Contracts;
2: using System.ServiceModel;
3: namespace Artech.WcfServices.Services
4: {
5: [ServiceBehavior(IncludeExceptionDetailInFaults = true)]
6: public class CalculatorService : ICalculator
7: {
8: //省略服务成员
9: }
10: }
当IncludeExceptionDetailInFaults被开启的ServiceDebug服务属性通过上述两种方式应用到我们例子中的服务CalculatorService的情况下,运行客户端应用程序,将会捕获包含有错误明细信息的异常,运行的结果如图3所示:
图3 客户端捕获到具有明细信息的异常
从图3中,我们可以看出客户端捕获到的实际上是一个泛型的System.ServiceModel.FaultException<TDetail>异常。FaultException<TDetail>继承自FaultException,这两种典型的异常类型在WCF异常处理中具有重要的地位,在本章后续章节中还会重点讲述,在这里先做一点简单的介绍。
对于所有从服务端抛出的异常,只有FaultException和直接或间接继承自FaultException的异常才能被序列化,并最终通过消息返回给服务的调用端。FaultException可以通过文本的形式保存相应的错误信息。FaultException<TDetail>在FaultException现有的基础上,增加了一个额外的特性:将错误信息通过一个具体的对象表示,其类型便是范型类型TDetail,该对象可以通过属性Detail设置或者获取。
1: [Serializable]
2: public class FaultException<TDetail> : FaultException
3: {
4: // 其他成员
5: public FaultException(TDetail detail);
6: public TDetail Detail { get; }
7: }
对于上面例子对应的场景,客户端捕获的异常类型实际上是FaultException< System.ServiceModel.ExceptionDetail>,也就是说其具体的泛型类型参数为System.ServiceModel.ExceptionDetail。ExceptionDetail的定义如下:
1: [DataContract]
2: public class ExceptionDetail
3: {
4: // 其他成员
5: public ExceptionDetail(Exception exception);
6:
7: [DataMember]
8: public string HelpLink { get; private set; }
9: [DataMember]
10: public ExceptionDetail InnerException { get; private set; }
11: [DataMember]
12: public string Message { get; private set; }
13: [DataMember]
14: public string StackTrace { get; private set; }
15: [DataMember]
16: public string Type { get; private set; }
17: }
ExceptionDetail是一个数据契约(Data Contract),也就意味ExceptionDetail是一个可以被DataContractSerializer进行序列化的对象。再仔细察看具体的属性成员列表,我想很多读者肯定有一种是曾相识的感觉:是不是和System.Exception的属性成员定义很相似。实际上,ExceptionDetail是WCF专门设计出来用于封装服务端抛出的异常信息的,其个属性HelpLink、InnerException和StackTrace各自和System.Exception的同名属性向对应,而属性Type表示异常的类型。
也就是说,对于应用了开启IncludeExceptionDetailInFaults的ServiceDebug服务行为的WCF服务,在执行服务操作抛出的异常信息,可以通过包含在客户端捕获的FaultException<ExceptionDetail>异常中的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("Message:{0}", ex.Detail.Message);
23: Console.WriteLine("Type:{0}", ex.Detail.Type);
24: Console.WriteLine("StackTrace:{0}", ex.Detail.StackTrace);
25: Console.WriteLine("HelpLink:{0}", ex.Detail.HelpLink);
26: (calculator as ICommunicationObject).Abort();
27: }
28: }
29: }
30: }
31: }
32: }
输出结果:
1: Message:试图除以零。
2: Type:System.DivideByZeroException
3: StackTrace: 在 Artech.WcfServices.Services.CalculatorService.Divide(Int32 x, Int32 y) 位置 D:/Demos/Artech.WcfServices/Services/CalculatorService.cs:行号 13
4: 在 SyncInvokeDivide(Object , Object[] , Object[] )
5: 在 System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]&; outputs)
6: 在System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc&; rpc)
7: 在 System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc&; rpc)
8: 在 System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage4(MessageRpc&; rpc)
9: 在 System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage3(MessageRpc&; rpc)
10: 在 System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage2(MessageRpc&; rpc)
11: 在 System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage1(MessageRpc&; rpc)
12: 在 System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)
13: HelpLink:
注:在catch程序块中,我们通过代码((calculator as ICommunicationObject).Abort();)将会话信道强行中断。原因在于,对于基于会话信道(Sessionful Channel)的服务调用,服务端抛出的异常会将该信道的状态转变为出错状态(Faulted),处于Faulted状态的会话信道将不能再用于后续的通信,即使你调用Close方法将其关闭。在这种情况下,需要调用Abort方法对其进行强行中止。具体的原理,在《WCF技术剖析(卷1)》的第9章有详细的介绍。
对于服务行为SerivceDebug的IncludeExceptionDetailInFaults属性,我需要再次重申一遍:由于会导致敏感信息泄露的潜在危险,一般地我们仅仅在调试的时候才会开启该属性。对于已经发布、付诸使用的服务,这个开关一般是关闭的。实际上,我们从这个服务行为的命名也可以看出,SerivceDebug,也是用于调试服务的服务行为罢了。