捕获和处理 Silverlight 中的 WCF 服务异常






4.94/5 (17投票s)
一套可重用的代码,用于在 Silverlight 中实现服务抛出异常的处理。
引言
在使用 Silverlight 玩了一段时间后,我因为一个我认为缺失的基本功能而感到非常沮丧……那就是从 Silverlight 应用程序中捕获 WCF 服务抛出的异常。因此,我整理了一些有用的可重用代码库来帮助解决这个问题。
背景
异常处理已经存在很多年了,并且是 .NET Framework 的一个关键功能。当你本地运行代码并且在 .NET 的范围内时,这一切都很好,但一旦你的解决方案扩展到企业级,你就会进入服务、通道和其他激动人心的事情的领域,在那里 .NET Framework 无法控制一切(尤其是当它试图符合标准时)。
因此,在 WCF 服务的世界中,我们有称为“故障”(Faults)的东西。简单来说,我们不会在服务中抛出异常,而是抛出一个特殊的 FaulException
,WCF Dispatcher 会处理它并将其包装到我们的响应消息中。当我们的客户端收到它时,它会被解包并抛给调用的客户端方法。
如果我们想在 fault 中发送额外的信息,我们只需要创建一个可序列化的类(使用 [Serializable]
或 [DataContract]
属性),然后抛出泛型 FaulException<T>
(其中 T
是我们的可序列化类),并将我们的类传递给构造函数。为了让客户端能够捕获这种独特的异常,必须在可能抛出此 fault 的操作合同上方声明一个 [FaultContractAttribute(typeof(T))]
(其中 T
是我们的可序列化类)。WCF 创建的客户端代理将读取返回消息中的 fault 头,并尝试将其反序列化为与合同的 FaultContractAttribute
中定义的类型匹配的泛型 FaultException
。
那么,问题是什么?
嗯,正如你们中的许多人现在可能已经意识到的那样,以上所有内容都不适用于 Silverlight。尽管它已经是第二个版本了,但 Microsoft 尚未实现其他 .NET 平台类型中可用的 fault 处理机制。
更糟糕的是,如果我们服务中抛出异常,Silverlight 会返回一个非常奇怪的 HTTP 错误代码异常,告诉我们文件未找到……这时,你可能会说,“什么?”
Silverlight 和浏览器
由于 Silverlight 运行时托管在浏览器中,它依赖浏览器来处理所有网络请求。这也是为什么在 Silverlight 中唯一能使用的绑定是 basic HTTP。当服务抛出异常或 fault 时,HTTP 响应代码不是常规的 200 OK 响应,而是 40X 错误响应。这会被浏览器拦截和处理,然后 Silverlight 才能检查整个响应头,而浏览器传递给 Silverlight 的响应仅仅是说……出了些问题。
我们如何解决这个问题?
我在网上看到过几种解决方法,其中将异常包装到派生自方法返回类型的类中(非常丑陋)。但这需要你修改所有返回的 DataContracts 以包含一个 ExtraException
属性,并且它不适用于原始返回类型。
因此,我们将按照以下方式进行:
- 在客户端,创建一个代理包装类,所有服务调用都将通过它路由。这个代理类将向发送的消息头添加一个额外的属性,声明客户端无法正确处理 fault。
- 在服务端,实现一个 Operation Invoker,它将实现
IOperationInvoker
接口,WCF Dispatcher 将调用它而不是服务的方法。我们的 Operation invoker 将使用一个简单的try/catch
块包装底层服务方法,如果抛出异常,它将检查客户端可能设置的特殊属性,该属性告诉我们它无法处理 fault/异常。如果属性存在,则向 dispatcher 返回null
(这将导致服务返回 200 响应给浏览器),但会在消息头中添加一些额外的 fault 信息,其中包含异常的信息。 - 回到客户端代理,测试响应消息的头中是否有 fault 信息。如果有,则将其反序列化并抛给客户端;否则,返回消息内容。
IOperation 是什么?(从 Dispatcher 接管控制权)
我提到了在服务端实现 IOperationInvoker
。不深入细节的话,我们要做的就是告诉 WCF Dispatcher,我们将处理对服务合同中特定方法(Operation)的请求。我们通过为特定方法注册一个 IOperationInvoker
项来做到这一点。从这里开始,每当有调用通过网络请求 WCF 调用已注册的操作时,WCF Dispatcher 将调用我们的方法,而不是调用实现合同的方法。这使我们能够在将消息发送到原始方法之前控制它,并在将消息发送出去之前修改返回的消息。
简化
要创建一个简单的可重用 IOperationInvoker
,我们可以创建一个派生自 .NET System.Attribute
类并实现 IOperationBehavior
接口的类。然后,我们可以将此属性添加到服务合同中实现操作的任何服务方法上,dispatcher 将调用它来应用任何自定义的 dispatch 行为。当调用传入时,我们将操作的 invoker 设置为我们的自定义 IOperationInvoker
类,这将把调用路由到我们的实现。
在提供的代码中,该属性定义为 WCFHelpers.Service.FaultHandlingAttribute
,它将 invoker 设置为新的 WCFHelpers.Service.FaultHandlingInvoker
类。
然后,我们可以将此属性添加到服务中的任何方法上
public class SimpleService : ISimpleService
{
#region ISimpleService Members
[FaultHandling]
public SimpleInfo GetSimpleInfoDirect(int Id)
{
//...
}
#endregion
}
FaultDetail 类
为了将自定义异常信息通过网络发送,WCFHelpers.Client
命名空间定义了一个 FaultDetail
类。这是一个精简的异常类,包含:
- 一个消息属性。
- 一个类型名称(代表原始异常的类型)。
- 一个
InnerFault
属性,其中包含任何InnerException
信息。 - 一个
OuterFault
,它类似于 WCF 的FaultException<T>
的Detail
属性,允许你向 fault 添加任何你想要的额外信息。
该类型有一个接受 Exception
项的构造函数,可以从中进行初始化。或者,你也可以实例化一个空白的,然后自己设置属性。
通过网络发送 Fault 信息
正如我之前提到的,一旦我们在自定义操作 invoker 类中捕获了异常,我们必须决定如何将其通过网络发送。
为了使代码尽可能通用,invoker 在捕获到异常后执行以下操作:
- 如果客户端可以处理 fault(例如,Windows Forms 应用程序),那么我们检查抛出的异常类型。如果它派生自 WCF
FaultException
,我们只需重新抛出它,让 WCF 处理 Fault 的序列化。如果它不是一个 proper WCFFaultException
,我们创建一个通用的包装器FaultException<WCFHelpers.Client.FaultDetail>
,并使用一个由原始异常创建的新FaultDetail
类来实例化它。 - 如果客户端无法处理 fault(如 Silverlight),那么我们创建一个由原始异常创建的新
FaultDetail
类(或者,从FaultDetailException
中提取FaultDetail
信息,如果抛出了的话),并将其序列化到传出的消息头中。
由于 invoker 区分不同的异常类型,你可以通过抛出 WCFHelpers.Client.FaultDetailException
并将自定义 FaultDetail
类传递给异常的构造函数来发送大部分信息。如果客户端支持 fault,它将被转换为 FaultException<WCFHelpers.Client.FaultDetail>
异常,或者将自定义 FaultDetail
属性序列化并发送给不支持的客户端。
对合同开发者的提示:由于所有异常都会被转换为 WCF FaultException<WCFHelpers.Client.FaultDetail>
以供支持 Fault 处理的客户端使用,因此你可以将此类型作为 FaultContractAttribute
添加到你的合同操作中。
客户端代理
代码文档相当完善,但我会简要解释客户端代理。代理定义在 WCFHelpers.Client.ObjectClient<TServiceContract>
中,其中 TServiceContract
是合同类型。它公开两个公共方法:
Invoke
:这是一个相当直接的方法,它同步调用服务操作并将结果返回给调用者。如果一个异常/fault 被通过网络发送,该方法会将其抛给调用者。Begin
:此方法包装异步操作调用,其中方法称为Beginxxx
和Endxxx
,并实现标准的 .NET 异步方法。此方法接受一个额外的回调委托,类型为EventHandler<ClientEventArgs>
,将在方法完成后调用。ClientEventArgs
将在其Result
属性中包含方法的返回结果,如果一个异常被通过网络发送,它将被设置为Exception
成员。
代理类还公开了一个名为 HandlesFaults
的公共 bool
属性(惊喜!)。当此属性设置为 true
时,通过代理进行的任何调用都将向消息添加一个额外的头,声明此客户端无法以传统方式处理 fault。这取决于我们自定义的服务操作 invoker 来拦截此头并相应地打包任何异常。
使用代码
辅助类分为两个独立的程序集:
- WCFHelpers.Client.dll:包含客户端命名空间,并包含客户端代理类(
ObjectClient<TServiceContract>
)、FaultDetail
和FualtDeatilException
类,以及一个名为Global
的静态类,该类包含读取和写入消息头中 fault 的方法。 - WCFHelpers.Service.dll:包含服务命名空间,并包含我们的自定义操作 invoker 以及
FaultHandlingAttribute
,后者应应用于任何可能拥有不支持 fault 处理的客户端的服务实现方法。该程序集还公开了一个Async
命名空间(WCFHelpers.Service.Async
),其中包含两个可重用类,用于帮助在服务中实现异步调用模式。
注意:此程序集引用 WCFHelpers.Client.dll 程序集。
为了使用代码,建议采用以下方法:
在服务端,为任何可能抛出异常的方法添加 FaultHandling
属性。另外,为这些方法的合同定义添加 [FaltContract(typeof(FaultDetail))]
属性。
在合同上
[ServiceContract]
public interface ISimpleService
{
[OperationContract]
[FaultContract(typeof(WCFHelpers.Client.FaultDetail))]
SimpleInfo GetSimpleInfoDirect(int Id);
[OperationContract(AsyncPattern=true)]
[FaultContract(typeof(WCFHelpers.Client.FaultDetail))]
IAsyncResult BeginGetSimpleInfo(int Id, AsyncCallback Callback, object state);
[FaultContract(typeof(WCFHelpers.Client.FaultDetail))]
SimpleInfo EndGetSimpleInfo(IAsyncResult result);
}
在服务上
public class SimpleService : ISimpleService
{
#region ISimpleService Members
[FaultHandling]
public SimpleInfo GetSimpleInfoDirect(int Id)
{
//...
}
[FaultHandling]
public IAsyncResult BeginGetSimpleInfo(int Id, AsyncCallback Callback, object state)
{
return WCFHelpers.Service.Async.AsyncResult<SimpleInfo>.BeginAsync(
delegate { return GetSimpleInfoDirect(Id); }, Callback, state);
}
[FaultHandling]
public SimpleInfo EndGetSimpleInfo(IAsyncResult result)
{
WCFHelpers.Service.Async.AsyncResult<SimpleInfo> simpleResult =
result as WCFHelpers.Service.Async.AsyncResult<SimpleInfo>;
//This will thro an exception if one has occurd in the initial execution
return simpleResult.EndInvoke();
}
#endregion
}
注意使用 AsyncResult<SimpleInfo>
来强类型化我们的异步模式方法。
在客户端
在客户端,最简单的用法是派生一个类自 WCFHelpers.Client.ObjectClient<TServiceContract>
,并为合同中的每个操作添加一个强类型的方法。
public class SimpleClient: WCFHelpers.Client.ObjectClient<ISimpleService>
{
public SimpleClient(String endpointConfigurationName)
: base(endpointConfigurationName)
{
//notify the proxy that we don't handle faults
base.HandlesFaults = false;
}
public SimpleClient(Binding binding, EndpointAddress address)
: base(binding, address)
{
//notify the proxy that we don't handle faults
base.HandlesFaults = false;
}
public void GetSimpleInfoAsync(int Id,
EventHandler<WCFHelpers.Client.ClientEventArgs> callback, object State)
{
base.Begin("GetSimpleInfo", callback, State, Id);
}
public SimpleInfo GetSimpleInfoDirect(int Id)
{
return base.Invoke("GetSimpleInfoDirect", Id) as SimpleInfo;
}
}
一些烦人的问题
就像所有美好的事物通常一样,此解决方案也存在一些令人烦恼的问题。
第一个问题是在 Silverlight 和标准 WinForms 客户端中重用 WCFHelpers.Client.dll 程序集。如果你曾经尝试将非 Silverlight 程序集添加到使用 Visual Studio 的 Silverlight 应用程序中,你可能会注意到它不起作用。这是因为 Silverlight 使用的基本运行时程序集(System.dll、mscorelib.dll 等)与标准 .NET 环境中的不同,因此当 Silverlight 想要将常规 .NET 程序集加载到内存中时,程序集会寻找错误的运行时引用,而不会加载。幸运的是,有解决办法,你可以阅读我的文章 Converting .NET Assemblies to Silverlight Assemblies 来了解如何解决这个问题。下载提供的项目包含一个生成 Silverlight 兼容版本的 WCFHelpers.Client.dll 的生成后命令,并将其放置在 bin\Silverlight 目录中(如上述文章所述)。
注意:此机制也应用于测试应用程序中的合同程序集,以便合同定义可以在 Silverlight 测试和 WinForms 测试客户端之间共享。
第二个令人烦恼的问题在“捶胸顿足”的层面上排名要高得多。如果你按照说明实现了以上所有代码,但在服务内部抛出了 WCF FaultException
,你可能会发现自己又回到了原点,即 fault 没有被发送回你的 Silverlight 客户端。这是因为 WCF dispatcher 会在内部处理 FaultException
,然后将异常重新抛给我们的自定义 Operation Invoker。当我们 **处理** 异常并将其替换为传出消息中的简单头时,dispatcher 已经将操作标记为“已故障”(Faulted),并会准备一个 Fault 回复,即使我们已经处理了异常。我目前唯一的解决方法就是不要直接抛出 WCF FaultException
,而是抛出 FaultDetailException
(或任何其他不派生自 FaultException
或 SecurityException
的 .NET Exception)。这些 fault 不会被 Dispatcher 拦截,当被我们的自定义 Operation Invoker 捕获时,如果客户端支持 fault 处理,它们将被重新抛出为 WCF FaultException<FaultDetail>
。
关注点
WCFHelpers.Service.AsyncResult<TResult>
包含一个名为 BeginAsync
的静态方法。这是一个有用的可重用代码,用于简化服务中的异步调用模式开发。它文档很完善,所以请查看一下……
结论
首先,我写这篇文章时没想到会这么长;否则,我可能就没有耐心坐下来写了。
总之,这是我一直在实施的方法,并且它经受住了考验。如果你遇到过这个问题,并且以不同的方向解决了它,请给我留言,我很乐意获得新的见解。
我包含了两个测试客户端,一个 Silverlight 客户端和一个 WinForms 客户端。它们的功能完全相同,只是 WinForms 客户端使用了 WCF 提供的传统 Fault 合同,而 Silverlight 版本使用了前面解释的替代机制。在这两个测试客户端中,按下按钮都会向服务发送一个随机数。服务为其中一些数字返回有效信息,为其他数字抛出异常,所以如果你没有立即获得异常,请多按几次按钮,最终它会抛出异常。
历史
一个很棒的频道,但还没有关于这篇文章的内容。