ものがたり(旧)

atsushieno.hatenablog.com に続く

SilverlightのWCFは失敗作だ (または SL/WCFをいじる休息なき戦い)

導入と結論

今回じつに素敵なタイトルをつけることになったのだけど、これは、前々から何となく「Silverlightに.NET 3.5のRESTful serviceのサポートが無いのっていくらなんでもおかしくね?」と思っていたのを、連休中に実装してみようと思い立ってやってみた顛末をまとめてみた話だ。

結論から言うと、SilverlightWCFでは実現出来ない。その理由は、大まかに言えばBehaviorのサポートの欠如で、厳密に言えば、ClientOperationを取得する方法が提供されていないことにある*1

WebHttpBindingが無いってあんまりじゃね?

Silverlightには、.NETでRESTful serviceを実現するためのアセンブリであるSystem.ServiceModel.Web.dllと同じ名前を持つSystem.ServiceModel.Web.dllが含まれているし、RESTful serviceの実装の上に作られたAstoria(ADO.NET Data Services)のクライアント ライブラリもSDKアセンブリとして含まれている。

しかし、SilverlightWCFと.NETのWCFはだいぶ内容が違うことは言うまでもない。SilverlightではIListenerChannelをはじめとするサービス側のAPIは存在しないし、従ってWSDLサポートもWS-MEXサポートも存在しないし、もっと重要かつ喜ばしい話としては、WS-SecurityをはじめとするWS-*のサポートも存在しない(WSHttpBindingなど)。

ただ、削られて良かったものばかりではない。さっき、SilverlightにはSystem.ServiceModel.Web.dllが含まれていると書いたけど、Silverlightに含まれているSystem.ServiceModel.Web.dllに含まれているのはDataContractJsonSerializerだけだ。奇妙なことに、これが前提にしているはずのJsonReaderWriterFactoryは SDKアセンブリSystem.Runtime.Serialization.Json.dllに含まれているし、RSSAtomのサポートも別アセンブリ(System.ServiceModel.Syndication.dll)になっている。RESTful serviceまわりは骨抜きもいいところだ。

WebHttpBindingは確かに使いどころが微妙なんだけど、既にWCFなんてものを選んでしまってSOAP通いを続けるどうしようもない人たちにとって、.NET 3.5になって唯一の救いとして登場した代替のRESTful bindingを無かったことにしてしまうのも残念な話ではないか。そんなわけで、無いんだったら自分で作っちゃえ、幸いわれわれにはWebHttpBindingのオープンソース実装があるじゃないか、と思ったわけだ。

拡張性が無い

しかし今回問題になったSilverlightの大きな制限は、その拡張性だ。IContractBehavior, IOperationBehavior, IEndpointBehaviorといったインターフェースと、そのサポートが全て削られている。

これはWebHttpBindingの実装を動かす上で、けっこう大きな障害だ。WebHttpBindingがどのように実装されているかは、id:atsushieno:20080218 で詳しくまとめてあるので、気になる人はそちらを見てもらえると良いと思うけど、特にどの辺で問題になるかというと:

  • WebGetAttributeとWebInvokeAttributeはIOperationBehaviorの実装クラスだ。ContractDescription.GetContract()が呼び出されると、サービス定義メソッド上にあるAttributeのうち、IOperationBehaviorを実装しているものは、返されるContractDescription(の中の該当するOperationDescription)のBehaviorsに自動的に追加される仕組みになっている。SilverlightにはBehaviorsプロパティが存在しないので、これを適用するためには、Behaviorsとは異なる仕組みでApplyClientBehavior()を自分で呼び出して実現しなければならない。
  • WebHttpBindingのコアはWebHttpBehaviorクラスだ。そしてこのクラスはIEndpointBehaviorの実装になっている。SilverlightのServiceEndpointには、やはりBehaviorsが存在しないので、どこかしらで手作業でApplyClientBehavior()を呼び出してやる必要がある。

しかしまあこれらは重要な問題ではない。多少の互換性を犠牲にしてでも、Silverlight環境で動かすときは、WebChannelFactoryを使うよう要求し、ServiceEndpoint.Behaviors上に追加するのではなくWebChannelFactoryのプロパティにWebHttpBehaviorのインスタンスを追加するようにして、ApplyClientBehavior()をその中で呼び出すようにしてやれば良い。

