WCF 扩展之我见: ErrorHandler

本系列索引,请见《开篇

一般情况下应用程序的错误处理都大同小异,如果为每个错误单独写 try...catch... 未尝不可,但是如果错误处理逻辑相同,就会导致违反 DRY (Don't Repeat Yourself) 原则。对于控制台、窗体等类似的应用程序,我们可能会考虑使用一些 AOP (面向切面的编程) 框架把错误处理做为一个切面插入到应用程序中。而在 WCF 中,框架本身就提供了一个全局的出错处理方式,即本文的主角:IErrorHandler。

这里还需要注意一点,与传统的错误消息(Exception)不同,WCF 返回给客户端的错误消息是以 Soap 消息的形式被传递,这样也符合 SOA 可以支持异构系统的特点。

默认行为

如果我们不进行任何错误处理,那么当服务端抛出一个异常时,客户端会收到一个 FaultException 异常,这是 WCF 通用的错误消息,所有错误的细节都不会显示。

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Header />
  <s:Body>
    <s:Fault>
      <faultcode xmlns:a="http://schemas.microsoft.com/net/2005/12/windowscommunicationfoundation/dispatcher">a:InternalServiceFault</faultcode>
      <faultstring xml:lang="en-US">The server was unable to process the request due to an internal error.  For more information about the error, either turn on IncludeExceptionDetailInFaults (either from ServiceBehaviorAttribute or from the &lt;serviceDebug&gt; configuration behavior) on the server in order to send the exception information back to the client, or turn on tracing as per the Microsoft .NET Framework SDK documentation and inspect the server trace logs.</faultstring>
    </s:Fault>
  </s:Body>
</s:Envelope>

如果希望客户端能显示详细的错误细节,此时可以根据上面的描述在 WCF 的配置文件中启用 IncludeExceptionDetailInFaults 这个开关。这样客户端收到的则是一个 FaultException<ExceptionDetail> 异常,通过查看该异常的 Detail 属性就可以看到具体的错误。

<!-- 配置文件 -->
      <serviceBehaviors>
        <behavior>
          <!-- To receive exception details in faults for debugging purposes, set the value below to true.  Set to false before deployment to avoid disclosing exception information -->
          <serviceDebug includeExceptionDetailInFaults="true"/>
        </behavior>
      </serviceBehaviors>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Header />
  <s:Body>
    <s:Fault>
      <faultcode xmlns:a="http://schemas.microsoft.com/net/2005/12/windowscommunicationfoundation/dispatcher">a:InternalServiceFault</faultcode>
      <faultstring xml:lang="en-US">Attempted to divide by zero.</faultstring>
      <detail>
        <ExceptionDetail xmlns="http://schemas.datacontract.org/2004/07/System.ServiceModel" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
          <HelpLink i:nil="true" />
          <InnerException i:nil="true" />
          <Message>Attempted to divide by zero.</Message>
          <StackTrace>   at WcfService2.Service1.GetData(Int32 value, Int32 v1, String v2) in C:\Users\xuchen\documents\visual studio 2010\Projects\WcfService2\WcfService2\Service1.svc.cs:line 23
   at SyncInvokeGetData(Object , Object[] , Object[] )
   at System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]&amp; outputs)
   at WcfService2.MyCacheInvoker.Invoke(Object instance, Object[] inputs, Object[]&amp; outputs) in C:\Users\xuchen\documents\visual studio 2010\Projects\WcfService2\WcfService2\MyContextInitilizer.cs:line 150
   at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc&amp; rpc)
   at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc&amp; rpc)
   at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage41(MessageRpc&amp; rpc)
   at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage4(MessageRpc&amp; rpc)
   at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage31(MessageRpc&amp; rpc)
   at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage3(MessageRpc&amp; rpc)
   at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage2(MessageRpc&amp; rpc)
   at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage11(MessageRpc&amp; rpc)
   at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage1(MessageRpc&amp; rpc)
   at System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)</StackTrace>
          <Type>System.DivideByZeroException</Type>
        </ExceptionDetail>
      </detail>
    </s:Fault>
  </s:Body>
</s:Envelope>

但是显然上面这种方式会暴露太多细节给客户端,且不够友好。如果只想暴露个别错误信息比如错误消息、代号,那么可以通过使用 FaultContract 方式(毕竟 WCF 双方交互都是基于 Contract 来的)。

FaultContract 告诉 WCF 某个异常是如何用 soap 消息展示的,至于如何使用 FaultContract 不在本文的范围内,请大家自行谷歌。

想要使用这种方式,必须显示得 throw 一个 FaultContract 的异常。我们可否不显示 throw 异常,而是交由全局的异常处理去自动捕获异常并封装成预期的错误消息返回给客户端呢?

作 用

通过使用 IErrorHandler 就可以实现我们想要的行为,即干预全局的错误处理,比如写日志,封装错误消息等。

执行时机

在本系列的开头,介绍过 WCF 消息的处理流程,在那篇文章中讲到在进行 MesagePump 的时候会调用 Dispatch 方法,该方法最终会调用 rpc.Process 开始以类似管道的方式对 Message 进行处理。

//operation.Parent.Dispatch
internal bool Dispatch(ref MessageRpc rpc, bool isOperationContextSet)
{
    rpc.ErrorProcessor = this.processMessage8;
    rpc.NextProcessor = this.processMessage1;
    return rpc.Process(isOperationContextSet);
}

