次の方法で共有


WCF の必須要素

一方向呼び出し、コールバック、およびイベントについての知識

Juval Lowy

この記事は、.NET Framework 3.0 のプレリリース版に基づいています。ここに記載される情報は、いずれも変更される可能性があります。

この記事で取り上げる話題:

  • Windows Communication Foundation における呼び出しのモデル
  • 一方向操作およびコールバック操作
  • 双方向コールバックおよびイベント
  • パブリッシュとサブスクライブのフレームワーク
この記事で使用する技術:
.NET Framework 3.0
サンプルコードのダウンロード:
WCF2006_10.exe (345KB)

目次

一方向操作
コールバック操作
クライアント コールバックの設定
コールバックの再入可能性
イベント
パブリッシュとサブスクライブのフレームワーク
永続的サブスクライバの管理
イベントのパブリッシュ
永続的サブスクライバの管理的作業
キューを使用するパブリッシャとサブスクライバ
まとめ

従来のオブジェクト指向、およびコンポーネント指向プログラミング モデルでは、クライアントがメソッドを呼び出す方法は 1 つしかありません。つまり、クライアントが呼び出しを発行し、そのクライアントは、呼び出しが処理されている間ブロックされ、メソッドが戻ると実行を続行します。Windows Communication Foundation (WCF) には、この従来の呼び出しモデルの他に、2 種類の操作に対するサポートが組み込まれています。1 つは、発行するだけの操作の一方向呼び出しです。もう 1 つは、サービスがクライアントにコールバックする、双方向コールバックです。

既定で、Windows Communication Foundation の操作は、要求と返信として考えられます。つまりクライアントは、要求をメッセージの形で発行し、返信メッセージを取得するまでブロックされます。既定のタイムアウトである 1 分以内にサービスが応答しない場合、クライアントは、TimeoutException を受け取ります。また、通信またはサービス側で例外が発生した場合には、プロキシが、クライアント側に例外をスローします。NetPeerTcpBinding、および NetMsmqBinding を除いて、すべてのバインディングは要求と返信の操作をサポートします。

この記事では、まず Windows Communication Foundation 操作を呼び出し、起動する方法、および関連する設計ガイドラインを説明します。その後でこのような種類の操作を、イベントのパブリッシュおよびサブスクライブのためのカスタム フレームワークの構築ブロックとして使用する方法を解説します。その過程で、Microsoft .NET Framework、および Windows Communication Foundation の高度なプログラミング技術を多数紹介します。

一方向操作

操作には、戻り値がないことがあります。また、クライアントは呼び出しが成功したのか、または失敗したのかを考慮しないことがあります。このような発行するだけの呼び出しをサポートするために、Windows Communication Foundation は、一方向操作を提供します。クライアントが呼び出しを発行した後、Windows Communication Foundation は要求メッセージを生成しますが、それに対応する返信メッセージがクライアントに戻ることはありません。このため、一方向操作では値を戻すことができず、サービス側でスローされた例外もクライアントには伝わりません。一方向呼び出しは、非同期呼び出しとは異なります。複数の一方向呼び出しがサービスに到着した場合、それらの呼び出しは、すべて一度にはディスパッチされずに、サービス側のキューに入れられ、1 つずつディスパッチされることがあります。こうした動作はすべて、サービスで構成された並行モード動作、およびセッション モードに従います。サービスがいくつのメッセージ (一方向、または双方向に関わらず) をキューに入れるかは、構成されたチャネル、および信頼性モードによって決まります。キューに入れるメッセージの個数がキューの容量を超える場合、クライアントは一方向呼び出しを発行した場合でも、ブロックされます。しかし呼び出しがキューに入った場合、クライアントはブロックされることなく、サービスがバックグラウンドで操作を処理する間、実行を続行できます。これは通常、非同期呼び出しと同じように見えます。すべての Windows Communication Foundation バインディングは、一方向操作をサポートします。

OperationContract 属性には、Boolean の IsOneWay プロパティがあります。このプロパティは既定では、要求と返信の操作であることを意味する false に設定されています。しかし IsOneWay を true に設定すると、メソッドは一方向操作として構成されます。

[ServiceContract]
interface IMyContract
{
   [OperationContract(IsOneWay = true)]
   void MyMethod()
}

一方向操作には返信が関連付けられないため、戻される値、または結果を保持するポイントはありません。操作は出力パラメータがなく、戻り値の型が void である必要があります。言語コンパイラには、Windows Communication Foundation に関する手掛かりがないため、使用が適切であるかをコンパイル時に検証することはできません。適切であるかは、実行時ホストを読み込むときに、メソッドのシグネチャを検証することによって確認され、適切でない場合には InvalidOperationException がスローされます。

