WCF クライアントはUsingブロック内で使うことはできない、なぜなら予期しない例外が投げられる可能性があるからだ。たとえ例外をキャッチしたとしても、接続が開きっぱなしになることもある。この問題に関するこれまでの経緯と提案されているいくつかの回避策をこれから見ていこう。
.NET におけるリソース管理の要になるのはIDisposableとUsingブロックだ。CLRオブジェクトは別として、.NET世界における全てのオブジェクトのライフスパンはこれらを用いて管理される。だからこそ疑問に思うのも当然のことだ。どうやってMicrosoftはWCFフレームワークについてこんなにお粗末な問題を起こすことができたのだろうか、と。
WCF クライアントの問題としてまず挙げられるのがClose/Disposeメソッドが例外を投げうることだ。このことでFinallyブロックから Disposeメソッドを呼び出すことができてしまう。これはFremework Design GuidelineおよびIDisposable契約に明らかに反している。
さらに悪いことに、Abortが呼ばれない場合はClose/Disposeメソッドが呼ばれても接続が開いたままになる可能性がある。もし多くの接続が開きっぱなしになればパフォーマンスの問題やアプリケーションの不安定さにつながることになる。
2006年にあるニュースグループのスレッドで、このような設計になっている背景をBrian McNamara氏が語っている(リンク)。
(ServiceHost、 ClientBase、IChannel、IChannelFactory、IChannelListenerの一番の親である)ICommunicationObject(を継承するオブジェクト)はオブジェクトを終了させるための2つのメソッドを必ず持たないといけません。それがCloseとAbortです。その違いは、終了を手順に則っておこなうために呼ぶのがCloseで、無理矢理終了させるために呼ぶのがAbortです。
その結果として、Close()にはタイムアウトと(ブロック化できるよう)非同期Close()があり、またClose()は例外を投げることができるようになっています。ドキュメントにあるCloseの投げる例外は、 CommunicationException(CommunicationObjectFaultedExceptionはこのサブクラス)と TimeoutExceptionです。
逆にAbort()はブロック(あるいは特定の例外を投げること)をサポートしておらず、そのためタイムアウトや非同期版のAbort()がありません。
この2つのコンセプトはIndigo(WCFの開発コードネーム)の開発当初から今日にいたるまでずっと続いています。今のところ有効に使えています。
この2つのコンセプトの元になっているのがICommunicationObject : IDisposableなのです。このインターフェースはマーカーインターフェースなので、このオブジェクトの解放が可能であれば、そのことをユーザへ知らせることができれば便利だろうと私たちは考えました。これが問題の始まりでした。
IndigoのBeta 1まではDispose()とAbort()とは同じものでした。そうした理由には、Dispose()はクリーンアップに必要な最低限のことだけすべきであるという考えもありました。しかしBeta 1で一番多かったであろうクレームがこの点についてだったのです。チャネルをusing()ブロックに置いている時に接続を閉じた場合は、待ち状態にあるメッセージを破棄できる方をユーザは望んだのです。トランザクションはコミットしない、セッションはACK(正常に終了したことを知らせるメッセージ)で終わらない、などなどです。
このフィードバックにより、Beta 2ではDispose()はほぼClose()と同じに振る舞うように変更しました。私たちは例外スローの問題(このスレッドにもあるような問題です)が起きることが分かっていたので、Disposeを「賢く」することにしました。つまり、オープン状態でない時にはAbort()を呼び出したように振る舞うようにしたのです。このことは別の問題も引き起こすようになり、その最たるものがシステムについて信頼性の麺で論理的な判断ができないということでした。Disposeは例外を投げることができます。しかし問題がある場合にいつも例外が起きるわけではなくなったのです。最終的に私たちは IDissposableをICommunicationObjectから取り除かなくてはならないという決断に至りました。さらに多くの議論を重ねた後、ServiceHostとClinetBaseにはIDisposableを残すことにしました。多くのユーザは、Disposeが例外を投げるとしてもusing()を使ったり解放することを知らせるためのマーカーインターフェースを使える便利さを選ぶと考えたのです。この2つのクラスからも IDisposableを取り除くべきだったと言う方もいるでしょうが(私たちの中にもそう言う人たちがいましたし)、良くも悪くも落ち着くべきところに落ち着いた結果なのです。この辺については全員の同意が得られることはない問題なので、SDKにあるサンプルにベストプラクティスを盛り込む必要がありました。そのベストプラクティスが try{Close}/catch{Abort} の方法です。
回避策
Steve Smith氏はCloseConnectionという拡張メソッドを提案している。FinallyブロックではCloseでなくこのメソッドを呼ぶことで、Close/Abortロジックをカプセル化する。
bog1978氏がニュースグループに投稿した方法は、C#のラムダ式を使ってUsing風のコンストラクタを作るものだ(リンク)。このメソッドが新しいクライアントオブジェクトを受け取り、一方で本来Usingブロックが持つのと同じコードを無名メソッドが保持する。
そしてErwyn Van Der Mer氏によるWCF Service Proxy Helperクラス(リンク)がある。通常のWCF Service Proxyクラスの代わりにこのクラスを使うと、接続を閉じた時にそのインスタンスが正しい処理をおこなってくれる。またインスタンス化の時には、本来の WCF Service Proxyも自動的にインスタンス化し、そのインスタンスをリードオンリーのプロパティとして参照することができる。