处理异常和错误

异常用来在服务或客户端实现中在本地传达错误, 另一方面,故障用于跨服务边界通信错误,例如从服务器到客户端,反之亦然。 除了故障之外,传输通道通常使用特定于传输的机制来传达传输级别的错误信息。 例如,HTTP 传输机制使用状态码(如 404)来传达不存在的终结点 URL(不存在发回错误的终结点)。 本文档由三个部分组成,这些部分提供自定义频道作者的指导。 第一部分提供有关何时以及如何定义和抛出异常的指导。 第二部分提供关于生成和使用错误的指南。 第三部分介绍如何提供跟踪信息,以帮助自定义通道的用户排查正在运行的应用程序的问题。

例外

引发异常时,需要记住两点:首先,它必须是一种类型,允许用户编写可以适当地响应异常的正确代码。 其次,它必须为用户提供足够的信息,以便用户了解出了什么问题、故障影响以及如何修复它。 以下部分提供有关 Windows Communication Foundation (WCF) 通道的异常类型和消息的指导。 有关 .NET 中的异常的一般指南,请参阅“异常设计指南”文档。

异常类型

通道抛出的所有异常必须是System.TimeoutExceptionSystem.ServiceModel.CommunicationException或派生自CommunicationException的类型。 如 ObjectDisposedException 等异常也可能被抛出,但这只是为了表明调用代码对通道的误用。如果通道被正确使用,那么它应该只抛出给定的异常。WCF 提供七种异常类型,这些类型派生自 CommunicationException 并专门设计用于通道。 还有其他从 CommunicationException 派生出的异常,这些异常被设计用于系统的其他部分。 这些异常类型包括:

异常类型 含义 内部异常的内容 恢复策略
AddressAlreadyInUseException 为侦听指定的终结点地址已在使用中。 如果存在,请提供有关导致此异常的传输错误的更多详细信息。 例如。 PipeException, HttpListenerExceptionSocketException 请尝试其他地址。
AddressAccessDeniedException 不允许该进程访问指定用于侦听的终结点地址。 如果存在,请提供有关导致此异常的传输错误的更多详细信息。 例如,PipeExceptionHttpListenerException 尝试使用不同的凭据。
CommunicationObjectFaultedException ICommunicationObject所使用的状态为“出错”(有关详细信息,请参阅“了解状态更改”。 请注意,当具有多个挂起调用的对象转换为故障状态时,只有一个调用会引发与失败相关的异常,其余调用将引发一个 CommunicationObjectFaultedException异常。 该异常通常是因为应用程序忽略了一些异常,并尝试在一个捕获原始异常的不同线程上使用已故障的对象而抛出的。 如果存在,则提供有关内部异常的详细信息。 创建一个新对象。 请注意,根据最初导致ICommunicationObject故障的原因,可能需要执行其他工作才能恢复。
CommunicationObjectAbortedException 正在使用的 ICommunicationObject 已中止(有关详细信息,请参阅 了解状态更改)。 与此类似 CommunicationObjectFaultedException,此异常表示应用程序已从另一个线程调用 Abort 对象,并且该对象由于该原因不再可用。 如果存在,则提供有关内部异常的详细信息。 创建一个新对象。 请注意,根据首先导致 ICommunicationObject 中止的原因,可能还需要完成其他工作来进行恢复。
EndpointNotFoundException 目标远程终结点未在进行侦听。 这可能会导致终结点地址的任何部分不正确、不可替代或终结点关闭。 示例包括 DNS 错误、队列管理器不可用以及服务未运行。 内部异常提供通常来自基础传输的详细信息。 请尝试其他地址。 或者,如果服务关闭,发送方可能会等待一段时间,然后重试
ProtocolException 终结点策略描述的通信协议在终结点之间不匹配。 例如,框架内容类型不匹配或超过最大消息大小。 如果存在,则提供有关特定协议错误的详细信息。 例如,当错误原因是超过 MaxReceivedMessageSize 时,QuotaExceededException 是内部异常。 恢复:确保发送方和收到的协议设置匹配。 执行此作的一种方法是重新导入服务终结点的元数据(策略),并使用生成的绑定重新创建通道。
ServerTooBusyException 远程终结点正在侦听,但尚未准备好处理消息。 如果存在,则内部异常提供 SOAP 错误或传输级错误详细信息。 恢复:请等待片刻,然后重试该操作。
TimeoutException 操作未能在超时期限内完成。 可能会提供有关超时的详细信息。 请等待并稍后重试该操作。

仅当该类型对应于与所有现有异常类型不同的特定恢复策略时,才定义新的异常类型。 如果定义了新的异常类型,则它必须从 CommunicationException 或其派生类之一派生。

异常消息

异常消息针对用户不是程序,因此他们应提供足够的信息来帮助用户理解和解决问题。 良好异常消息的三个关键部分是:

发生了什么事。 使用与用户体验相关的术语明确说明问题。 例如,错误的异常消息为“配置节无效”。 这让用户想知道哪个配置节不正确,以及为什么它不正确。 改进的消息为“配置部分 <customBinding> 无效”。 “Cannot add the transport named myTransport to the binding named myBinding because the binding already has a transport named myTransport”(无法将名为 myTransport 的传输添加到名为 myBinding 的绑定中,因为该绑定已经有一个名为 myTransport 的传输)是较好的消息。 这是一条非常具体的消息,它使用用户可以轻松地在应用程序的配置文件中识别的术语和名称。 但是,仍有一些关键组件缺失。

错误的重要性。 除非消息明确指出错误的含义,否则用户可能会怀疑它是致命错误还是可以忽略错误。 通常,消息中应首先声明错误的含义或重要性。 为了改进前面的示例,消息可能是“ServiceHost 由于配置错误而无法打开:无法将名为 myTransport 的传输添加到名为 myBinding 的绑定,因为绑定已具有名为 myTransport 的传输”。

用户应如何更正问题。 消息最重要的部分是帮助用户解决问题。 该消息应包含一些指导或提示,说明要检查或修复哪些内容来解决问题。 例如,“ServiceHost 由于配置错误而无法打开:无法将名为 myTransport 的传输添加到名为 myBinding 的绑定,因为绑定已具有名为 myTransport 的传输。 Please ensure there is only one transport in the binding”(ServiceHost 因配置错误而未能打开: 无法将名为 myTransport 的传输添加到名为 myBinding 的绑定中,因为该绑定已经有一个名为 myTransport 的传输。请确保该绑定中只有一个传输)。

通信故障

SOAP 1.1 和 SOAP 1.2 都定义了故障的特定结构。 这两个规范之间存在一些差异,但通常情况下,Message 和 MessageFault 类型用于创建和处理故障。

SOAP 1.2 错误和 SOAP 1.1 错误
SOAP 1.2 错误(左)和 SOAP 1.1 错误(右)。 在 SOAP 1.1 中,只有 Fault 元素是命名空间限定的。

SOAP 将错误消息定义为一种消息,其中仅包含一个错误元素(名称为 <env:Fault>)作为 <env:Body> 的子元素。 错误元素的内容在 SOAP 1.1 和 SOAP 1.2 之间略有不同,如图 1 所示。 但是,类 System.ServiceModel.Channels.MessageFault 将这些差异规范化为一个对象模型:

public abstract class MessageFault  
{  
    protected MessageFault();  
  
    public virtual string Actor { get; }  
    public virtual string Node { get; }  
    public static string DefaultAction { get; }  
    public abstract FaultCode Code { get; }  
    public abstract bool HasDetail { get; }  
    public abstract FaultReason Reason { get; }  
  
    public T GetDetail<T>();  
    public T GetDetail<T>( XmlObjectSerializer serializer);  
    public System.Xml.XmlDictionaryReader GetReaderAtDetailContents();  
  
    // other methods omitted  
}  

Code 属性对应于 env:Code (或在 faultCode SOAP 1.1 中),并标识错误的类型。 SOAP 1.2 定义了五个允许的 faultCode 值(例如,发送方和接收方),并定义了一个可以包含任何子代码值的 Subcode 元素。 (有关允许的错误代码及其含义的列表,请参阅 SOAP 1.2 规范 。SOAP 1.1 具有略有不同的机制:它定义了四 faultCode 个值(例如客户端和服务器),可以通过定义全新的值或使用点表示法来创建更具体的 faultCodes值,例如 Client.Authentication。

使用 MessageFault 对错误进行编程时,FaultCode.Name 和 FaultCode.Namespace 映射到 SOAP 1.2 env:Code 或 SOAP 1.1 faultCode的名称和命名空间。 FaultCode.SubCode 在 SOAP 1.2 中映射为 env:Subcode,而在 SOAP 1.1 中则为 null。

如果需要通过编程方式区分故障,您应该创建新的故障子代码(或使用 SOAP 1.1 时创建新的故障代码)。 这类似于创建新的异常类型。 应避免将点表示法与 SOAP 1.1 错误代码一起使用。 ( WS-I 基本配置文件 也禁止使用错误代码点表示法。

public class FaultCode  
{  
    public FaultCode(string name);  
    public FaultCode(string name, FaultCode subCode);  
    public FaultCode(string name, string ns);  
    public FaultCode(string name, string ns, FaultCode subCode);  
  
    public bool IsPredefinedFault { get; }  
    public bool IsReceiverFault { get; }  
    public bool IsSenderFault { get; }  
    public string Name { get; }  
    public string Namespace { get; }  
    public FaultCode SubCode { get; }  
  
//  methods omitted  
  
}  

Reason 属性对应于 env:Reason (或在 SOAP 1.1 中为 faultString),它是对错误条件的人类可读描述,类似于异常消息。 FaultReason 类(以及 SOAP env:Reason/faultString)内置支持多语言翻译,以促进全球化。

public class FaultReason  
{  
    public FaultReason(FaultReasonText translation);  
    public FaultReason(IEnumerable<FaultReasonText> translations);  
    public FaultReason(string text);  
  
    public SynchronizedReadOnlyCollection<FaultReasonText> Translations
    {
       get;
    }  
  
 }  

在 MessageFault 上,错误详细信息内容通过各种方法公开,包括 GetDetail<T> 和 GetReaderAtDetailContents()。 故障细节是一种不透明的元素,用于携带有关故障的额外详细信息。 如果您希望错误中包含一些任意的结构化详细信息,则这非常有用。

生成错误

本部分说明在通道或由通道创建的消息属性中检测到错误条件时生成故障的过程。 典型的示例是在收到包含无效数据的请求消息时返回错误信息。

生成错误时,自定义通道不应直接发送错误,而是应引发异常,并让上面的层决定是否将该异常转换为错误以及如何发送它。 为了帮助进行此转换,通道应提供一个 FaultConverter 实现,该实现可以将自定义通道引发的异常转换为适当的错误。 FaultConverter 定义为:

public class FaultConverter  
{  
    public static FaultConverter GetDefaultFaultConverter(  
                                   MessageVersion version);  
    protected abstract bool OnTryCreateFaultMessage(  
                                   Exception exception,
                                   out Message message);  
    public bool TryCreateFaultMessage(  
                                   Exception exception,
                                   out Message message);  
}  

生成自定义错误的每个通道都必须实现 FaultConverter 并从调用 GetProperty<FaultConverter>中返回它。 自定义 OnTryCreateFaultMessage 实现必须将异常转换为错误或者将其委托给内部通道的 FaultConverter。 如果该通道是传输通道,则该实现要么必须转换异常,要么必须将其委托给编码器的 FaultConverter 或者委托给 WCF 中提供的默认 FaultConverter。 默认的 FaultConverter 会转换与 WS-Addressing 和 SOAP 所指定的错误消息相对应的错误。 下面是一个示例 OnTryCreateFaultMessage 实现。

public override bool OnTryCreateFaultMessage(Exception exception,
                                             out Message message)  
{  
    if (exception is ...)  
    {  
        message = ...;  
        return true;  
    }  
  
#if IMPLEMENTING_TRANSPORT_CHANNEL  
    FaultConverter encoderConverter =
                    this.encoder.GetProperty<FaultConverter>();  
    if ((encoderConverter != null) &&
        (encoderConverter.TryCreateFaultMessage(  
         exception, out message)))  
    {  
        return true;  
    }  
  
    FaultConverter defaultConverter =
                   FaultConverter.GetDefaultFaultConverter(  
                   this.channel.messageVersion);  
    return defaultConverter.TryCreateFaultMessage(  
                   exception,
                   out message);  
#else  
    FaultConverter inner =
                   this.innerChannel.GetProperty<FaultConverter>();  
    if (inner != null)  
    {  
        return inner.TryCreateFaultMessage(exception, out message);  
    }  
    else  
    {  
        message = null;  
        return false;  
    }  
#endif  
}  

此模式的含意为,在各层之间针对那些要求错误的错误条件而引发的异常必须包含足够的信息,以使相应的错误生成器能创建正确的错误。 自定义通道作者可以定义对应于不同故障条件的异常类型(如果此类异常尚不存在)。 请注意,遍历通道层的异常应传达错误条件,而不是不透明的故障数据。

错误类别

通常有三类故障:

  1. 遍布整个堆栈的错误。 可以在通道堆栈中的任何层(例如 InvalidCardinalityAddressingException)遇到这些错误。

  2. 堆栈中某个层上方可能遇到的错误,例如,与流事务或安全角色相关的某些错误。

  3. 针对堆栈中单个层的错误,例如,像 WS-RM 序列号错误之类的错误。

类别 1. 故障通常有 WS-Addressing 和 SOAP 故障。 WCF 提供的基 FaultConverter 类转换与 WS-Addressing 和 SOAP 指定的错误消息对应的错误,因此你不必自行处理这些异常的转换。

类别 2. 当某一层向消息添加属性,而该层的相关消息信息未被完全利用时,会发生错误。 当较高层要求消息属性进一步处理消息信息时,可能会检测到错误。 此类通道应实现此前指定的 GetProperty,以使更高层能够返回正确的故障。 例如 TransactionMessageProperty。 此属性将添加到邮件中,而不会完全验证标头中的所有数据(这样做可能涉及联系分布式事务处理协调器 (DTC)。

类别 3. 故障仅由处理器中的单个层生成和发送。 因此,所有异常都包含在层中。 为了提高通道之间的一致性并简化维护,自定义通道应使用之前指定的模式来生成错误消息,即使内部故障也是如此。

解释收到的错误

本节提供关于接收错误消息时生成相应异常的指南。 用于处理堆栈中每个层的消息的决策树如下所示:

  1. 如果层认为消息无效,则层应执行其“无效消息”处理。 此类处理特定于该层,但是可以包括丢弃消息、跟踪或引发已转换为错误的异常。 示例包括安全接收未正确保护的消息,或 RM 接收带有错误序列号的消息。

  2. 否则,如果消息是专门应用于层的错误消息,并且该消息在层交互之外没有意义,则层应处理错误条件。 这方面的示例包括“RM 序列被拒绝”错误,这类错误对于 RM 通道上方的各层没有意义,表示从挂起操作引发的 RM 通道错误。

  3. 否则,应从 Request() 或 Receive()返回消息。 这包括如下情况:该层能够识别此错误,但是此错误仅指示请求失败,而不表示从挂起操作引发的通道错误。 为了改进此类情况下的可用性,层应实现 GetProperty<FaultConverter> 并返回一个 FaultConverter 派生类,该类可以通过重写 OnTryCreateException将错误转换为异常。

以下对象模型支持将消息转换为异常:

public class FaultConverter  
{  
    public static FaultConverter GetDefaultFaultConverter(  
                                  MessageVersion version);  
    protected abstract bool OnTryCreateException(  
                                 Message message,
                                 MessageFault fault,
                                 out Exception exception);  
    public bool TryCreateException(  
                                 Message message,
                                 MessageFault fault,
                                 out Exception exception);  
}  

通道层可以实现 GetProperty<FaultConverter> 以支持将错误消息转换为异常。 为此,需要重写 OnTryCreateException 并检查错误消息。 如果识别出来,请进行转换,否则请让内部通道来转换。 传输通道应委托给 FaultConverter.GetDefaultFaultConverter,以便获取默认的 SOAP/WS-Addressing FaultConverter。

典型的实现如下所示:

public override bool OnTryCreateException(  
                            Message message,
                            MessageFault fault,
                            out Exception exception)  
{  
    if (message.Action == "...")  
    {  
        exception = ...;  
        return true;  
    }  
    // OR  
    if ((fault.Code.Name == "...") && (fault.Code.Namespace == "..."))  
    {  
        exception = ...;  
        return true;  
    }  
  
    if (fault.IsMustUnderstand)  
    {  
        if (fault.WasHeaderNotUnderstood(  
                   message.Headers, "...", "..."))  
        {  
            exception = new ProtocolException(...);  
            return true;  
        }  
    }  
  
#if IMPLEMENTING_TRANSPORT_CHANNEL  
    FaultConverter encoderConverter =
              this.encoder.GetProperty<FaultConverter>();  
    if ((encoderConverter != null) &&
        (encoderConverter.TryCreateException(  
                              message, fault, out exception)))  
    {  
        return true;  
    }  
  
    FaultConverter defaultConverter =  
             FaultConverter.GetDefaultFaultConverter(  
                             this.channel.messageVersion);  
    return defaultConverter.TryCreateException(  
                             message, fault, out exception);  
#else  
    FaultConverter inner =
                    this.innerChannel.GetProperty<FaultConverter>();  
    if (inner != null)  
    {  
        return inner.TryCreateException(message, fault, out exception);  
    }  
    else  
    {  
        exception = null;  
        return false;  
    }  
#endif  
}  

对于具有不同恢复方案的特定故障条件,请考虑定义派生类 ProtocolException

MustUnderstand 处理

SOAP 定义一个用于指示接收方未理解必需的标头的通用错误。 此错误称为 mustUnderstand 错误。 在 WCF 中,自定义通道永远不会生成 mustUnderstand 错误。 而不是这样,位于 WCF 通信堆栈顶部的 WCF 调度程序会检查基础堆栈是否理解所有标记为 MustUnderstand=true 的标头。 如果均未理解,则此时会生成一个 mustUnderstand 错误 (用户可以选择关闭此 mustUnderstand 处理,并让应用程序接收所有消息标头。在这种情况下,应用程序负责执行 mustUnderstand 处理。生成的错误包括一个 NotUnderstood 标头,其中包含所有未理解的 MustUnderstand=true 标头的名称。

如果协议通道发送具有 MustUnderstand=true 的自定义标头并收到 mustUnderstand 错误,则必须确定该错误是否由发送的标头导致。 MessageFault 这个类中有两个成员对实现这一目的很有用:

public class MessageFault  
{  
    ...  
    public bool IsMustUnderstandFault { get; }  
    public static bool WasHeaderNotUnderstood(MessageHeaders headers,
        string name, string ns) { }  
    ...  
  
}  

如果故障是 IsMustUnderstandFault 故障,true 返回 mustUnderstand。 如果具有指定名称和命名空间的标头以 NotUnderstood 标头的形式包括在该错误中,则 WasHeaderNotUnderstood 将返回 true。 否则,它将返回 false

如果通道发出标记为 MustUnderstand = true 的标头,则该层还应实现异常生成 API 模式,并将该标头导致的故障转换为 mustUnderstand 更有用的异常,如前所述。

追踪

.NET Framework 提供了一种机制来跟踪程序执行,作为帮助诊断生产应用程序或间歇性问题的方法,在这些情况下,无法仅附加调试器并单步执行代码。 此机制的核心组件位于命名空间中 System.Diagnostics ,包括:

跟踪组件

从自定义通道跟踪

当无法将调试器附加到正在运行的应用程序时,自定义通道应写出跟踪消息,以帮助诊断问题。 这涉及到两个高级任务:实例化 TraceSource 和调用其方法来写入跟踪。

实例化 a TraceSource时,指定的字符串将成为该源的名称。 此名称用于配置跟踪源(启用/禁用/设置跟踪级别)。 此名称还显示在跟踪输出本身当中。 自定义通道应使用唯一的源名称来帮助跟踪输出的读取者了解跟踪信息的来源。 常见的做法是将正在写入信息的程序集的名称用作跟踪源的名称。 例如,WCF 使用 System.ServiceModel 作为从 System.ServiceModel 程序集写入的信息的跟踪源。

获得跟踪源后,可以调用TraceDataTraceEventTraceInformation方法,将跟踪条目写入跟踪侦听器。 对于写入的每个跟踪条目,需要将事件类型分类为在其中 TraceEventType定义的事件类型之一。 此分类和配置中的跟踪级别设置确定跟踪条目是否输出到侦听器。 例如,如果将配置中的跟踪级别设置为 Warning,则将允许写入 WarningErrorCritical 跟踪项,但会阻止 Information(信息)和 Verbose(详细)项。 下面的示例关于在 Information(信息)级别实例化跟踪源和写出项:

using System.Diagnostics;  
//...  
TraceSource udpSource = new TraceSource("Microsoft.Samples.Udp");  
//...  
udpsource.TraceInformation("UdpInputChannel received a message");  

重要

强烈建议你指定自定义通道特有的跟踪源名称,以帮助跟踪输出读取器了解输出的来源。

与跟踪查看器集成

通道生成的跟踪可以作为跟踪侦听器使用 System.Diagnostics.XmlWriterTraceListener 可读取的格式输出。 这不是你作为频道开发人员需要做的。 应用程序用户(或负责应用程序故障排除的人)需要在应用程序的配置文件中配置此跟踪侦听器。 例如,以下配置将来自 System.ServiceModelMicrosoft.Samples.Udp 的跟踪信息输出到名为 TraceEventsFile.e2e 的文件:

<configuration>  
  <system.diagnostics>  
    <sources>  
      <!-- configure System.ServiceModel trace source -->  
      <source name="System.ServiceModel" switchValue="Verbose"
              propagateActivity="true">  
        <listeners>  
          <add name="e2e" />  
        </listeners>  
      </source>  
      <!-- configure Microsoft.Samples.Udp trace source -->  
      <source name="Microsoft.Samples.Udp" switchValue="Verbose" >  
        <listeners>  
          <add name="e2e" />  
        </listeners>  
      </source>  
    </sources>  
    <!--   
    Define a shared trace listener that outputs to TraceFile.e2e  
    The listener name is e2e   
    -->  
    <sharedListeners>  
      <add name="e2e" type="System.Diagnostics.XmlWriterTraceListener"  
        initializeData=".\TraceFile.e2e"/>  
    </sharedListeners>  
    <trace autoflush="true" />  
  </system.diagnostics>  
</configuration>  

跟踪结构化数据

System.Diagnostics.TraceSource 具有一个 TraceData 方法,该方法采用一个或多个要包含在跟踪条目中的对象。 通常,该方法 Object.ToString 在每个对象上调用,生成的字符串将作为跟踪条目的一部分写入。 使用 System.Diagnostics.XmlWriterTraceListener 输出跟踪时,可以将数据 System.Xml.XPath.IXPathNavigable 对象作为数据对象传递给 TraceData。 生成的跟踪项包括由System.Xml.XPath.XPathNavigator提供的XML。 下面是一个包含 XML 应用程序数据的示例条目:

<E2ETraceEvent xmlns="http://schemas.microsoft.com/2004/06/E2ETraceEvent">  
  <System xmlns="...">  
    <EventID>12</EventID>  
    <Type>3</Type>  
    <SubType Name="Information">0</SubType>  
    <Level>8</Level>  
    <TimeCreated SystemTime="2006-01-13T22:58:03.0654832Z" />  
    <Source Name="Microsoft.ServiceModel.Samples.Udp" />  
    <Correlation ActivityID="{00000000-0000-0000-0000-000000000000}" />  
    <Execution  ProcessName="UdpTestConsole"
                ProcessID="3348" ThreadID="4" />  
    <Channel />  
    <Computer>COMPUTER-LT01</Computer>  
  </System>  
<!-- XML application data -->  
  <ApplicationData>  
  <TraceData>  
   <DataItem>  
   <TraceRecord
     Severity="Information"  
     xmlns="…">  
        <TraceIdentifier>some trace id</TraceIdentifier>  
        <Description>EndReceive called</Description>  
        <AppDomain>UdpTestConsole.exe</AppDomain>  
        <Source>UdpInputChannel</Source>  
      </TraceRecord>  
    </DataItem>  
  </TraceData>  
  </ApplicationData>  
</E2ETraceEvent>  

WCF 跟踪查看器了解前面显示的元素的 TraceRecord 架构,并从其子元素中提取数据,并将其以表格格式显示。 为了帮助 Svctraceviewer.exe 用户读取数据,您的通道在跟踪结构化应用程序数据时应当使用此架构。