問題は、WebHttpBinding.ApplyClientBehavior()の中で、IClientMessageFormatterを設定してやらなければならない部分にある。先の詳細記事でも言及しているが、WebHttpBehaviorは、HTTPリクエストのPUTのbodyではなくQueryStringからMessageを(UriTemplateとDataContractJsonSerializer経由で)生成する仕組みを実現するようなIClientMessageFormatterを、ClientOperationのFormatterプロパティに設定することで、その機能を実現している。しかし、SilverlightWCFには、ClientOperationを取得する方法が存在しないのである。.NETの場合は、ClientRuntimeにOperationsというプロパティがあって、そこから全てのClientOperationを拾い上げることができるのだけど、これがWCFでは提供されていない。

とりあえず今分かっているのは、これが理由で、Silverlight WCFではRESTful bindingをサポート出来ない、ということだ。(今分かっている範囲で、でしかないが)唯一の理由と言ってもいい。

コード(動かないけど)

当初、これはMonoのクラスライブラリの一部としてビルドシステムを再利用して作っていた。その時点ではビルドが上手くいって、しめしめと思っていたのだけど、これを使ってサンプルを組んでSilverlight上で動かしてみたら、メタデータが無いと言われて、気になってVS2008でビルドしてみたら実は出来ていなかったということに気づいて、VSのプロジェクトとして大幅にやり直した。VSのプロジェクトとしてはmcsのソースへの外部参照になっているし、そのmcsのソースには大幅なNET_2_1ビルド追加のためのパッチが必要になる。

…ということを前提にした上でソースを置いておくので、いや絶対出来るに違いない、ほかに方法があるぜ、と思った人はここからトライしてみてくださいな:

この問題をどう改善するか?

さて、とりあえず今分かっている問題点は、ClientOperationが取得できないということだけにある。それならば、MSのproduct feedbackにClientOperationを取得できるようClientRuntimeにメンバを追加してくれ(というか戻してくれ)というだけでも十分かもしれない。実のところ、SilverlightにClientOperationが存在する理由は全く分からない。取得しようがないのだから。

もっとも、この変更だけでちゃんと使い物になるかどうかは分からない。ひとつ考えているのは、MonoTouchにWCFが追加された時に、このメンバを追加して、上記コードを試してみるというやり方だ。MonoTouchはMoonlightとは異なる独自拡張で動いているので、これは割と実現可能な計画だ(メンバをひとつ追加するだけだし)。

何でSystem.Data.Services.Clientは動くの?

ちなみに、AstoriaのサポートがSilverlightにも存在していることは最初の方で言及したのだけど、WebHttpBindingが実装できないのに、何でその上にあるはずのAstoriaが含まれているのか、と疑問に思う人はいるかもしれない(いや、僕も疑問に思っている)。ただ、Silverlightで実現できていないのは、たかだかWebHttpBindingだけだ。その下回りで使われているDataContractJsonSerializerはSystem.ServiceModel.Web.dllに含まれているし、UriTemplateなどは自分で実装して利用することも可能だろう。

この辺をやってみようと思って、最近復活の噂が出てきたLingrC#クライアントを実装してみたので、ついでに公開しておこうと思う。とは言っても、実際にSilverlightやMonoTouchでコレを使ってみたわけではないので、まだその辺に移植したらAPI上の障害が残っている可能性はあるけど。
http://github.com/atsushieno/slingr/tree/master/slingr/

最初は、ふつうにWebHttpBindingを使って、Lingr APIに対応するサービスメソッドをServiceContractとして実装しようとしていたのだけど、WebHttpBindingがきちんと期待するHTTPリクエストを送ってくれないようなので、UriTemplateやDataContractJsonSerializerを使って「ほぼ同じ動作をするけどSystem.Net.WebClientを直接使う」ような実装に置き換えて、ようやく動くようになったものだ。

あと、DataContract(Json)SerializerではObserveまわりでLingr APIを適切に反映できない仕様上の問題があったので*2、内部ではさらにJSONレスポンスに型情報を追加していたりもする(そのためだけにSystem.Jsonに相当するコードが追加されている)。WCFってやっぱり使えないんじゃないかとも思った。

話をAstoriaに戻すと、僕が上記のコードでやっているように、service contractが決まり切っているものについては、手作業でメソッドを実装しつつ、内部ではHttpWebRequestを使って実装する、といったことをしているのではないかと思う。System.Data.Services.Client.dllもSDKアセンブリなので、ランタイムアセンブリに適用しうる特別ルールに依存しているわけでもないだろう。

最後に

連休を返しておくれ。

*1:実はこの2つは関係ないんだけど

*2:messageとpresenceで共通基底クラスを用意して配列化したら、戻り値のJSONに動的型情報が無いため、KnownTypesがあってもデシリアライズ出来ない