はじめに
.NET 3.5のWCFに新しく追加されたWebHttpBindingとその周辺をmonoで実装してみたので、細かいことを分かっている範囲でちまちまと書いてみる。how to use的な話を書いてもMSDNとかぶるだけなので書かない。あと例によってconfigurationを使ったやり方も僕の興味の範囲外なので書かない。
ユーザーコードで必要になるのは次のクラスのみである:
- WebMessageEncodingBindingElement (Sys.SM.Channels)
- WebHttpBehavior (Sys.SM.Description)
- WebGetAttribute or WebInvokeAttribute (Sys.SM.Web)
実際には、WebMessageEncodingBindingElementを直接使用しなくてもWebHttpBindingを使えばよいし、 WebServiceHost, WebServiceHostFactory, WebChannelFactoryを使えばWebHttpBehaviorなんて自分で使う必要は無い。これらはあくまでWebHttpBinding のサポートする機能を利用するために最低限必要になるものだ。
基本的なクラス
WebChannelFactoryとWebServiceHost
もし普段クライアント側でChannelFactoryを使っているのなら、それをWebChannelFactoryに置き換えると、後述する WebHttpBehavior(IEndpointBehavior)を自動的に補完してくれる。自分で ServiceEndpoint.BehaviorsにWebHttpBehaviorを追加するなら、WebChannelFactoryは不要だ。
同様のことがWebServiceHost, WebServiceHostFactoryにも言える(WebHttpBehaviorを補完してくれるだけ)。
WebHttpBindingとWebMessageEncodingBindingElement
Bindingは基本的にBindingElementの組み合わせを生成するだけだ。WebHttpBindingも、 WebMessageEncodingBindingElementとHttpTransportBindingElementを生成するだけなので、もしこれを手作業でやってCustomBindingを生成するような人(他にinterceptorを挟んだりするような人)には、 WebHttpBindingは必要ない。
WebMessageEncodingBindingElementは、MessageEncoderを生成するために用いられるものである。その MessageEncoderは、MessageをHTTP GETのリクエストに変換したり、POSTするリクエストの内容をJSON文字列に変換したり、逆にサービスからのJSONレスポンスをMessageに変換したりするために用いられる。
WebGetAttributeとWebInvokeAttribute
これらはOperationContractとなるメソッドに追加されている必要がある。
これらはIOperationBehaviorを実装していて、IOperationBehaviorを実装するようなAttributeは、 ContractDescription.GetContract()が呼び出された時に、OperationContractとなるメソッドから OperationDescriptionを生成する時に、そのBehaviorsに自動的に追加される。*1
WebHttpBehavior
WebHttpBindingで最も重要なクラスはこれだ(と僕は断言する)。このクラスから、以下のものが生成される。また、これらを生成するメソッドはいずれもvirtualなので拡張可能である。
- WebHttpDispatchOperationSelector
- これはIDispatchOperationSelectorの実装で、あるendpointに到達したMessageが、どのOperationを呼び出しているのか、選択するために用いられる。
- IClientMessageFormatter
- クライアント側で、ランタイムメソッド呼び出しで渡されたCLR型のパラメータ群から、Messageを生成するために用いられる (SerializeRequest)。また、サービスから返ってきたMessageからランタイムメソッド呼び出しの結果をCLRオブジェクトとして生成するために用いられる(DeserializeReply)。
- IDispatchMessageFormatter
- サービス側で、Messageからサービスのランタイムメソッド呼び出しに必要なCLRオブジェクトを生成するために用いられる(DeserializeRequest)。また、ランタイムメソッド呼び出しの結果からMessageを生成するために用いられる(SerializeReply)。
*1:OperationContractとなるメソッドに属性として設定せずにOperationDescription.Behaviorsに手動で追加してちゃんと動くかどうかは分からない。
リクエストの発行
ClientRuntime
クライアントコードでChannelFactoryからサービスインターフェースの実装をCreateChannel()で生成すると、それは実際には ServiceContractであるインターフェースから自動生成されたproxy typeのインスタンスであり、その中にはClientRuntimeが含まれている。
このClientRuntimeはServiceEndpointをもとに生成されるが、その際にServiceEndpointのBehavior (IEndpointBehavior)やContractDescriptionのBehavior(IContractBehavior)、 OperationDescriptionのBehavior(IOperationBehavior)について、 ApplyClientBehavior()が呼び出されることになる。
WebChannelFactoryで生成したServiceEndpointにはWebHttpBindingが自動的に追加されている(WebChannelFactoryを用いず、自ら追加することもできる)。このWebHttpBehaviorの ApplyClientBehavior()が呼び出されると、WebHttpBehaviorは、ClientRuntime.Operationsに含まれるそれぞれのClientOperationについて、Formatterプロパティの値を自らのIClientMessageFormatter 実装に設定する。これによって、通常のSOAPメッセージシリアライゼーションが、RESTfulなサービス呼び出し用のシリアライゼーションに切り替わることになる。
UriTemplate
インターフェース上のランタイムメソッド呼び出しに用いられるパラメータは、 IClientMessageFormatter.SerializeRequest()によってMessageに変換される。 WebHttpBindingで行われるリクエストは、SOAPを利用しない。リクエストパラメータは、HTTPのリクエストURLそのものに表される。従って、そのような可変のURLをあらわす「URIテンプレート文字列」が、何らかの形で提供されなければならない。
UriTemplateは、パラメータのテンプレートを独自のエスケープ構文で記述したURIテンプレート文字列から生成される。URIテンプレート文字列は、たとえばこんな感じだ:
- /foo?p1={foo}&p2={bar}
- /foo/{bar}/{baz}
URIテンプレート文字列はoperationごとに、WebGetAttributeあるいはWebInvokeAttributeに含まれる UriTemplateプロパティから与えられる。もしUriTemplateプロパティが空文字列なら、URIテンプレート文字列は次のように自動生成される:
- void Foo(string s, int i) -> /Foo?s={s}&i={i}
さて、実際のHTTPリクエストでこんなURLが用いられることはない。実際のリクエストのパス/クエリは、以下のようになるだろう:
- /foo?p1=hoge&p2=fuga
- /foo/2008/0101
このような実際の呼び出しURLを生成するのに用いられるのがUriTemplateクラスである。このUriTemplateは、ベースアドレスとなる Uriに、特定の文字列パラメータをバインドして、Uriを生成することができる。これによって、クライアント側でHTTP GETリクエストの送り先を決定するために用いられる。
UriTemplateはサービス側の処理でも重要な役割を果たす。
QueryStringConverter
さて、実際にUriTemplateをする前に、もう一つ要求される作業がある。UriTemplateにバインドできるのは文字列でしかないので、ランタイムオブジェクトを文字列に変換しなければならない。この目的で使用されるのがQueryStringConverterである。ランタイムオブジェクトからリクエストパラメータ値となる文字列に変換するにはこれのConvertValueToString()を使用する。
WebHttpBehavior.GetQueryStringConverter()は拡張点のひとつとなっていて、任意の QueryStringConverterを返すことが出来る。WebScriptEnablingBehaviorはこれを拡張して JsonQueryStringConverterを返すようになっている(はずだ)。
QueryStringConverterはサービス側でも重要な役割を果たす。
IRequestChannel.Request()
WebHttpBindingが返すIClientMessageFormatterは、ランタイムオブジェクトであるパラメータを、(全てではないが)UriTemplateを通じてUriに変換してしまう。これはMessage.Headers.Toに設定される。
生成されたMessageは、HttpTransportBindingElementから生成されるIChannelFactoryから生成される IRequestChannel(以下HttpRequsetChannelと仮称する)からRequest()に渡される。 HttpRequestChannel.Request()の中では、WebMessageEncodingBindingElementから生成された MessageEncoderを通じて、MessageからHTTPリクエストのStreamに出力される…が、実際に全てのパラメータが UriTemplateのパラメータとして展開されていれば(HTTP GETの場合はもともとStreamへの出力が許されない)、リクエストURLが全てを表しているので、単純にGETリクエストが発行されるだけである。
サービス側のリクエスト処理
ここからはサービス側の処理フローの説明になる。
WebHttpDispatchOperationSelector
サービスがメッセージを受け取るのは、TransportBindingElementから生成されたIChannelListenerから生成された IReplyChannelやIInputChannelを通じてのことである。WebHttpBindingの場合、 TransportBindingElementはHttpTransportBindingElementのみであり、その内部では HttpTransportBindingElementから生成されたIReplyChannel(面倒なので今後HttpReplyChannelと仮称しよう)か生成されたMessageであることが求められる。正確には、生成されたMessageに HttpRequestMessagePropertyやHttpResponseMessagePropertyが求められる。
さて、HttpReplyChannelから生成されてきたMessageは、ChannelDispatcherと EndpointDispatcherを経由してendpointに渡される。そして、そのServiceEndpointのContractに含まれるどのOperationの呼び出しと見なされるべきか、DispatchRuntimeのOperationSelectorによって判断されなければならない。それがIDispatchOperationSelectorである。WebHttpDispatchOperationSelectorはこれを実装するものだ。
ちなみに、DispatchRuntime.OperationSelectorは、WebHttpBehaviorがApplyDispatchBehavior()を呼び出された際に設定される。
UriTemplateTable, UriTemplateMatch
さて、WebHttpDispatchOperationSelectorは、Messageの情報から、endpointにあるoperationを選択しなければならない。その基準として利用されるのが、再びUriTemplateである。
繰り返しになるが、UriTemplateは、パラメータのテンプレートを独自のエスケープ構文で記述したURIテンプレート文字列から生成される:
- /foo?p1={foo}&p2={bar}
- /foo/{bar}/{baz}
そして実際にMessageに含まれるリクエストのパス/クエリは、以下のようなものになる:
- /foo?p1=hoge&p2=fuga
- /foo/2008/0101
このような実際の呼び出しURLが、あるURIテンプレート文字列にマッチするか判断したり、マッチした場合にパラメータの値を取り出すために利用されるのが、UriTemplateとUriTemplateMatchである。
さて、1つのendpointには1つのserviceと複数のoperationが含まれているので、あるMessageがどのoperationに向けられたものであるかを判断しなければならない。BasicHttpBindingなどの場合は、SOAP Actionをもとに判断していたが、WebHttpBindingの場合は、リクエストURLから判断されることになる。このために用いられるのが UriTemplateTableである。
UriTemplateTableには、複数のUriTemplateをもたせておいて、あるリクエストが来たら、そのURIがマッチするUriTemplateを適宜選択することができる(どれにも該当しなければエラーとなる)。
WebHttpDispatchOperationSelectorは、Message.Headers.ToのUriをもとに、このUriTemplateTableを使用して、マッチするUriTemplateをもつoperationを選択しているのである。
ちなみに、WebGetAttributeあるいはWebInvokeAttributeは、WebHttpDispatchOperationが ServiceEndpointから生成される時、そのContractに含まれるOperationDescriptionのそれぞれについて、 Behaviorとして追加されている必要がある(前述の通り、通常はContractDescription.GetContract()で追加されるはずである)。
ちなみに、UriTemplateは、ベースアドレスとなるUriに、特定の文字列パラメータをバインドして、Uriを生成することもできる。これはクライアント側でHTTP GETリクエストの送り先を決定するために用いられる。
IDispatchMessageFormatterとQueryStringConverter
さて、対象となるOperationが選択できたら、そのサービスメソッドをCLIランタイムの文脈で呼び出せるように、Messageをランタイムオブジェクトのパラメータ群に変換しなければならない。これを行うのがIDispatchMessageFormatter.DeserializerRequest()である。
Messageをランタイムオブジェクトに変換するには2つのステップが必要になる。
文字列パラメータからランタイムオブジェクトを生成する時に使用されるのが、再び登場するQueryStringConverterである。リクエストパラメータ文字列をランタイムオブジェクトに変換するにはこれのConvertStringToValue()を使用する。
WebHttpBehavior.GetQueryStringConverter()は拡張点のひとつとなっていて、任意の QueryStringConverterを返すことが出来\る。WebScriptEnablingBehaviorはこれを拡張して JsonQueryStringConverterを返すようになっている(はずだ)。
ランタイムメソッドの実行とWebOperationContext
リクエストパラメータがランタイムオブジェクトに変換されたら、ようやくサービスメソッドを呼び出すことが出来る(この呼び出しはWCFのコアで行われる)。
WCFのコア部分では、サービス実行中にOperationContext.Currentが適宜生成され、サービスメソッド上でユーザが IExtension
このWebOperationContextが生成されるタイミングは必ずしも自明ではないが、monoでは IDispatchMessageFormatterのDeserializeRequest()で生成し、SerializeReply()の呼び出し後に解放している。いずれにせよ、サービスメソッド呼び出し中は存在していることになる。
ユーザはコード上で特にOutgoingRequestやOutgoingResponseを操作することができる。 IDispatchMessageFormatter.SerializeReply()では、OutgoingRequestに加えられた変更を、戻り値となるMessageに追加されるHttpResponseMessagePropertyに反映している。
HTTPレスポンスの生成
サービスメソッド呼び出しの結果は、IDispatchMessageFormatter.SerializerReply()によってMessageに変換される。ここでは、レスポンスフォーマットに応じて、使用するXmlObjectSerializerが変わる。レスポンスフォーマットがJSONであれば、DataContractJsonSerializerが使用される。XMLであれば通常のDataContractSerializerが使用される(XmlSerializerが使用される場合があるかもしれない)。返されるMessageにはWebContentFormatをもつ WebBodyFormatMessagePropertyが追加される。
生成された応答Messageは、HttpReplyChannelからGetRequestContext()で返されたRequestContext のReply()によって送信される。この中では、WebMessageEncodingBindingElementから生成された MessageEncoderが用いられ、Messageが(HTTPレスポンスの)Streamに出力される。
MessageEncoderの中では、そのMessageがもつWebBodyFormatMessagePropertyのFormatの値によって、利用するXmlReaderあるいはXmlWriterが異なってくる。具体的には、JSONで応答が返される場合は、 JsonReaderWriterFactory.CreateWriter()を使用して返されたXmlWriterを使って、XML的な構造化ツリーをJSONの形で出力することになる。
クライアントによるレスポンスの処理
さて、クライアントはHttpRequestChannel.Request()を通じてMessageを応答として受け取ることになる。HTTPレスポンスとなるストリームは、そのContent-Typeによって、適切な解析フォーマットが選択され、それに基づいて処理される。
もしWebMessageEncodingBindingElementにWebContentTypeMapperが設定されていれば、その GetMessageFormatForContentType()によって、解析フォーマットが選択される。設定されていなければ、 application/jsonであればJSON、application/xmlであればXML…といったように決定される。
フォーマットが選択されたら、それに合わせて適宜XmlReaderが生成される。Xmlであれば通常のXmlReaderであり、JSONであれば JsonReaderWriterFactoryが使用される。また、WebBodyFormatMessagePropertyが設定される。
Messageが生成されたら、今度はそれがIClientMessageFormatter.DeserializeReply()に渡される。この時もフォーマットに応じて、XmlObjectSerializerが適宜生成される(DataContractSerializer / DataContractJsonSerializer)。これによってデシリアライズされた結果が、クライアント側のランタイムメソッドの結果として返される。
最後に、まだ不明なもの
WebHttpBindingを用いた一連の処理の流れは、以上の通りになるが、僕がまだ解いていないパズルがある。
- WebOperationContextのOutgoingRequest / IncomingResponseがどの場面で使用できるのだろうか。
- WebHttpBehaviorにはGetRequestDispatchFormatter()と GetReplyDispatchFormatter()があるが、DispatchOperationに設定できる IDispatchOperationFormatterは1つだけである(Formatterプロパティ)。これらはどう使い分けられているのか。 IClientMessageFormatterも同様である。
他にもあったかもしれないがとりあえず思い出せない。分かったらここに追記することにする。
2009-09-27追記: 前者は、サービス側ではなくクライアント側からHTTPリクエスト等のプロパティを操作したりHTTPレスポンス等のプロパティを参照したりするために存在している。後者は、単純に、それぞれのformatterをマージしたIDispatchMessageFormatterやIClientMessageFormatterを作って、それらのSerializeRequest/DeserializeRequest/SerializeReply/DeserializeReplyの各メソッドで、それらを使い分けていると考えれば解決だ。
おまけの笑い話
これね、デブサミ2008で .NET 3.5のセッションを聴きながら実装していたわけですよ。System.WorkflowServicesも実装したら面白いかもねえとか考えながら。こっちはWebHttpBindingの2倍くらいのクラス/メンバがあるから大変そうだけど(それにWFよう分からんし)。