WCF 客户端不能用在 Using 语句块中,因为它可能会抛出不可预知的异常。即使你捕获了异常,仍有可能一直保持连接。让我们来看看形成这一问题的历史原因,并提出几个补救措施。
在.NET 中,资源管理的基础就是 IDisposable 和 Using 语句块。除了 CLR 对象,.NET 中一切对象均使用这些工具进行管理。因此,我们需要知道为何微软对于 WCF 框架的资源管理如此一筹莫展。
WCF 客户端的首要问题是 Close/Dispose 方法会抛出异常。这与框架设计指南以及 IDisposable 规约背道而驰,从而导致 Dispose 方法可以在 Finally 语句块中被不安全的调用。
更糟糕的是,只要不调用 Abort,Close/Dispose 方法就会一直保持连接。太多的连接打开就会带来性能的问题,应用程序也会变得不够稳定。
在新闻组中,有关此问题的讨论可以追溯到 2006 年,Brian McNamara 介绍了这一设计缺陷的幕后故事。
ICommunicationObject(它是 ServiceHost,ClientBase,IChannel,IChannelFactory 与 IChannelListener 最终继承的对象) 总是具有关闭对象的两个方法:(a)Close,(b)Abort。按照字面的理解,如果希望主动关闭对象,则调用 Close;若要强制关闭则调用 Abort。
因此,Close() 方法会接收一个 Timeout 参数,并包括一个异步版本(因为它可能阻塞线程),而且 Close() 还会抛出异常。Close 抛出的异常为 CommunicationException(CommunicationObjectFaultedException 是其子类)与 TimeoutException。
相反,Abort() 并不会阻塞线程(也不会抛出任何异常),因此没有 Timeout 值,也并不包含异步版本。
这两个概念从最初的 Indigo 一直沿用至今 [译注:所谓至今是指 Brian 发表帖子的时间 2006 年 10 月 25 日]。就目前而言一切正常。
最初的定义为 ICommunicationObject : IDisposalbe。作为一个标记接口,我们认为它可以用于通知用户在可能的时候即刻释放对象。然而问题却接踵而来。
从 Beta 1 版本开始,我们修改了 Dispose(),让其等同于 Abort() 方法。一部分原因是 Dispose() 应该完成最起码的必要的对象清理工作。在 Beta 1 中,这可能算得上是我们的头号麻烦了。用户可以将它们的通道(channel)对象放在 using() 语句块中,缓存中任何等待被取出的消息都可能会丢失。事务无法提交,会话可能得到告知收到(ACKed)的消息等。
鉴于用户的反馈,在 Beta 2 中我们又修改了实现,让 Dispose() 近似等于 Close()。我们知道,异常的抛出是问题之所在(部分原因在这篇帖子中已经说明),因此我们试图 让 Dispose 变得更加“聪明”。那就是说,如果当前并非 Opened 状态,就会在内部调用 Abort()。这仍然存在一系列问题,最主要的是你无法从 可靠性角度推断系统。Dispose 仍然会抛出异常,但并非总是会通知你某些事情发生错误。最终,我们决定将 IDisposable 从 ICommunicationObject 中移走。经过几番争辩,IDisposable 在 ServiceHost 和 ClientBase 中被保留了下 来,因为从理论上讲,对于多数用户而言 Dispose 抛出异常仍然是可以接受的,他们更偏向于使用 using() 的便利性,具有该标记接口就可以更及时地 清除对象。你可能主张(我们的一部分开发人员抱有同样的态度):应该将它从这两个类中移走,然而好也罢歹也罢,我们终究作出了选择。对于这个问题,你永远 都不可能达成一致,因此我们在 SDK 样例中给出了最佳实践,那就是遵循 try{Close}/catch{Abort}范式。
补救措施
Steve Smith 提出了 CloseConnection 扩展方法 [译注:原文并没有给出 CloseConnection 扩展方法的帖子,你可以访问 IDisposable 与 WCF ]。在 Finally 语句块中可以调用该方法,而不是调用 Close,它封装了 Close/Abort 逻辑。
新闻组的发帖人 bog1978 建议使用 C# lambda 以支持创建类似Using 的结构。方法接收一个新的客户端对象,以及一个匿名方法,该方法持有的代码与正常使用using 语句块包含的代码完全相同。
最后,还有一个补救措施是Erwyn Van Der Meer 定义的 WCF 服务代理辅助类。用户可以创建它,而不是通常的代理类,它可以纠正在关闭连接时出现的问题。一旦创建,它就会自动构建实际的代理,然后通过只读属性暴露它。
评论