65.9K
CodeProject 正在变化。 阅读更多。
Home

WCF 错误处理和故障转换

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.69/5 (42投票s)

2008年5月24日

CPOL

8分钟阅读

viewsIcon

247201

downloadIcon

5559

本文描述了 WCF 的错误处理范例,并提供了一种将异常自动映射到 WCF 故障的机制。

引言

WCF 似乎是一项易于掌握的技术。它是框架设计的终极典范——对于简单任务来说很简单,对于复杂任务来说也不算太复杂;它是可扩展和可替换的;它的 API 设计精良且方便。我在 WCF 中遇到的最大挑战之一是克服 .NET 开发人员一种非常自然的倾向:.NET 世界至上的谬论

WCF 是为面向服务而设计的,使用 WCF 编写明确界定的服务确实很容易。这类服务将在当今围绕数十种不断变化的标准的互操作世界中成为一流的公民。然而,即使你无意为之,编写耦合的 .NET 到 .NET 的解决方案也同样容易。

这种耦合可以在许多方面表现出来,贯穿于契约设计、服务边界、错误处理——几乎是服务的每一个方面。例如,以类层次结构作为 WCF 服务数据契约的基础似乎很自然。然而,继承的概念本身对服务世界来说是陌生的:它是面向对象编程的概念——一个 .NET 概念,而不是一个服务概念。另一个例子是错误处理——还有什么比抛出异常来指示失败更自然的呢?但是,异常再次是一个 .NET 概念——服务堆栈跟踪对服务消费者有什么价值?如何传播和消费异常数据?异常层次结构意味着什么,这让我们回到继承的例子?

但是,如果一方面,继承和异常的概念深深地根植于我们的开发过程中,而另一方面,这些概念又与 WCF 世界格格不入,那么编写恰当的面服务应用程序岂不是极其困难?确实如此,除非你有框架支持来弥合这一差距。

在本文中,我们将探讨如何管理 .NET 异常处理世界与面向服务的 WCF 错误处理范例(即故障)之间的自动桥梁。

故障概览

关于 WCF 错误处理的主题已经有很多文章,所以我只会提及基本原理。有关更详细的信息,请参考 MSDN 文档或任何好的 WCF 入门教材。

面向服务的错误处理的根本原则是SOAP 故障消息,它传达失败的语义以及与失败相关的附加信息(例如原因)。

大多数需要错误处理的服务还需要将附加信息传递给错误通知。这些信息可以作为标准的 WCF 数据契约,伪装成故障,传递给客户端。一个特定的服务操作可能导致指定故障的契约规范称为故障契约。以下代码演示了一个可能导致 MyApplicationFault 故障消息的服务操作契约。

[ServiceContract]
public interface IMyService {
    [OperationContract]
    [FaultContract(typeof(MyApplicationFault))]
    void MyMethod();
}

在客户端看来,这是一个契约——服务仅仅承诺只允许 MyApplicationFault 故障消息逃逸其边界。现在客户端可以在与服务的通信中预期这种故障消息。

产生和消耗故障

WCF 服务可以通过抛出异常来产生属于其故障契约的故障。对于 .NET 开发人员来说,抛出异常是指示失败最自然的方式。服务应该抛出 FaultException<TDetail> 通用异常,其中 TDetail 是正在传递给客户端的实际故障类型。例如,以下服务代码将 MyApplicationFault 故障传递给客户端。

class MyService : IMyService {
    public void MyMethod() {
        MyApplicationFault fault = new MyApplicationFault(...);
        throw new FaultException<MyApplicationFault>(fault);
    }
}

在 .NET 客户端端消耗该故障就像捕获 FaultException 一样简单。可以捕获非通用的 FaultException 基类,或任何特定的通用派生类。同样有理由预期与特定服务实现无关的通信相关的异常。例如,以下客户端代码是一种调用 MyService 服务并捕获故障的合理方式。

IMyService proxy = ...;    //Get proxy from somewhere
try {
    proxy.MyMethod();
}
catch (CommunicationException) { ... }
catch (TimeoutException) { ... }
catch (FaultException<MyApplicationFault> myFault) {
    MyApplicationFault detail = myFault.Detail;
    //Do something with the actual fault
}
catch (FaultException otherFault) { ... }

此代码防御性地假设可能会发生通信问题,在等待服务调用完成时可能会发生超时,并且可能会发生一般性的意外故障(除了客户端预期的 MyApplicationFault 之外)。

