第6章
错误
不管是哪一种服务操作,在任意时刻都可能遭遇一些不可预期的错误。问题在于如何将错误报告给客户端。异常与异常处理机制是与特定的技术紧密结合的,不能够跨越服务边界。此外,错误处理通常都属于本地的实现细节,不会影响到客户端。这样设计的原因在于客户端不需要关心错误的细节(以及发生错误的事实),但最主要的还是因为在设计良好的应用程序中,服务是被封装的,因此客户端无法知道有关错误的信息。设计良好的服务应尽可能是自治的,不能依赖于客户端去处理或恢复错误。任何非空的错误通知都应该是客户端与服务之间契约交互的一部份。本章介绍了服务与客户端如何处理这些声明的错误,以及如何扩展与改善这样一种基础的实现机制。
错误与异常
在传统的.NET编程中,任何未经处理的异常都会立刻终止抛出异常的进程。但WCF却与之大相径庭。如果代表某个客户端的服务调用导致异常,并不会结束宿主进程,其他客户端仍然可以访问该服务,托管在相同进程中的其他服务也不会受到影响。因此,当一个未经处理的异常离开服务的范围时,分发器会捕获它,并将它序列化到返回消息中传递给客户端。当返回消息到达代理时,代理会在客户端抛出一个异常。
当客户端试图调用服务时,实际上可能会遭遇三种错误类型。第一种错误类型为通信错误,例如网络故障、地址错误、宿主进程没有运行等。客户端的通信错误表现为CommunicationException异常。
客户端可能遇到的第二种错误类型与代理和通道的状态有关,例如试图访问已经关闭的代理,就会导致ObjectDisposedException异常。或者契约与绑定的安全保护级别不相匹配,也会出现错误。
第三种错误类型源于服务调用。这种错误既可能是服务抛出的异常,也可能是服务在调用其他对象或资源时,通过内部调用抛出的异常。这些错误正是本章所要讲述的主题。
出于封装与解耦的目的,在默认情况下,所有服务端抛出的异常总是以FaultException类型到达客户端:
public class FaultException : CommunicationException
{...}
如果要解耦客户端与服务,就应该对所有的服务异常一视同仁。客户端知道服务端发生的内容越少,则两者之间关系的解耦才越彻底。
异常与实例管理
当服务实例出现异常时,WCF并不会关闭宿主进程,但错误可能会影响服务实例,同时还会影响到客户端继续使用代理(事实上是通道)访问服务的能力。准确的说,异常对于客户端与服务实例的影响与服务的实例模式有关。
单调服务与异常
如果调用引发异常,那么紧跟在异常之后,服务实例会被释放,代理将在客户端抛出FaultException异常。在默认情况下,所有服务抛出的异常(包括FaultException的派生类)会使得通道发生错误。即使客户端捕获了异常,它也不能发出随后的调用,因为它们会引发一个CommunicationObjectFaultedException异常。此时,客户端只能关闭代理。
会话服务与异常
无论使用何种WCF会话绑定,在默认情况下,所有异常(包括FaultException的派生类)都会终止会话。WCF将会释放实例,而客户端则获得一个FaultException异常。即使客户端捕获了该异常,也不能继续使用代理,因为随后的调用会引发一个CommunicationObjectFaultedException异常。客户端唯一可以安全执行的就是关闭代理,因为一旦参与会话的服务实例遇到了错误,会话就不能再使用了。
单例服务与异常
当我们调用单例服务时,如果遇到异常,单例实例并不会终止,而是继续运行。在默认情况下,所有异常(包括FaultException的派生类)都会导致通道发生错误,客户端无法发出随后的调用,只能关闭代理。如果客户端包含了一个单例实例的会话,那么会话会终止。
错误
异常的根本问题在于它们与特定的技术紧密结合,因此无法跨越服务边界被调用两端共享。若要考虑良好的互操作性,我们就需要将基于特定技术的异常映射为某种与平台无关的错误信息。这种表现形式就是所谓的SOAP错误(SOAP Fault)。SOAP错误基于一种行业标准,它不依赖于任何一种诸如CLR异常、Java异常或C++异常之类的特定技术的异常。若要为了抛出一个SOAP错误(或者简称错误),服务就不能抛出一个传统的CLR异常,而是抛出一个FaultException<T>类的实例,它的定义如例6-1所示:
例6-1:FaultException<T>类
[Serializable] //更多特性
public class FaultException : CommunicationException
{
public FaultException();
public FaultException(string reason);
public FaultException(FaultReason reason);
public virtual MessageFault CreateMessageFault();
//更多成员
}
[Serializable]
public class FaultException<T> : FaultException
{
public FaultException(T detail);
public FaultException(T detail,string reason);
public FaultException(T detail,FaultReason reason);
//更多成员
}
FaultException<T>是FaultException的特化,因此任何针对FaultException异常进行编程的客户端都能够处理FaultException<T>类型。
FaultException<T>的类型参数T负责传递错误细节(Error Detail)。没有要求错误细节必须为Exception的派生类,它可以是任何类型。唯一的约束是该类型必须支持序列化,或者必须是数据契约。
例6-2演示了一个简单的计算器服务,在实现Divide()方法时,如果除数为0,则抛出一个FaultException<DivideByZeroException>异常。
例6-2:抛出FaultException<T>异常
[ServiceContract]
interface ICalculator
{
[OperationContract]
double Divide(double number1,double number2);
//更多方法
}
class Calculator : ICalculator
{
public double Divide(double number1,double number2)
{
if(number2 == 0)
{
DivideByZeroException exception = new DivideByZeroException();
throw new FaultException<DivideByZeroException>(exception);
}
return number1 / number2;
}
//其余的实现
}
除了FaultException<DivideByZeroException>异常,服务还能够抛出参数不是Exception派生类类型的异常:
throw new FaultException<double>();
但是,将Exception派生类作为错误细节类型更加符合传统的.NET编程实践,代码也具有更强的可读性。此外,它允许实现后面将要讨论的异常提升(Exception Promotion)功能。
传递给FaultException<T>构造函数的reason参数作为异常消息,因此我们可以传递纯粹的字符串作为reason参数的值:
DivideByZeroException exception = new DivideByZeroException();
throw new FaultException<DivideByZeroException>(exception,"number2 is 0");
如果要求本地化,更有效的方式是传递FaultReason对象。