クライアントが呼び出しの結果を考慮しないということは、クライアントが、呼び出しが行われたかどうかを一切考慮しないということではありません。一般的に一方向呼び出しであっても、サービスの信頼性は考慮する必要があります。信頼性を確保するためには、要求がサービスに確実に配信されたかを確認します。ただし一方向呼び出しでは、クライアントが複数の一方向操作の呼び出し順序を考慮する場合と、しない場合とがあります。これは Windows Communication Foundation が信頼性のある配信を有効にすることと、順序の決定された配信、および実行を有効にすることを分離している大きな理由 の 1 つとなっています。

コールバック操作

Windows Communication Foundation は、サービスがクライアントにコールバックすることをサポートします。コールバック中には、多くの点において立場が逆転します。サービスがクライアントとなり、クライアントがサービスとなります (図 1 を参照)。


図 1 コールバック操作

クライアントは、コールバック オブジェクトのホストも行う必要があります。すべてのバインディングがコールバック操作をサポートしているわけではありません。HTTP は、そのコネクションレスという特性のため、コールバックには使用できません。つまり、BasicHttpBinding、WSHttpBinding ではコールバックを使用できません。Windows Communication Foundation は、NetTcpBinding、NetNamedPipeBinding でコールバック サポートを提供します。これは、基盤となるトランスポートが双方向であるためです。HTTP でコールバックをサポートするために、Windows Communication Foundation には WSDualHttpBinding があります。これは、実際には 2 つの HTTP チャネルを設定します。1 つはクライアントからサーバーへの呼び出しのためのチャネル、もう 1 つは、サーバーからクライアントへの呼び出しのためのチャネルです。

サービス コントラクトが保持できるコールバック コントラクトは、最大で 1 つです。コールバック コントラクトが定義されると、クライアントはコールバックをサポートする必要があり、すべての呼び出しにおいてサービスにコールバック エンドポイントを提供する必要があります。ServiceContract 属性には、Type 型の CallbackContract プロパティがあります。次のようにこのプロパティをコールバック コントラクト型に設定し、コールバック コントラクトの定義を提供する必要があります。

interface IMyContractCallback
{
   [OperationContract] 
   void OnCallback();
}

[ServiceContract(CallbackContract = typeof(IMyContractCallback))] 
interface IMyContract
{
   [OperationContract] 
   void DoSomething();
}

コールバック コントラクトには、ServiceContract 属性を付ける必要がないことに注意してください。これは、暗黙で指定されています。

クライアント側にインポートされるコールバック インターフェイスの名前は、元のサービス側の定義内の名前と必ずしも同じではありません。ここでは、サービス コントラクト インターフェイスの末尾に Callback という語を付加した名前になります。

クライアント コールバックの設定

コールバック オブジェクトのホスト、およびコールバック エンドポイントの公開はクライアントの役割です。サービス インスタンスの最内部の実行スコープは、次のようなインスタンス コンテキストです。

public sealed class InstanceContext : CommunicationObject, ...
{
   public InstanceContext(object implementation);
   ... // 他のメンバ 
}

クライアントが、コールバック オブジェクトをホストするために必要なことは、コールバック オブジェクトをインスタンス化し、そのコンテキストを作成することだけです。

class MyCallback : IMyContractCallback 
{
   public void OnCallback() {...}
}
IMyContractCallback callback = new MyCallback();
InstanceContext context = new InstanceContext(callback);

クライアントは、コントラクトでコールバック コントラクトを定義しているサービス エンドポイントとやり取りする場合は、必ず双方向通信を設定し、コールバック エンドポイントの参照をサービスに渡すプロキシを使用する必要があります。クライアントが使用するプロキシは、図 2 に示すように、特殊化されたプロキシ クラス DuplexClientBase<T> から派生させる必要があります。

クライアントは、コールバック オブジェクトをホストするためにインスタンス コンテキストを使用する、DuplexClientBase<T> のコンストラクタを提供する必要があります (通常のプロキシと同様に、サービス エンドポイント情報も必要です)。プロキシは、コールバック コンテキストに基づいてエンドポイントを作成するときに、コールバック エンドポイントの詳細をサービス エンドポイントの構成から推測します。コールバック エンドポイントのコントラクトは、サービス コントラクトのコールバック型で定義されたコントラクトです。コールバック エンドポイントは、発信呼び出しと同じバインディング (つまり、トランスポート) を使用します。Windows Communication Foundation は、アドレスとしてクライアントのマシン名を使用し、HTTP を使用する場合はポートも選択します。インスタンス コンテキストを双方向プロキシに渡し、そのプロキシを使用してサービスを呼び出すだけで、クライアント側のコールバック エンドポイントが公開されます。

SvcUtil、または Visual Studio 2005 を使用してプロキシ クラスを生成する場合、これらのツールは**図 3** に示すように、DuplexClientBase<T> を派生させたクラスを生成します。クライアントは、次のようにコールバックのインスタンスを作成し、1 つのコンテキストでそれをホストし、プロキシを作成してサービスを呼び出します。これで、コールバック エンドポイントの参照が渡されます。

class MyCallback : IMyContractCallback 
{
   public void OnCallback() {...}
}
IMyContractCallback callback = new MyCallback();
InstanceContext context = new InstanceContext(callback);

