错误契约
在默认情况下,服务抛出的异常均以FaultException类型传递到客户端。原因在于任何服务希望与客户端共享的基于通信错误之上的任何异常,都必须属于服务契约行为的一部分。为此,WCF提供了错误契约,通过它列出服务能够抛出的错误类型。这些错误类型应该与FaultException<T>使用的类型参数的类型相同。只要它们在错误契约中列出,WCF客户端就能够分辨契约错误与其他错误之间的区别。
服务可以使用FaultContractAttribute特性定义它的错误契约:
[AttributeUsage(AttributeTargets.Method,AllowMultiple = true,Inherited = false)] public sealed class FaultContractAttribute : Attribute { public FaultContractAttribute(Type detailType); //更多成员 }
|
我们可以将FaultContract特性直接应用到契约操作上,指定错误细节类型,如例6-3所示。
例6-3:定义错误契约
[ServiceContract] interface ICalculator { [OperationContract] double Add(double number1,double number2); [OperationContract] [FaultContract(typeof(DivideByZeroException))] double Divide(double number1,double number2); //更多方法 }
|
FaultContract特性只对标记了它的方法有效。只有这样的方法才能抛出错误,并将它传递给客户端。此外,如果操作抛出的异常没有包含在契约中,则以普通的FaultException形式传递给客户端。为了传递异常,服务必须抛出与错误契约所列完全相同的细节类型。例如,若要满足如下的错误契约定义:
[FaultContract(typeof(DivideByZeroException))] |
服务必须抛出FaultException<DivideByZeroException>异常。服务甚至不能抛出错误契约的细节类型的子类,因为它要求异常要满足契约的定义:
[ServiceContract] interface IMyContract { [OperationContract] [FaultContract(typeof(Exception))] void MyMethod(); } class MyService : IMyContract { public void MyMethod() { //不满足契约的要求 throw new FaultException<DivideByZeroException> (new DivideByZeroException()); } } |
FaultContract特性支持重复配置,可以在单个操作中列出多个错误契约:
[ServiceContract] interface ICalculator { [OperationContract] [FaultContract(typeof(InvalidOperationException))] [FaultContract(typeof(string))] double Add(double number1,double number2); [OperationContract] [FaultContract(typeof(DivideByZeroException))] double Divide(double number1,double number2); //更多方法 }
|
如上的代码允许服务抛出契约定义中的任何一种异常,并将它们传递给客户端。
警告:我们不能为单向操作提供错误契约,因为从理论上讲,单向操作是没有返回值的:
//无效定义 [ServiceContract] interface IMyContract { [OperationContract(IsOneWay = true)] [FaultContract(...)] void MyMethod(); } |
如果这样做,就会在装载服务时引发InvalidOperationException异常。
错误处理
错误契约与其他服务元数据一同发布。当WCF客户端导入该元数据时,契约定义包含了错误契约,以及错误细节类型的定义。错误细节类型的定义包含了相关的数据契约。如果细节类型是某个包含了各种专门字段的定制异常,那么错误细节类型对数据契约的支持就显得格外重要了。
客户端期望能够捕获和处理导入的错误类型。例如,在针对例6-3所示的契约编写客户端时,客户端能够捕获FaultException<DivideByZeroException>异常:
CalculatorClient proxy = new CalculatorClient(); try { proxy.Divide(2,0); proxy.Close(); } catch(FaultException<DivideByZeroException> exception) {...} catch(CommunicationException exception) {...}
|
注意,客户端仍然可能引发通信异常。
客户端可以采用处理FaultException基类异常的方式,统一地处理所有与通信无关的服务端异常:
CalculatorClient proxy = new CalculatorClient(); try { proxy.Divide(2,0); proxy.Close(); } catch(FaultException exception) {...} catch(CommunicationException exception) {...}
|
注意:当客户端的开发者通过在客户端移除错误契约,手动修改导入契约的定义时,情况就变得复杂了。此时,如果服务抛出的异常是在服务端错误契约的列表之中,该异常在客户端会被表示为FaultException,而不是契约错误。
当服务抛出的异常属于服务端错误契约中列举的异常时,异常不会导致通信通道出现错误。客户端能够捕获该异常,继续使用代理,或者安全地关闭代理。
未知错误
FaultException<T>类继承自FaultException类。服务(或者服务使用的所有下游对象)可以直接抛出FaultException实例:
throw new FaultException("Some Reason");
|
FaultException是一种特殊的异常类型,称之为未知错误(Unknown Fault)。一个未知错误以FaultException类型传递到客户端。它不会使通信通道发生错误,因此客户端能够继续使用代理,就好像该异常属于错误契约中的一部分那样。
注意:服务抛出的FaultException<T>异常总是以FaultException<T>或者FaultExcetion类型到达客户端。如果没有错误契约(或者T没有包含在契约中),则服务抛出的FaultExcetion和FaultException<T>异常则以FaultException类型到达客户端。
客户端异常对象的Message属性可以被设置为FaultException的reason构造参数。FaultException对象主要被服务的下游对象使用,这些对象并不知道它们正在调用的服务所使用的错误契约。为避免这些下游对象与顶层服务之间的耦合,同时又不希望通道发生错误,就应该抛出FaultException异常。如果这些下游对象希望客户端能够处理独立于其他通信错误的异常,同样应该抛出FaultException异常。
调试错误
如果服务已经部署,那么最佳方案就是解除该服务与调用它的客户端之间的耦合,在服务的错误契约中,声明最少的异常类型,提供最少的错误信息。但如果是在测试与调试期间,用途更大的却是在返回给客户端的信息中包含所有的异常。它可以使得开发者使用一个测试客户端与调试器分析错误源,而不必处理完全封装的不透明的FaultException。为此,我们应使用ExceptionDetail类,它的定义如下:
[DataContract] public class ExceptionDetail { public ExceptionDetail(Exception exception); [DataMember] public string HelpLink {get;private set;} [DataMember] public ExceptionDetail InnerException {get;private set;} [DataMember] public string Message {get;private set;} [DataMember] public string StackTrace {get;private set;} [DataMember] public string Type {get;private set;} } |
我们需要创建一个ExceptionDetail实例,然后通过要传递给客户端的异常对它进行初始化。接着,我们将ExceptionDetail的实例作为构造参数,同时提供一个最初的异常消息作为错误原因,抛出一个FaultException<ExceptionDetail>异常对象,而不能抛出不规则的异常。这一过程如例6-4所示。
例6-4:在错误消息中包含服务异常
[ServiceContract] interface IMyContract { [OperationContract] void MethodWithError(); } class MyService : IMyContract { public void MethodWithError() { InvalidOperationException exception = new InvalidOperationException("Some error"); ExceptionDetail detail = new ExceptionDetail(exception); throw new FaultException<ExceptionDetail>(detail,exception.Message); } }
|
如此做法可以使客户端能够发现最初的异常类型和消息。客户端的错误对象定义了Detail.Type属性,它包含了最初的服务异常名,而Message属性则包含了最初的异常消息。例6-5演示的客户端代码对例6-4抛出的异常进行了处理。
例6-5:处理包含的异常
MyContractClient proxy = new MyContractClient(endpointName); try { proxy.MethodWithError(); } catch(FaultException<ExceptionDetail> exception) { Debug.Assert(exception.Detail.Type == typeof(InvalidOperationException).ToString()); Debug.Assert(exception.Message == "Some error"); }
|
以声明方式包含异常
ServiceBehavior特性定义了Boolean类型的属性IncludeExceptionDetailInFaults,如下所示:
[AttributeUsage(AttributeTargets.Class)] public sealed class ServiceBehaviorAttribute : Attribute, ... { public bool IncludeExceptionDetailInFaults {get;set;} //更多成员 } IncludeExceptionDetailInFaults属性的默认值为false。如下的代码段将它的值设置为true: [ServiceBehavior(IncludeExceptionDetailInFaults = true)] class MyService : IMyContract {...}
|
它的功能与例6-4相同,但它却能够自动包含异常:为了客户端程序能够处理它们,服务或服务的下游对象抛出的所有非契约型错误与异常,都将传递给客户端,在返回的错误消息中包含这些异常,正如例6-5所演示的那样:
[ServiceBehavior(IncludeExceptionDetailInFaults = true)] class MyService : IMyContract { public void MethodWithError() { throw new InvalidOperationException("Some error"); } }
|
服务(或服务的下游对象)抛出的任何错误,只要是在错误契约中列出的,都不会受到影响,并被传递给客户端。
包含所有的异常有利于调试,但必须避免发布和部署IncludeExceptionDetailInFaults属性值为true的服务。若要自动实现这一步骤,以避免潜在的缺陷,可以使用条件编译,如例6-6所示。
例6-6:调试状态(译注1)下设置IncludeExceptionDetailInFaults的值为true
public static class DebugHelper { public const bool IncludeExceptionDetailInFaults = #if DEBUG true; #else false; #endif } [ServiceBehavior(IncludeExceptionDetailInFaults = DebugHelper.IncludeExceptionDetailInFaults)] class MyService : IMyContract {...}
|
警告:当IncludeExceptionDetailInFaults属性值为true时,异常实际上会导致通道发生错误,因而客户端不能发出随后的调用。
宿主与异常诊断
显然,在错误消息中包含所有异常有助于调试,同时也可以用于分析已部署服务存在的问题。值得庆幸的是,WCF允许我们选择编程方式或管理方式设置宿主配置文件,通过宿主将IncludeExceptionDetailInFaults的值设置为true。如果以编程方式设置,就需要在打开宿主之前查找服务描述中的服务行为,然后设置IncludeException-DetailInFaults属性值:
ServiceHost host = new ServiceHost(typeof(MyService)); ServiceBehaviorAttribute debuggingBehavior = host.Description.Behaviors.Find<ServiceBehaviorAttribute>(); debuggingBehavior.IncludeExceptionDetailInFaults = true; host.Open();
|
可以在ServiceHost<T>中封装这一过程,实现简化,如例6-7所示。
例6-7:ServiceHost<T>与返回的未知异常
public class ServiceHost<T> : ServiceHost { public bool IncludeExceptionDetailInFaults { set { if(State == CommunicationState.Opened) { throw new InvalidOperationException("Host is already opened"); } ServiceBehaviorAttribute debuggingBehavior = Description.Behaviors.Find<ServiceBehaviorAttribute>(); debuggingBehavior.IncludeExceptionDetailInFaults = value; } get { ServiceBehaviorAttribute debuggingBehavior = Description.Behaviors.Find<ServiceBehaviorAttribute>(); return debuggingBehavior.IncludeExceptionDetailInFaults; } } }
|
ServiceHost<T>的用法简单、易读:
ServiceHost<MyService> host = new ServiceHost<MyService>(); host.IncludeExceptionDetailInFaults = true; host.Open(); |
若要通过管理方式应用这一行为,可以在宿主配置文件中添加定制行为节,然后在服务定义中引用它,如例6-8所示。
例6-8:通过管理方式在错误消息中包含异常
<system.serviceModel> <services> <service name = "MyService" behaviorConfiguration = "Debugging"> ... </service> </services> <behaviors> <serviceBehaviors> <behavior name = "Debugging"> <serviceDebug includeExceptionDetailInFaults = "true"/> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel>
|
管理方式配置的优势在于它能够绑定已部署服务的行为,而不会影响服务代码。
错误与回调
由于通信异常或者回调自身抛出了异常,到客户端的回调自然就会失败。与服务契约操作相似,回调契约操作同样可以定义错误契约,如例6-9所示。
例6-9:包含错误契约的回调契约
[ServiceContract(CallbackContract = typeof(IMyContractCallback))] interface IMyContract { [OperationContract] void DoSomething(); } interface IMyContractCallback { [OperationContract] [FaultContract(typeof(InvalidOperationException))] void OnCallBack(); } |
注意: WCF中的回调通常被配置为单向调用,因而无法定义自己的错误契约。
然而,不同于通常一般的服务调用,传递给服务的内容以及错误展现自身的方式,与以下内容相关:
•调用回调的时间。则意味着,或者回调在它正在调用的客户端发出服务调用期间被调用,或者是在宿主端的某个参与方对回调执行带外(Out-Of-Band)调用(译注2)。
•使用的绑定。
•抛出的异常类型。
如果回调属于带外调用,也就是说,在服务操作期间它被除了服务之外的其他参与方调用,那么回调的执行方式则与通常的WCF操作调用相似。例6-10演示了回调契约的带外调用,其中,回调契约的定义请参见例6-9。
例6-10:带外调用中的错误处理
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] class MyService : IMyContract { static List<IMyContractCallback> m_Callbacks = new List<IMyContractCallback>(); public void DoSomething() { IMyContractCallback callback = OperationContext.Current.GetCallbackChannel<IMyContractCallback>(); if(m_Callbacks.Contains(callback) == false) { m_Callbacks.Add(callback); } } public static void CallClients() { Action<IMyContractCallback> invoke = delegate(IMyContractCallback callback) { try { callback.OnCallback(); } catch(FaultException<InvalidOperationException> exception) {...} catch(FaultException exception) {...} catch(CommunicationException exception) {...} }; m_Callbacks.ForEach(invoke); } }
|
正如例6-10所示,由于通过回调契约可以将错误传递到宿主端,因而能够处理回调错误契约。如果客户端回调抛出的异常属于回调错误契约列出的异常,或者回调抛出一个FaultException异常,那么异常并不会导致回调通道发生错误,我们能够捕获异常,继续使用回调通道。然而,如果其中一个异常不属于错误契约的一部分,那么在抛出该异常之后,服务调用应会避免使用回调通道。
如果在服务操作期间,服务直接调用回调,并且抛出的异常被定义在错误契约的列表中,或者客户端回调抛出了一个FaultException异常,则回调错误的表现与在带外调用中的表现相同执行方式就是带外调用:
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)] class MyService : IMyContract { public void DoSomething() { IMyContractCallback callback = OperationContext.Current.GetCallbackChannel<IMyContractCallback>(); try { callback.OnCallBack(); } catch(FaultException<int> exception) {...} } }
|
注意,服务必须被配置为重入,才能避免死锁,正如第5章所阐释的那样(译注3)。因为回调操作定义了一个错误契约,同时又必须保证它不能为单向方法,因此需要定义为重入。
无论是带外调用还是服务回调,都是为预期的行为提供的。
当服务调用回调时,如果回调操作抛出的异常不在错误契约之列(或者不是FaultException),情况就变得异常复杂了。
如果服务使用的绑定为TCP或IPC绑定,当回调抛出的异常不在契约之中时,即使服务捕获了异常,第一个调用服务的客户端仍然会立即收到一个CommunicationException异常。然后,服务会获得一个FaultException异常。服务能够捕获和处理该异常,但它却会导致通道出现错误,因此服务无法重用它:
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)] class MyService : IMyContract { public void DoSomething() { IMyContractCallback callback = OperationContext.Current.GetCallbackChannel<IMyContractCallback>(); try { callback.OnCallBack(); } catch(FaultException exception) {...} } }
|
如果服务使用双向WS绑定,当回调抛出的异常不在契约之中时,即使服务捕获了异常,第一个调用服务的客户端仍然会立即收到一个CommunicationException异常。其间,服务会被阻塞,直到抛出超时异常才会解除:
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)] class MyService : IMyContract { public void DoSomething() { IMyContractCallback callback = OperationContext.Current.GetCallbackChannel<IMyContractCallback>(); try { callback.OnCallBack(); } catch(TimeoutException exception) {...} } }
|
服务不能重用回调通道。
注意:前面描述的各种回调行为,存在着巨大的差异,这是WCF的一个设计瑕疵。但它并非一个缺陷,或许在未来的版本中能够得到修正。
回调的调试
回调能够使用例6-4所示的技术,手动地将异常包含在错误消息中。CallbackBehavior特性提供了Boolean类型的属性IncludeExceptionDetailInFaults,用来在消息中包含所有非错误类型的契约异常(FaultException除外):
[AttributeUsage(AttributeTargets.Class)] public sealed class CallbackBehaviorAttribute : Attribute,... { public bool IncludeExceptionDetailInFaults {get;set;} //更多成员 }
|
与服务相似,引入异常有助于调试:
[CallbackBehavior(IncludeExceptionDetailInFaults = true)] class MyClient : IMyContractCallback { public void OnCallBack() { ... throw new InvalidOperationException(); } }
|
同样,我们可以在客户端配置文件中以管理方式配置这一行为:
<client> <endpoint ... behaviorConfiguration = "Debug" ... /> </client> <behaviors> <endpointBehaviors> <behavior name = "Debug"> <callbackDebug includeExceptionDetailInFaults = "true"/> </behavior> </endpointBehaviors> </behaviors>
|
注意,endpointBehaviors标签的使用会影响到客户端的回调终结点。
【责任编辑:
雪花 TEL:(010)68476606】