在《WCF技术剖析(卷1)》的最后一章,我给出了一个具体的应用WCF的分布式应用实例,我把这个实例命名为PetShop。在这个例子中,我利用WCF的扩展实现了一些设计、架构模式,比如AOP、IoC等。看过本书的读者,一定还记得我还通过WCF扩展实现了于微软企业库(Enterprise Library)异常处理应用块(Exception Handling Application Block:EHAB)的集成。当时由于缺乏相应的背景知识,不可能介绍具体的实现,现在我们可以详细来讲述这是如何实现的。 (Source Code从这里下载)
一、 基本原理介绍
在一个基于WCF的分布式应用中,服务端和客户端需要进行单独的异常处理。在服务端,让EHAB处理抛出的异常是很容易的,我们只需要按照上面代码所示的方式调用ExcpetionPolicy的HandleException方法,传入抛出的异常并指定相应的异常处理策略名称即可。关键的是如何实现让EHAB处理客户端进行服务调用抛出的异常。
我们知道,客户端进行 服务调用抛出的异常类型总是FaultException(包括FaultException<TDetail>)。而EHAB采用的是完全基于异常类型的异常,即抛出的异常类型决定了异常处理方式。也就是说,即使两种完全不同的出错场景,只要抛出的异常具有相同的类型,EHAB就会采用相同的方式来处理该异常。采用这样的方式来直接处理调用WCF服务抛出的异常,显然具有很大的局限:如果服务不错任何处理,客户端捕获的永远是FaultException(不包括FaultException<TDetail>)异常,如果采用EHAB的话,意味着只有一种唯一异常处理方式。当然,在服务端的操作实现中你可以根据具体的场景抛出FaultException<TDetail>异常,并通过不同类型的错误明细(TDetail)封装具体的错误信息,那么客户端就可以针对具体的FaultException<TDetail>异常类型选择不同的方式进行处理。这样使你的异常处理方式是真正的场景驱动的。
理论上来讲,我们需要的正是这种方式的异常处理方式。但是在快速开发中,这样的方式不太具有可操作性,因为异常的一个本质属性就是具有不可预测性。对于某项服务操作,不太可能罗列出所有的错误场景并抛出相应类型的异常。这也正是我们需要一种像EHAB这样一种可配置的异常处理框架的原因,这样我们才能够通过修改相应的配置为某个我们之前没有预知的异常定义相应的异常处理策略。
我们接下来的介绍的解决方案通过一种变通的方式解决了上面的问题,这种方式与通过ServiceDebugBehavior实现异常的传播有点类似:服务端抛出的异常先通过EHAB按照配置好的异常处理策略进行相应的处理。然后将处理后的异常相关的信息(包括异常类型的AssemblyQualifiedName)封装到一个类似于ExceptionDetail的可序列化对象中。最后,以该对象为基础创建MessageFault,并进一步生成Fault消息传回客户端;客户端在接收到该Fault消息后,提取服务端异常相关的信息利用反射重建异常对象(已经明确了异常类型的AssemblyQualifiedName使异常对象的重建变成可能)比将其抛出。那么对于客户端的应用程序来说,就像是捕获从服务端抛出的异常一样了。通过EHAB针对客户端配置的异常处理策略对抛出的异常进行处理,那么这种异常处理方式依然是场景驱动的。
在本例中,我们通过如下一个名称为ServiceExceptionDetail的类型来封装异常相关信息。为了简单起见,我直接让ServiceExceptionDetail继承自ExceptionDetail。由于ServiceExceptionDetail对象需要从服务端向客户端传递,我将其定义成数据契约。在ServiceExceptionDetail仅仅定义了一个唯一的属性成员:AssemblyQualifiedName,表示异常的类型的程序集有效名称,这是为了基于反射的异常重建的需要。在ServiceExceptionDetail中,定义了3个字符串常量表示对应SOAP Fault的SubCode名称和命名空间,以及对应Fault消息的Action。整个解决方法实现的原理大体上可以通过图1示。
using System.Runtime.Serialization;
using System.ServiceModel;
namespace Artech.EnterLibIntegration.WcfExtensions
5: {
)]
class ServiceExceptionDetail : ExceptionDetail
8: {
;
;
;
12:
13: [DataMember]
string AssemblyQualifiedName
private set; }
16:
17: [DataMember]
new ServiceExceptionDetail InnerException
private set; }
20:
public ServiceExceptionDetail(Exception ex)
base(ex)
23: {
this.AssemblyQualifiedName = ex.GetType().AssemblyQualifiedName;
null != ex.InnerException)
26: {
new ServiceExceptionDetail(ex.InnerException);
28: }
29: }
30:
string ToString()
32: {
this.Message;
34: }
35: }
36: }