上面的代码中的 ErrorProcessor 即会调用 ErrorHandler 对错误进行处理。而什么时候会调用 ErrorProcessor 呢?当 Process 过程中处理任何异常的时候,就会调用这个 ErrorProcessor。

如何扩展

首先,来看一下 IErrorHanlder 接口的定义:

    public interface IErrorHandler
    {    
        void ProvideFault(Exception error, MessageVersion version, ref Message fault);        
        bool HandleError(Exception error);
    }

ProvideFault 方法主要用于创建返回给客户端的错误消息 FaultException<ExceptionDetail>。由于 ErrorHandler 可以有多个,当所有的 ProvideFault 都执行完后,fault 会被返回给 client 。(当该方法被调用时,fault 已经包含了 WCF 处理完的错误消息。如果 fault 为 null,则 WCF 通用的错误消息将会被返回给 client)。

一般情况下,如果返回的是 fault 消息(即符合 Soap Fault 结构的消息),则客户端会识别为一个错误。如果不希望以 fault 消息的方式返回给客户端(比如,通信双方约定在正常的消息结构中增加一个 error 结点来表示错误消息),只需要将期望的 message 赋值给 fault 即可。


HandleError 在所有的 ProvideFault 全部执行完后,错误消息准备返回给客户端后才会执行,常用于执行一些和 Error 相关的逻辑,比如日志记录、发送通知、关闭应用程序等,并返回一个布尔值用于告诉 WCF 异常是不是被处理了。如果返回的是 false (默认),表示异常没有被处理,此时当前客户的会话将被终止(服务实例也会被终止,InstanceContextMode = Single 的除外)。返回 true,暗示 WCF 运行时可以安全得继续处理当前的会话。由于 ErrorHandler 可以有多个,只有当所有的 HandleError 返回的是 false,才会被认为异常是没有被处理,如果有一个返回 true,则表示异常被处理了。


与其它扩展类似,如果想要把 ErrorHandler 加到 WCF 运行时中,需要使用 Behavior 方式进行关联,可以参考《WCF 扩展之我见: Behaviors》。


1.多个 ErrorHandler 的执行顺序与加入到 ChannelDispatcher.ErrorHandlers 的先后顺序一致,即先加入该集合的 ErrorHandler 会先调用。
2.由于很多地方都可能会调用 IErrorHandler,所以请不要假设执行 ProvideFault 和 HandleError 的线程就是调用 operation 的线程。


举个粟子

服务契约

[ServiceContract]
public interface IService1
{
    [OperationContract]
    [FaultContract(typeof(MyCustomException))]
    string GetData(int value);
}

通过 FaultContract 来告诉通信双方 MyCustomException 是一个错误。MyCustomException 必须是可序列化的。


下面是自定义的 ErrorHandler 和 MyCustomException:

[Serializable]
public class MyCustomException : Exception
{
    public MyCustomException(string code, string message)
        : base(message)
    {
        Code = code;
    }

    public string Code { get; internal set; }
}

public class MyErrorHandler : IErrorHandler
{
    public bool HandleError(Exception error)
    {
        //Log error
        return true;
    }

    public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
    {
        FaultException faultEx = error as FaultException;
        if (faultEx == null)
        {
            MyCustomException myEx = error as MyCustomException;
            if (myEx != null)
            {
                //如果是某种期望的错误,则以期望的 response 展示给客户端
                GetDataResponse response = new GetDataResponse
                {
                    Header = new Header
                    {
                        Success = false,
                        Error = new Error { Code = myEx.Code, Message = myEx.Message }
                    }
                };
                fault = Message.CreateMessage(version, null, response);
                return;
            }

            faultEx = new FaultException(error.Message);
        }

        fault = Message.CreateMessage(version, faultEx.CreateMessageFault(), faultEx.Action);
        return;
    }
}


然后通过 Behavior 关联到 WCF 运行时:

class MyErrBehaviorAttribute : Attribute, IServiceBehavior
{
    public void AddBindingParameters(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
    {
    }

    public void ApplyDispatchBehavior(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
    {
        foreach (ChannelDispatcher dispatcher in serviceHostBase.ChannelDispatchers)
        {
            dispatcher.ErrorHandlers.Add(new MyErrorHandler());
        }
    }

    public void Validate(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
    {
    }
}


服务的实现:

[MyErrBehavior]
public class Service1 : IService1
{
    public string GetData(int value)
    {
        //Business Logic
        
        
        //throw 一个 FaultException
        //throw new FaultException<MyCustomException>(new MyCustomException("aaa","bbb"), new FaultReason("aaa"));
        
        //throw 一个普通的 Exception
        throw new MyCustomException("This is my fault", "This is my fault Message");
    }
}


最终的消息:

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Header />
  <s:Body>
    <GetDataResponse xmlns="http://tempuri.org/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
      <GetDataResult i:nil="true" />
      <Header xmlns:a="http://blog.chenxu.me">
        <a:Error>
          <a:Code>This is my fault</a:Code>
          <a:Message>This is my fault Message</a:Message>
        </a:Error>
        <a:Success>false</a:Success>
      </Header>
    </GetDataResponse>
  </s:Body>
</s:Envelope>


参考资源

WCF Extensibility – IErrorHandler

WCF Error Handling

文章索引

[隐 藏]

本站采用知识共享署名 3.0 中国大陆许可协议进行许可。 ©2014 Charley Box | 关于本站 | 浙ICP备13014059号