ものがたり(旧)

atsushieno.hatenablog.com に続く

WCFのASP.NET AJAX統合を実装しよう

僕は割と最近になるまで全然知らなかったのだけど、.NET 3.5で追加されたWCFのRESTful bindingの機能のひとつに、ASP.NET AJAXを活用したJavascriptからの呼び出しなんてものがあったらしい。そしてどうやらそれが WebScriptServiceHostFactoryだのWebScriptEnablingBehaviorだのといったよく分からんかったクラスの存在意義だったということに気がついてびっくりしたというのは秘密だ(この辺のクラス名にまで言及していたことすらあるのに!)。まあ仕事柄 ASP.NETなんて普段全く使いませんもの。

というわけで今日は、皆さんがどうやったらASP.NET AJAX統合を実装できるかを書いてみようと思う(!)

まず参考用によそ様の記事を引っ張ってくることにしよう。僕が一番参考にしたのは以下の記事だ:

ASP.NET AJAXからWCFを使う場合の基本的なやり方は上の記事に、その補足が下の記事にある。

(念のために書いておくけど、ASP.NET AJAX統合の「使い方」の説明はしない。使い方は↑の記事などを読むと良いんじゃないかと思う。)

WCF呼び出しのJavascriptを生成する

上記2番目の記事の「なぜサービスを IIS にホストしなければならないのか?」の項では、WCFの呼び出しは http://hogehost/hogeservice.svc/jsdebug というURLから取得できるJavascriptによって行われる、ということが説明されている。ということで、まずIIS を使わずにこのJavascriptを生成するところから始めよう。これは簡単。ProxyGenerator.GetClientProxyScript(Type type, string path, bool debug)を使えばいい。type引数にはcontract interfaceを、pathにはリクエストを送るためのベースとなるURLを渡す。debugがtrueなら /jsdebug の、falseなら /js のスクリプトが生成できる。ちなみに皆さんがコレを実装する必要は無いが、僕が実装した時はasmx呼び出しのために書かれた既存のコードをちょろっと焼き直した程度でできた。

ServiceHostへの追加

さて生成されたJavascriptを /jsdebugへのリクエストにマッピングする(あと /js でデバッグコメントの埋め込まれていないJavascriptも返されるので、これにマッピングする)にはどうすればいいか。やり方は何通りか考えられるけど、とりあえずWCFでやっていそうな手法を紹介する。