但是,如果一个意外异常从服务方逃逸了会怎样?嗯,客户端会收到一个非通用的 FaultException,它不会传递太多信息,并且客户端的通道将发生故障(可以通过使用 ServiceBehavior.IncludeExceptionDetailInFaults 来获取更多关于故障的信息,但这仅对调试环境有用——你不想向外部世界公开服务器堆栈跟踪)。总而言之,这不是一种友好的行为,应该不惜一切代价避免。

但是,真的很容易避免吗?考虑一个典型的服务,它除了将几个数字相加并返回结果之外,还做了一些额外的事情。它可能会调用许多下游类,这些类根本不知道它们是由特定的服务方法调用的,并且具有特定的故障契约。即使它们知道,将它们的实现与故障契约耦合也是一种糟糕的设计决策,因为它降低了它们的通用可重用性。另一方面,在每次服务调用中都放置 try...catch 块并决定返回什么故障消息也是一项繁琐的任务。这是另一个经典示例,我们希望框架代表我们执行此操作。

错误处理行为

WCF 有一个出色的内置可扩展机制,用于将异常转换为故障。这个可扩展点可以通过 IErrorHandler 接口来消费,该接口提供了两个方法:HandleErrorProvideFaultHandleError 方法在调用完成后在单独的线程上调用,以可能记录错误并执行其他簿记操作。这在我们的场景中是无用的。另一方面,ProvideFault 方法在调用服务调用的工作线程上调用,并接受由服务抛出的异常。它负责提供将发送给客户端的故障消息,因此完全符合我们的目标。在运行时,这些方法的实现可以挂接到服务端的 ChannelDispatcher,并且每当未处理的异常从服务代码中逃逸时,都会自动调用它们。

我们将从错误处理程序的核心开始。我们如何决定如何处理异常?好吧,我们的第一次尝试可能是将任何异常转换为 FaultException<TDetail>,其中 TDetail 是异常类型。例如,如果 ArgumentException 可能从下游代码抛出,那么我应该在我的操作上放置 [FaultContract(typeof(ArgumentException))] 属性,并让错误处理程序将异常转换为 FaultException<ArgumentException> “故障”。考虑到 .NET 异常是可序列化的,这是最省力的路径。另一方面,.NET 异常类型包含太多仅与服务侧相关的 <$>信息。虽然很容易实现,但这种方法会暴露实现细节并加剧 .NET 世界至上的谬论。

为了强制执行更强的分离,我们需要一种 .NET 异常和故障之间的映射机制。虽然理论上可以在服务范围内甚至进程范围内建立这种映射,但在操作特定的方式下执行它可能更有意义。像任何操作特定的行为一样,这是一个属性的好选择——所以,我们实际上在寻找这样的语法。

[ServiceContract]
public interface IMyService {
    [OperationContract]
    [FaultContract(typeof(MyApplicationFault))]
    [MapExceptionToFault(typeof(ApplicationException), typeof(MyApplicationFault))]
    void MyMethod();
}

我们在这里说的是什么?首先,我们指定 MyApplicationFault 故障是操作故障契约的一部分。其次,我们指定每当抛出 ApplicationException 异常时,它都应该被转换为 MyApplicationFault 故障。这相对容易实现。

sealed class ErrorHandler : IErrorHandler {
    public void ProvideFault(Exception error,
                 MessageVersion version,
                 ref Message fault)
        {
            //If it's a FaultException already, then we have nothing to do
            if (error is FaultException)
                return;

            //Get the operation description; omitted for brevity
            OperationDescription operationDesc = ...;

            object faultDetail = GetFaultDetail(operationDesc.SyncMethod,
                        operationDesc.Faults,
                        error);
            if (faultDetail != null)
            {
                Type faultExceptionType =
                    typeof(FaultException<>).MakeGenericType(faultDetail.GetType());
                FaultException faultException =
                    (FaultException)Activator.CreateInstance(
            faultExceptionType, faultDetail, error.Message);
                MessageFault faultMessage = faultException.CreateMessageFault();
                fault = Message.CreateMessage(version,
                          faultMessage,
                          faultException.Action);
            }
        }