MyContractClient proxy = new MyContractClient(context);
proxy.DoSomething();

クライアントがコールバックを想定している限り、プロキシを閉じることはできないことに注意してください。プロキシを閉じることは、コールバック エンドポイントを閉じることになり、サービスがコールバックを試みたときに、サービス側でエラーが発生する原因となります。クライアント自体がコールバック コントラクトを実装する場合もあります。この場合、図 4 に示すように、通常クライアントはプロキシにメンバ変数を使用し、プロキシはクライアントが破棄されるときに閉じられます。

クライアント側のコールバック エンドポイントの参照は、クライアントが行うすべての呼び出しで、着信メッセージの一部としてサービスに渡されます。OperationContext クラスは、サービスがジェネリック メソッド GetCallbackChannel<T> を介して、コールバックの参照に簡単にアクセスできるようにします。サービスがコールバックの参照を実際にどのように処理するか、および参照をいつ使用するかの決定は、完全にサービスに任されます。サービスは、操作コンテキストからコールバックの参照を抽出し、後で使用するために保管することができます。またサービスの操作の中で、クライアントにコールバックするために参照を使用することもできます。図 5 は、保管する方法を示しています。

このサービスは、前に示したものと同じコールバック コントラクトの定義を使用し、静的ジェネリック リストで、IMyContractCallback 型インターフェイスへの参照を保管します。サービスはどのクライアントがその参照を呼び出しているか、およびクライアントが既にその参照を呼び出したことがあるかどうかを認識しないので、サービスは呼び出しごとにチェックを行い、既にリストにそのコールバックの参照が存在するかを確認します。リストにその参照が含まれない場合、サービスはリストにコールバックを追加します。サービス クラスには、CallClients 静的メソッドもあります。これは次のようにホスト側のどこでも、クライアントにコールバックするために簡単に使用できます。

MyService.CallClients();

このような呼び出しでは、呼び出す側はコールバック呼び出しに、何らかのホスト側のスレッドを使用します。このスレッドは、着信したサービス呼び出しを実行しているスレッドとはまったく関係ありません。

コールバックの再入可能性

サービスはコントラクト操作の実行の中で、渡されたコールバックの参照を呼び出したり、コールバックのリストを呼び出したりすることが必要となる場合があります。しかしそのような呼び出しは、既定ではサービス クラスが単一スレッド アクセス用に構成されているため、許可されません。サービス インスタンスにはロックが関連付けられ、同時にロックを取得し、サービス インスタンスにアクセスできるスレッドは 1 つのみとなります。操作の中でクライアントへの呼び出しを行うには、コールバックを呼び出している間、サービス スレッドをブロックすることが必要です。問題は、コールバックを戻したクライアントからの返信メッセージの処理は、同じロックを所有する必要があるため、デッドロックが発生してしまう点です。

デッドロックを避けるため、単一スレッド サービス インスタンスが、そのクライアントにコールバックを試みると、Windows Communication Foundation は、InvalidOperationException をスローします。解決方法としては 3 つの方法が可能です。1 つ目は、サービスをマルチ スレッド アクセス用に構成します。この場合サービスにはロックが関連付けられないため、コールバックが可能になります。しかしこれはサービスの同期化を提供する必要があるため、サービス側の開発者の負担が増します。

2 つ目の解決方法は、サービスを再入可能に構成することです。サービス インスタンスは再入可能に構成された場合でも、ロックに関連付けられ、単一スレッド アクセスのみが許されます。しかし、サービスがクライアントにコールバックする場合、Windows Communication Foundation は何も通知することなく、最初にロックを解除します。

次のコードは、再入可能に構成されたサービスを示しています。操作の実行中にサービスは操作コンテキストを使用し、コールバックの参照を取得しそれを呼び出します。制御はコールバックが戻った後にサービスに戻され、サービス自体のスレッドがロックを再取得する必要があります。

[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)]
class MyService : IMyContract
{
   public void DoSomething()
   {
      IMyContractCa llback callback = OperationContext.Current.
         GetCallbackChannel<IMyContractCallback>();
      callback.OnCallback();
   }
}

サービスが安全にクライアントにコールバックすることを可能にする 3 つ目の方法は、コールバック コントラクト操作を一方向操作として構成することです。この場合サービスは、並行性が単一スレッドと設定されている場合でもコールバックできます。ロックが競合する返信メッセージがないためです。

イベント

双方向コールバックは、通常イベントと関連して使用されます。クライアントはイベントによって、サービス側で発生した事柄について通知を受けることができます。イベントは直接的なクライアント呼び出しの結果の場合もありますが、サービスが監視している何かの結果である場合もあります。図 6に示すように、イベントを発生させるサービスはパブリッシャと呼ばれます。一方、イベントを受け取るクライアントは、サブスクライバと呼ばれます。

図 6 パブリッシャおよびサブスクライバ
図 6 パブリッシャおよびサブスクライバ

