ものがたり(旧)

atsushieno.hatenablog.com に続く

はじめに

.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つのステップが必要になる。

  • UriTemplate(とUriTemplateMatch)を用いて、リクエスURIからパラメータ値を文字列で取得する
  • それぞれのパラメータについて、文字列からランタイムオブジェクトを生成する

文字列パラメータからランタイムオブジェクトを生成する時に使用されるのが、再び登場するQueryStringConverterである。リクエストパラメータ文字列をランタイムオブジェクトに変換するにはこれのConvertStringToValue()を使用する。

WebHttpBehavior.GetQueryStringConverter()は拡張点のひとつとなっていて、任意の QueryStringConverterを返すことが出来\る。WebScriptEnablingBehaviorはこれを拡張して JsonQueryStringConverterを返すようになっている(はずだ)。

ランタイムメソッドの実行とWebOperationContext

リクエストパラメータがランタイムオブジェクトに変換されたら、ようやくサービスメソッドを呼び出すことが出来る(この呼び出しはWCFのコアで行われる)。

WCFのコア部分では、サービス実行中にOperationContext.Currentが適宜生成され、サービスメソッド上でユーザが IExtensionを通じて何らかの拡張動作を制御することができる。WebHttpBinding の場合は、WebOperationContextを通じてこれを参照または制御できる。

この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よう分からんし)。