これらの特別なURLはサービスのエンドポイントの一部として追加される(ASP.NET上でサービスを公開しているなら、デフォルトでは http://yourserver/hoge.svc/jsdebug という感じ)。これは、わかりやすく不正確に言えば *.svc ごとに、すなわちServiceEndpointごとに付いてくることになる。

WCF でサービスを動かすために必要なのはServiceHostだ。この /jsdebug などのページを生成するWCFサービスは、IISどころかASP.NETが無くてもホストできる(ただしこのJavascriptASP.NET AJAXから呼び出さないとほぼ意味がない)。通常のServiceHostにこの /jsdebug サポートを追加する方法はいくつかある:

  • 上記ページのいずれにもある通り、WebScriptServiceHostFactoryを使用するのが一番簡単だ。ちなみに WebScriptServiceHostFactoryの派生クラスを作って、protected なCreateServiceHost(Type type, Uri [] baseAddresses)を呼び出すメソッドを作ると楽だ。
  • もうひとつのやり方としては、WebScriptEnablingBehaviorを手作業で追加する方法だ。これはIEndpointBehavior を実装する(WebHttpBehaviorの派生クラスである)ので、ServiceEndpointに追加する。
  • さらに低レベルなやり方は、WebScriptEnablingBehaviorがやっていることを手作業で行う(!)方法だ。

まずWebScriptEnablingBehaviorの使い方だけざっと例示しておこう:


var host = new ServiceHost(typeof (FooService));
var se = host.AddServiceEndpoint ("IFooService", new WebHttpBinding (),
new Uri ("http://localhost:8080"));
se.Behaviors.Add (new WebScriptEnablingBehavior ());
host.Open ();

次に、WebScriptEnablingBehaviorのやっていることを手作業で行う方法について説明しようと思うが、その前にServiceHostの仕組みを簡単に説明しよう。

ServiceHostBaseとChannelDispatcherの役割

ServiceHostBase がホストするチャネルリスナは、ChannelDispatcherというクラスによって管理されている。ChannelDispatcherは、 IChannelListenerというインターフェースでのみBinding(のBuildChannelListenerメソッド)から提供されるリスナを使って、リクエスト受け付けチャネルを生成して、リクエストを処理し、次のリクエストを待ち受けるという作業を担当している。(IChannelListener自体は、その名に反して、そのような複雑な仕組みをもたず、誰かからAcceptChannelメソッドを呼ばれない限り、受付チャネルを生成することはないし、そこから生成されるIReplyChannelやIInputChannelが、リクエストを自動的に処理することもない。)

ServiceHostBaseには、ユーザあるいはconfigファイルによって、オープンされる前にひとつ以上のServiceEndpointが定義される(ひとつも定義されなければエラーになる)。ServiceEndpointは、いわゆるABC, address/binding/contract をもつクラスで、ServiceHostBase.AddServiceEndpoint()から返される。

ServiceHostBase.Open() が呼び出されると、このServiceEndpointひとつから、ChannelDispatcherがひとつ作成される。 ChannelDispatcherには、ひとつのIChannelListener(これは Binding.BuildChannelListener()が返す)と、ひとつ以上のエンドポイントがある。後者は EndpointDispatcherというクラスによって表される。ひとつのEndpointDispatcherには、通常ひとつのservice contractが対応し、それぞれに通常はひとつのEndpointAddressが存在する。


ServiceHost
- Description : ServiceDescription
- Endpoints : ServiceEndpointCollection
(ServiceEndpoint)
- Contract : ContractDescription
- Address : EndpointAddress
- ChannelDispatchers : ChannelDispatcherCollection
(ChannelDispatcher)
- Endpoints : SynchronizedCollection
(EndpointDispatcher)

さて、ここが重要なのだけど、ServiceHostBase.Open()が呼び出されたときに生成されるChannelDispatcherは、必ずしもServiceEndpointで定義されたものだけであるとは限らない。ServiceHostBase.ChannelDispatchersに手作業でChannelDispatcherを追加すれば、それもエンドポイントのような存在とみなされて、ServiceHostBaseがOpenしてリクエスト処理のループを回してくれるようになる。

今回の /js や /jsdebug に対するリクエストも、この仕掛けを利用して実現することができる。これらの文字列をフラットに返すservice contractを用意しておいて、message encoder(WebMessageEncodingBindingElementから生成される)で、 /js や /jsdebug の応答Messageの場合はJavascript文字列をそのままHTTP出力する、とすればよい。僕が実装した時は、service contractは単にMessageを返すようにして、Javascript文字列はPropertiesに追加して、message encoderで取り出すようにした。

ちなみに、ServiceHostBase.Open()が呼び出されると、IServiceBehaviorやIEndpointBehaviorなどのインスタンスそれぞれに対してApplyDispatchBehaviorメソッドが呼び出されることになるので、ChannelDispatcher を追加するならこのタイミングで行うことができる。WebScriptEnablingBehaviorが何をしているのか、これで想像できるのではないだろうか。

結論

あらためてwrap upしなくても分かった人もいるかもしれないけど、以上の仕組みは、ASP.NETとは無関係なところで行われている(ProxyGeneratorは、アセンブリとしてはASP.NET AJAXの一部だけど、これを使うためにASP.NETが動いている必要は全然無い)。ServiceHostでサービスを動かして、そこで /jsdebug を取得することも可能だ。

しかし、ここで取得したJavascriptASP.NETのページがクライアントとして連動していなければ、ほぼ意味がないのよね…というわけで、冒頭に紹介した記事の「IISが無いと使えない」という趣旨は概ね正しい。

いずれにしても、/js や /jsdebug のようなURLでスクリプトを生成するようにしたいのであれば、このChannelDispatcherを追加するやり方でいけるはずだ。

補論: ServiceMetadataBehavior

ちなみに、ここにservice contractではないものとして含まれうるもっとも典型的なものはコレだ: IMetadataExchange

IMetadataExchange は、WCFWSDLサポートで使われているものだ。http://hogeserver/hogeservice.svc?wsdl で出てくるあのページである。実際にはこのWSDL出力機能は、そのservice instanceに対応して、このcontractをもつ「もうひとつの」ChannelDispatcherを追加することで実現しているものだ。


ServiceHostBase
.Description
.Contract
.Name = "IFooService"
.ChannelDispatchers[0]
.Endpoints[0]
.ContractName = "IFooService"
.ChannelDispatchers[1]
.Endpoints[0]
.ContractName = "IHttpGetHelpPageAndMetadataContract"

実際に.NETでIMetadataExchangeのエンドポイントに割り当てられたContractNameは IHttpGetHelpPageAndMetadataContract というものなのだが、これはおそらくIMetadataExchangeの派生インターフェースとして機能しているのではないかと思う。そして、この長い名前のインターフェースでは、ServiceDebugBehaviorによって調整される「ヘルプページ」を返すことも期待されている…はずだ。実装では実際に行われているはずである。

応用

先にも述べたとおり、今回説明したのと同じようなやり方でWSDLやmex のサポートも実装されている。はずだ。だから、仕組みさえ分かってしまえば、たとえばRESTful bindingなのだからということで、WSDLの代わりにWADLを出力するような機能も実装できるようになる。はず。この辺をいじっているのが、 WSDLではなくSSDLをサポートするというsoya projectだ。SSDLに興味がある人はほとんどいないと思うけど、任意のエンドポイントに対して所定のメタデータを出力する仕組みを実装したいという人には、参考になるかもしれない。