Windows Communication Foundation のイベントは、まさにコールバック操作ですが、イベントの特性としてパブリッシャとサブスクライバの関係は、通常クライアントとサービスの関係よりも結合性の低い関係となります。イベントを処理する場合、サービスは通常、複数のサブスクライバに同じイベントをパブリッシュします。パブリッシャは多くの場合、サブスクライバを呼び出す順序、またはサブスクライバのイベント処理の間に発生するエラーについて考慮しません。またサービスは、サブスクライバから結果が戻ることについても考慮しません。したがってイベント処理操作は戻り値の型が void であり、出力パラメータを持つことがなく、一方向として指定される必要があります。次のようにイベントは、独立したコールバック コントラクトとして分離し、同じコントラクトに、通常のコールバックとイベントを混在させないことをお勧めします。

interface IMyEvents
{
   [OperationContract(IsOneWay = true)]
   void OnEvent1();

   [OperationContract(IsOneWay = true)]
   void OnEvent2(int number);

   [OperationContract(IsOneWay = true)]
   void OnEvent3(int number, string text);
}

イベントに双方向コールバックをそのまま使用すると、多くの場合は、パブリッシャとサブスクライバの間の結合性が強くなり過ぎます。サブスクライバは、パブリッシュを行うすべてのサービスがアプリケーションのどこにあるか認識し、それらに接続する必要があります。サブスクライバが認識していないパブリッシャは、そのサブスクライバにイベントを通知することができません。これは既に配置されているアプリケーションにおいて、新しいサブスクライバを追加 (または、既存のサブスクライバを削除) することを困難にします。特定の種類のイベントがアプリケーションのどこで発生しても、必ずサブスクライバに通知が行われるようにする手段はありません。このためサブスクライバは、サブスクライブする場合およびサブスクライブを解除する場合のどちらでも、各パブリッシャに対して、コストの高くなる可能性のある呼び出しを何回も行う必要があります。パブリッシャが異なると、同じイベントでもサブスクライブする方法、またはサブスクライブを解除する方法が少し異なる場合があるため、それぞれのメソッドに、サブスクライバを結合させることもあります。

パブリッシャもほぼ同様であり、認識しているサブスクライバのみに通知できます。パブリッシャは、受信しようとしているサブスクライバすべてという方法でイベントを配信することはできません。また、イベントをブロードキャストする機能もありません。さらにすべてのパブリッシャはサブスクライバのリスト、およびパブリッシュ処理自体を管理するために必要なコードを備える必要があります。こうしたコードは、サービスで対応する必要のある本来のビジネスの問題についての処理とはほとんど関係がありません。また並行パブリッシュなどの高度な機能を取り入れる場合には、相当に複雑になる可能性があります。

双方向ベースのコールバックは、これに加えて、パブリッシャのライフラインとサブスクライバとの結合性も生み出します。サブスクライバはイベントをサブスクライブし、受信するためには実行されている状態である必要があります。イベントが発生したときにサブスクライバが実行されるようにする手段はありません。アプリケーションがサブスクライバのインスタンスを作成しておき、それでイベントを処理する必要があります。

最後に、サブスクリプションの設定はプログラムで行う必要があります。アプリケーションのサブスクリプションを構成したり、システムの稼動中にサブスクライバの設定を変更したりする簡単な管理的手段はありません。

これらの問題を解決するには、図 7に示すように、専用のサブスクリプション サービス、および専用のパブリッシュ サービスを導入し、パブリッシュとサブスクライブに基づく設計パターンを使用しながら、パブリッシャとサブスクライバを切り離すように設計します。


図 7 パブリッシュとサブスクライブのシステム

イベントをサブスクライブする必要のあるサブスクライバは、サブスクリプション サービスに登録します。サブスクリプション サービスは、サブスクライバのリストを管理し、同様にサブスクライブの解除も管理します。同じように、すべてのパブリッシャはパブリッシュ サービスを使用してイベントを発生させ、イベントをサブスクライバに直接的に配信することを避けます。サブスクリプション サービス、およびパブリッシュ サービスは、システムを分割し、間接的な結合とするための層を提供します。サブスクライバはパブリッシャの識別をまったく認識しなくなります。サブスクリプションのメカニズムがすべてのパブリッシャについて統一されるため、サブスクライバは、特定の種類のイベントをサブスクライブすることができるようになり、そのイベントはどのパブリッシャからのイベントであっても受信されます。実際にパブリッシャは、サブスクリプションのリストを管理する必要がなく、サブスクライバが何であるのかを認識しません。パブリッシャがパブリッシュ サービスに対してイベントを配信することで、イベントはそれを必要としているサブスクライバすべてに提供されるようになります。