        private object GetFaultDetail(MethodInfo method,
        FaultDescriptionCollection faults,
            Exception error)
        {
            if (method != null)
            {
                MapExceptionToFaultAttribute[] mappers =
            (MapExceptionToFaultAttribute[])
                    method.GetCustomAttributes(
            typeof(MapExceptionToFaultAttribute), true);
                foreach (MapExceptionToFaultAttribute mapAttribute in mappers)
                {
                    if (mapAttribute.ExceptionType == error.GetType())
                    {
            //Creates an instance of the fault detail
            //based on the exception object
                        faultDetail =
                mapAttribute.GetFaultDetailForException(error);
                        if (faultDetail != null)
                        {
                            return faultDetail;
                        }
                    }
                }
            }
        //No mapping found, so try the fault contract:
            foreach (FaultDescription faultDesc in faults)
            {
                if (faultDesc.DetailType == error.GetType())
                {
                   faultDetail = error;
                   break;
                }
            }
            return null;
        }
    //Other members omitted for brevity
}

如果没有映射属性,代码会自动尝试将异常转换为故障(如果该故障是契约的一部分)。如果存在映射属性,则使用它来进行转换。

编写上述代码时,棘手的部分之一是获取当前正在执行的服务操作的操作描述。Juval Lowy 在他的《WCF 服务编程》一书中,解析服务类型,查找接口实现和接口方法上的属性。我找到了一个替代方案,它使用更简洁的 WCF API 来查找当前正在执行的操作。

OperationContext context = OperationContext.Current;
ServiceEndpoint endpoint =
    context.Host.Description.Endpoints.Find(
        context.EndpointDispatcher.EndpointAddress.Uri);
DispatchOperation dispatchOperation =
    context.EndpointDispatcher.DispatchRuntime.Operations.Where(
        op => op.Action == context.IncomingMessageHeaders.Action).First();
OperationDescription operationDesc =
    endpoint.Contract.Operations.Find(dispatchOperation.Name);

安装错误处理行为

我们如何才能将此错误处理程序安装到我们服务的所有通道调度程序上?嗯,这很简单,只需定义一个属性并实现 IServiceBehavior。将此属性装饰到我们的服务类上,将在服务主机打开时安装错误处理程序。以下代码演示了该行为需要做什么。

public sealed class ErrorHandlingBehaviorAttribute : Attribute, IServiceBehavior
{
    public void ApplyDispatchBehavior(ServiceDescription serviceDescription,
        ServiceHostBase serviceHostBase)
    {
        foreach (ChannelDispatcherBase chanDispBase in
         serviceHostBase.ChannelDispatchers)
        {
            ChannelDispatcher channelDispatcher =
        chanDispBase as ChannelDispatcher;
            if (channelDispatcher == null)
                continue;
            channelDispatcher.ErrorHandlers.Add(new ErrorHandler(...));
        }
    }
    //Other interface methods omitted for brevity
}

此行为现在可以应用于服务实现类。

[ErrorHandlingBehavior]
class MyService : IMyService ...

为了完整性(通过配置提供此行为),需要一个行为扩展元素——但这通常是您更倾向于通过代码而非配置提供的服务特性之一。

显式翻译

声明式模型并非总是适用于所有服务。有时,翻译语义必须在运行时才能指定——这是属性无法提供的。一种可能的方法是在服务上的属性中指定一个增强异常转换过程的类型。我们可以为外部类型定义以下接口来实现:

public interface IExceptionToFaultConverter
{
    object ConvertExceptionToFaultDetail(Exception error);
}
//Example implementation:
class MyServiceFaultProvider : IExceptionToFaultConverter
{
    public object ConvertExceptionToFaultDetail(Exception error)
    {
        if (error is ApplicationException)
            return new MyApplicationFault(...);
        return null;
    }
}

...然后,在实现服务时,将辅助转换的类型指定为属性的一部分:

[ErrorHandlingBehavior(
    ExceptionToFaultConverter=typeof(MyServiceFaultProvider))]
class MyService : IMyService ...

向错误处理程序本身添加此功能支持是微不足道的(必要的代码包含在本文章的源代码下载中)。

摘要

本文概述的方法使服务开发人员能够专注于他们的业务逻辑并直接调用下游设施。它消除了服务开发人员必须担心只允许允许的故障逃逸服务边界的需要,并提供了一种方便的机制,用于将 .NET 异常映射到明确定义的 WCF 故障。

历史

  • 版本 1 -- 2008 年 5 月 24 日
© . All rights reserved.