定義できるサブスクライバは、2 種類あります。1 つは一時的サブスクライバで、メモリ内で実行されるサブスクライバです。もう 1 つは、永続的サブスクライバで、ディスクに永続化され、イベントが発生したときに呼び出すサービスを表します。一時的サブスクライバの場合、実行するサービスにコールバック参照を簡単に渡す方法として、双方向コールバック メカニズムを使用できます。永続的サブスクライバの場合は、サブスクライバのアドレスを参照として記録するだけです。イベントが発生したとき、パブリッシュ サービスは永続的サブスクライバのアドレスに対して呼び出しを行い、イベントを配信します。この 2 種類のサブスクリプションの大きな違いとして、永続的サブスクリプションはディスク、またはデータベースに保管できます。保管することによりアプリケーションをシャットダウンした場合、またはマシンが停止した場合でもサブスクリプションが永続化されているため、サブスクリプションの管理的な構成が可能になります。一時的サブスクリプションは、アプリケーションをシャットダウンした後も続けて保存しておくことはできません。一時的サブスクリプションは、アプリケーションの開始ごとに明示的に設定する必要があります。

パブリッシュとサブスクライブのフレームワーク

この記事に付属のソース コードには、パブリッシュとサブスクライブの完全なサンプルが含まれています。このコードは、単なるサンプルとしてパブリッシュとサブスクライブのサービスとクライアントを提供するのではなく、すべてのアプリケーションにこのようなサービスを実装し、サポートを追加することを自動化する汎用的なフレームワークを提供することを目的としています。フレームワーク構築の最初のステップとして、パブリッシュとサブスクライブの管理インターフェイスを分析し、一時的サブスクリプション、および永続的サブスクリプションへのパブリッシュのためにそれぞれのコントラクトを提供します。

一時的サブスクリプションの管理のために、次のように ISubscriptionService インターフェイスを定義します。

[ServiceContract]
public interface ISubscriptionService
{
   [OperationContract]
   void Subscribe(string eventOperation);

   [OperationContract]
   void Unsubscribe(string eventOperation);
}

ISubscriptionService は、実装しているエンドポイントが想定するコールバックコントラクトを識別しないことに注意してください。コールバック インターフェイスは次のように、アプリケーションで ISubscriptionService を継承し、必要なコールバック コントロールを指定することによって提供されます。

[ServiceContract(CallbackContract = typeof(IMyEvents))]
interface IMySubscriptionService : ISubscriptionService {}

ISubscriptionService のサブ インターフェイスに、操作を追加する必要はありません。一時的サブスクリプション管理機能は ISubscriptionService によって提供されます。Subscribe、または Unsubscribe の各呼び出しでは、サブスクライバがサブスクライブする、またはサブスクライブを解除する操作の名前 (イベント) を提供する必要があります。呼び出し側がすべてのイベントをサブスクライブする、またはサブスクライブを解除する場合は、空または null の文字列を渡すことができます。

ここでのフレームワークは、次のようにジェネリック抽象クラス SubscriptionManager<T> で ISubscriptionService のメソッドの実装を提供します。

public abstract class SubscriptionManager<T> where T : class
{
   public void Subscribe(string eventOperation);
   public void Unsubscribe(string eventOperation);
   ... // 他のメンバ 
}

SubscriptionManager<T> のジェネリック型パラメータは、イベントのコントラクトです。SubscriptionManager<T> は、ISubscriptionService から派生していないことに注意してください。

アプリケーションは、 ISubscriptionService の特定のサブインターフェイスをサポートする独自の一時的サブスクリプションサービスをエンドポイントの形で公開する必要があります。このためにアプリケーションは、SubscriptionManager<T> から派生するサービス クラスを提供し、コールバック コントラクトを型パラメータとして指定して、ISubscriptionService の特定のサブ インターフェイスを派生させる必要があります。

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MySubscriptionService : 
   SubscriptionManager<IMyEvents>,IMySubscriptionService {}

MySubscriptionService の中にはコードは一切必要ありません。これは IMySubscriptionService では新しい操作を何も追加しておらず、SubscriptionManager<T> が既に ISubscriptionService のメソッドを実装しているためです。

最後にアプリケーションは次のように、IMySubscriptionService のエンドポイントを定義する必要があります。

<services>
   <service name ="MySubscriptionService">
      <endpoint
         address = "..."
         binding = "..."
         contract= "IMySubscriptionService"
      />
   </service>
</services>

図 8 は、SubscriptionManager<T> が一時的サブスクリプションを管理する方法を示しています。SubscriptionManager<T> は、一時的サブスクライバを、次のような m_TransientStore という名前の静的ジェネリック ディクショナリに保管します。

static Dictionary<string,List<T>> m_TransientStore;

このディクショナリの各項目には、イベント操作の名前、およびそのすべてのサブスクライバが、リンク リストの形式で含まれます。SubscriptionManager の静的コンストラクタは、リフレクションを使用して、コールバック インターフェイス (SubscriptionManager の型パラメータ) のすべての操作を取得し、ディクショナリをすべての操作と空のリストで初期化します。Subscribe メソッドは、操作の呼び出しコンテキストからコールバックの参照を抽出します。呼び出し側が操作名を指定した場合、Subscribe は、ヘルパ メソッド AddTransient を呼び出します。このメソッドは、イベントのサブスクライバのリストの保管からサブスクライバを取得します。リストにサブスクライバが含まれていない場合、AddTransient はそれを追加します。

呼び出し側が操作名として空の文字列、または null を指定した場合、Subscribe はコールバック コントラクト内の各操作について、AddTransient を呼び出します。Unsubscribe も、同様に動作します。呼び出し側は、すべてのイベントをサブスクライブしてから、特定のイベントのみサブスクライブを解除できることに注意してください。

永続的サブスクライバの管理

永続的サブスクライバを管理するために、ここでは図 9に示すように、IPersistentSubscriptionService インターフェイスを定義します。

永続的サブスクライバを追加するには、呼び出し側が PersistSubscribe を呼び出し、サブスクライバのアドレス、イベント コントラクト名、および特定のイベント操作を指定する必要があります。サブスクライブを解除するには、呼び出し側が PersistUnsubscribe を使用し、同じ情報を指定します。IPersistentSubscriptionService は、サブスクライバがサービス側でどこに永続化されているかに一切影響されないことに注意してください。これは実装の詳細です。

前に説明した SubscriptionManager クラスで、IPersistentSubscriptionService のメソッドも実装します。

public abstract class SubscriptionManager<T> where T : class
{
   public void PersistUnsubscribe(
      string address, string eventsContract, string eventOperation);
   public void PersistSubscribe(
      string address, string eventsContract, string eventOperation);
   ... // 他のメンバ 
}

SubscriptionManager<T> は、SQL Server に永続的サブスクライバを保管します。SubscriptionManager<T> は、IPersistentSubscriptionService から派生していないことに注意してください。使用するアプリケーションは、独自の永続的サブスクリプションサービスを公開する必要がありますが、コールバックの参照は必要ないため、IPersistentSubscriptionService から新しいコントラクトを派生させる必要はありません。アプリケーションは、単純に SubscriptionManager<T> から派生させて、イベント コントラクトを型パラメータとして指定し、IPersistentSubscriptionService からの派生物を追加します。

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MySubscribtionService : SubscriptionManager<IMyEvents>,
   IPersistentSubscriptionService {}

MySubscriptionService の中にコードは一切必要ありません。これは、SubscriptionManager<T> が IPersistentSubscriptionService のメソッドを既に実装しているためです。

最後にアプリケーションは、次のように IPersistentSubscriptionService のエンドポイントを定義する必要があります。

<services>
   <service name ="MySubscriptionService">
      <endpoint
         address = "..."
         binding = "..."
         contract= "IPersistentSubscriptionService"
      />
   </service>
</services>

SubscriptionManager<T> による、IPersistentSubscriptionService のメソッドの実装を図 10 に示します。この実装は、サブスクライバをメモリ内のディクショナリではなく、SQL Server に保管することを除いて、図 8 と同様です。

 

アプリケーションが、一時的サブスクライバ、および永続的サブスクライバの両方を同じイベント コントラクトについて必要とする場合は、単純に、サブスクリプション サービス クラスを、ISubscriptionService の特殊化サブ インターフェイス、および IPersistentSubscriptionService の両方から派生させ、それぞれに対応した 2 つのエンドポイントを公開します。

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MySubscriptionService : SubscriptionManager<IMyEvents>,
   IMySubscriptionService,IPersistentSubscriptionService {}

イベントのパブリッシュ

このフレームワークでは、パブリッシュ サービスも簡単に実装できます。このサービスは、サブスクライバと同じイベント コントラクトをサポートしている必要があり、アプリケーションにおいて、パブリッシャに認識される唯一の接点である必要があります。パブリッシュ サービスは、エンドポイントでイベント コントラクトを公開するため、イベント コントラクトは、一時的サブスクライバとの双方向コールバックのみに使用する場合も、次のようにサービス コントラクトとして指定する必要があります。

[ServiceContract]
interface IMyEvents {...}

パブリッシュとサブスクライブのフレームワークには、次のように定義される PublishService<T> ヘルパ クラスがあります。

public abstract class PublishService<T> where T : class
{
   protected static void FireEvent(params object[] args);
}

PublishService<T> は、イベント コントラクト型の型パラメータを必要とします。独自のパブリッシュサービスを提供するには、次のように PublishService<T> を派生させ、FireEvent メソッドを使用して、一時的、または永続的に関わらずすべてのサブスクライバにイベントを配信します。

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyPublishService : PublishService<IMyEvents>,IMyEvents
{
   public void OnEvent1() { FireEvent(); }
   public void OnEvent2(int number) { FireEvent(number); }
   public void OnEvent3(int number,string text) {
      FireEvent(number,text); 
   }
}

FireEvent は、params キーワード付きでオブジェクト配列を使用するため、パラメータに関係なくどのような型のイベントを発生させる場合にも、FireEvent を使用することができます。

最後に、アプリケーションは、イベント コントラクトを持つパブリッシュ サービスのエンドポイントを公開する必要があります。

<services>
   <service name ="MyPublishService">
      <endpoint
         address = "..."
         binding = "..."
         contract="IMyEvents"
      />
   </service>
</services>

図 11 は、PublishService<T> の実装を示しています。

 

パブリッシュ サービスによるイベントの発生を簡略化するため、FireEvent メソッドは、サブスクライバに渡すパラメータを受け入れますが、FireEvent メソッドの呼び出し側では、サブスクライバで呼び出される操作の名前を指定しません。このため、FireEvent はスタック フレームにアクセスし、呼び出しメソッドの名前を抽出します。FireEvent は、PublishPersistent ヘルパ メソッドを使用して、すべての永続的サブスクライバにパブリッシュし、PublishTransient ヘルパ メソッドを使用して、すべての一時的サブスクライバにパブリッシュします。どちらのパブリッシュ メソッドも動作はほぼ同じで、SubscriptionManager<T> にアクセスし、それぞれのサブスクライバ リストを取得し、Publish メソッドを使用してイベントを発生させています。サブスクライバは、サブスクライバに対するプロキシの配列の形で戻されます。この配列が、Publish メソッドに渡されます。

Publish は、この時点で単純にサブスクライバを呼び出すことも可能です。しかし今回のサンプルでは、イベントの並行パブリッシュをサポートします。つまり、いずれかのサブスクライバが正常に制御されていない場合、またはイベントの処理に長い時間がかかる場合でも、他のサブスクライバが即座にイベントを受信することが妨げられないようにします。イベント操作を一方向として指定しても、非同期呼び出しを保証することにはならないことに注意してください。またここでは、イベント操作が一方向と指定されていない場合でも、並行パブリッシュをサポートするようにします。

Publish メソッドは、2 つの匿名メソッドを定義します。最初の匿名メソッドは、Invoke ヘルパ メソッドを呼び出し、指定された個々のサブスクライバにイベントを発生させます。その後、プロキシを閉じるように指定されている場合には閉じます。Invoke は、特定のサブスクライバ型に対してコンパイルされることはないため、これはリフレクション、または遅延バインディングを使用して呼び出しを行います。Invoke は、呼び出しによって発生する例外も抑制します。これは、例外はパブリッシュを行う側には関係ないためです。2 つ目の匿名メソッドは、スレッド プールからのスレッドによって実行されるように、最初の匿名メソッドをキューに入れます。最後に、Publish は、指定された配列のサブスクライバすべてについて、2 つ目の匿名メソッドを呼び出します。

PublishService<T> がサブスクライバを統一的に処理していることに注意してください。サブスクライバが一時的であるか、永続的であるかは関係ありません。唯一の違いは、永続的サブスクライバにパブリッシュを行った後は、プロキシを閉じる必要があることです。この統一性は、SubscriptionManager<T> の GetTransientList、および GetPersistentList のヘルパ メソッドによって実現されています。これらの 2 つのうち、GetTransientList の方は次に示すように単純です。

public abstract class SubscriptionManager<T> where T : class
{
   internal static T[] GetTransientList(string eventOperation)
   {
      lock(typeof(SubscriptionManager<T>))
      {
         List<T> list = m_TransientStore[eventOperation];
         return list.ToArray();
      }
   }
   ... // 他のメンバ 
}

GetTransientList は、指定された操作に対するすべてのサブスクライバを一時的保管から検索し、配列としてそれらを戻します。一方、GetPersistentList には大きな課題があります。永続的サブスクライバのプロキシのそのまま使用できるリストは存在しません。プロキシについてわかっていることは、そのアドレスのみです。このため GetPersistentList は、図12に示すように、永続的サブスクライバのプロキシをインスタンス化する必要があります。

各サブスクライバのプロキシを作成するには、GetPersistentList がそのアドレス、バインディング、およびコントラクトを必要とします。コントラクトは、SubscriptionManager の型パラメータです。アドレスを取得するために、GetPersistentList は GetSubscribersToContractEventOperation を呼び出します。これはデータベースにクエリし、指定されたイベントをサブスクライブする、永続的サブスクライバすべてのアドレスを含む配列を戻します。最後に、GetPersistentList は、各サブスクライバが使用するバインディングを必要とします。そのために GetPersistentList は、ヘルパ メソッド GetBindingFromAddress を呼び出します。このメソッドは、アドレス スキーマから、使用するバインディングを推測します。GetBindingFromAddress は、すべての HTTP アドレスを WSHttpBinding として処理します。

永続的サブスクライバの管理的作業

永続的サブスクリプションの追加、および削除は、図 9 に示した IPersistentSubscriptionService のメソッドを使用して実行時に行うことが可能ですが、永続的という特性からこのサブスクリプションの管理は、通常何らかの管理ツールを介して行われます。そこで 図 13に示すように、IPersistentSubscriptionService で、サブスクライバ ストアのさまざまなクエリに答える追加の操作を定義します。

これらの管理操作は、サブスクライバのアドレス、サブスクライブされるコントラクト、およびイベントを含む PersistentSubscription と呼ばれる単純なデータ構造を使用します。今回のパブリッシュとサブスクライブのフレームワークのサンプルには、図 14に示す、Persistent Subscription Manager という名前のサンプルの永続的サブスクライブ管理ツールが含まれています。

図 14 Persistent Subscription Manager
図 14 Persistent Subscription Manager (Click the image for a larger view))

このツールは、IPersistentSubscriptionService を使用して、サブスクリプションを追加、または削除します。新しいサブスクリプションを追加するには、ツールにイベント コントラクト定義のメタデータ交換アドレスを指定する必要があります。永続的サブスクライバそのもののメタデータ交換アドレスを使用することもできますが、メタデータ交換アドレスが多様である場合には、パブリッシュ サービスのメタデータ交換アドレスも使用できます。メタデータ交換アドレスを [MEX Address] テキストボックスに入力し、[Lookup] ボタンをクリックします。このツールは、プログラムでイベント サービスのメタデータを取得し、[Contract]、および [Event] のコンボボックスにデータを設定します。

サブスクライブするには、永続的サブスクライバのアドレスを指定し、[Subscribe] ボタンをクリックします。Persistent Subscription Manager は、サブスクリプション サービス (MySubscriptionService サービスがサンプルに含まれています) を呼び出すことによりサブスクリプションを追加します。サブスクリプション サービスのアドレスは、Persistent Subscription Manager の構成ファイルに保持されます。

キューを使用するパブリッシャとサブスクライバ

イベントをパブリッシュ、またはサブスクライブするごとに同期バインディングを使用する代わりに、NetMsmqBinding を使用することができます。イベントのパブリッシュ、およびサブスクライブにキューを使用することにより、疎結合システムによる利益と、非接続実行による柔軟性を併せて享受することができます。キュー イベントを使用する場合には、当然、コントラクトのすべてのイベントを一方向操作と指定しておく必要があります。図 15に示すようにキューは独立しており、どちらかの側だけでも使用できます。

図 15 キューを使用するパブリッシュとサブスクライブ**
図 15 **キューを使用するパブリッシュとサブスクライブ

キュー パブリッシャと、接続された同期サブスクライバを使用することができます。接続されたパブリッシャで、キュー サブスクライバに対してパブリッシュを行うこともできます。またパブリッシャ、およびサブスクライバの両方でキューを使用することもできます。しかし、一時的サブスクリプションではキューを使用できないことに注意してください。これは、MSMQ バインディングに双方向コールバックに対するサポートがないためです。前に説明したように、管理ツールを使用してサブスクライバを管理できます。管理操作は接続されており、同期的です。

キュー パブリッシャを使用するには、パブリッシュ サービスが、MSMQ バインディングを使用してキュー エンドポイントを公開する必要があります。キュー パブリッシャでイベントが発生する場合は、パブリッシュ サービスをオフラインとしたり、パブリッシュ クライアントそのものを切断したりすることができます。2 つのイベントをキュー パブリッシュ サービスにパブリッシュする場合、配信の順序、およびこれらのイベントが配信先のサブスクライバで処理される順序は保証されないことに注意してください。パブリッシュの順序は、イベント コントラクトがセッションに構成され、パブリッシュを行うサービスが 1 つである場合のみ推測できます。

キュー サブスクライバを配置するには、永続的サブスクライブ サービスがキュー エンドポイントを公開する必要があります。これにより、パブリッシャがオンラインのときに、サブスクライバをオフラインにすることができます。サブスクライバは再び接続したときに、キューに入れられたイベントをすべて受信します。さらに、キュー サブスクライバは、イベントの喪失がないことから、パブリッシュ サービス自体が切断される場合にも理想的な手段と言えます。単一のキュー サブスクライバで複数のイベントが発生した場合、イベントの配信の順序に関する保証はありません。サブスクライバは、イベント コントラクトがセッションに構成されている場合にのみ、パブリッシュの順序を推測できます。パブリッシャとサブスクライバの両方でキューを使用すれば、それらを同時にオフラインにすることも可能です。

まとめ

Windows Communication Foundation は、要求と返信、一方向操作、または双方向コールバックを使用する、強力で有用なプログラミング モデルの組み込みサポートを提供します。これらの操作を理解し、適切な呼び出しモデルを選択することは、最も重要な設計判断の 1 つです。またいずれのモードも、適用は驚くほど簡単です。これらの操作は、サービス指向アプリケーションでそのまま使用することができます。また、パブリッシュとサブスクライブのパターンのように、独自の高度な概念を組み立てることもできます。


Juval Lowyは、ソフトウェア アーキテクトであり、IDesign で、WCF トレーニング、および WCF アーキテクチャ コンサルティングを行っています。現在は、WCF に関する総合的な書籍を執筆中です。彼は、シリコン バレー地域の Microsoft Regional Director でもあります。Juval の連絡先については、www.idesign.netを参照してください。


 この記事は、 MSDN マガジン - 2006 年 10 月号からの翻訳です。 .


Back to top