ものがたり(旧)

atsushieno.hatenablog.com に続く

WCFのSystem.ServiceModel.PeerResolversのビミョーなバグ

WCFのduplex session channelが使えるところまでもっていこうとおもって、そのためにMSのchat applicationのサンプルを使っていろいろやっているのだけど、実態はほとんどCustomPeerResolverServiceの実装とバグフィックスになっている。それで、なかなかおもしろい…というかおもしろくないバグに遭遇したので書いておこうと思う。このバグがなぜ生じたかを知ると、何かしらの参考になるかもしれない。

今回のバグを一言で書くと、「IPeerResolverContractのサービスがServiceMetadataBehaviorを使ってWSDLで公開できない」ということだ。ちなみに僕はMSにこのバグを教えるつもりは無いので(というのもどうせ直せないと思っているので)、報告したい人は(僕はダメ元だと思うけど)こんなタイトルで報告してみるとちょうど良いと思う。まあ、心配しなくても、このバグで困る人はまずいない。

まず、いろいろ前提知識になる話(知っている人が読み飛ばせる部分)を書いておこう。

ServiceMetadataBehavior

ServiceMetadataBehaviorは、WCFのサービスをWSDLとして公開する時に使うIServiceBehavior実装だ。これをServiceHostBaseのDescription(ServiceDescription型)のBehaviorsに追加して、HttpGetUrlなど必要なメンバを設定してやると、その設定URLでasmxのページにアクセスした時のようなヘルパーページが出せるようになる。

NetPeerTcpBinding

NetPeerTcpBindingは、peer resolverという機能を使って、通信相手となるpeer nodeを探索して接続して、TCPでバイナリ メッセージをやりとりするBindingだ。このpeer resolverは、ピア ノードを取得したり、必要であればクライアント自らをノードとして登録したり、更新・削除したり、といった作業を行うためのものだ。peer resolverは、NetPeerTcpBindingの中に含まれるPeerResolverBindingElementが管理している。

勘違いしないでもらいたいのだけど、これから書く話は、NetPeerTcpBinding「を使った」サービスのWSDL公開とは何の関係もない。NetPeerTcpBinding「が」使っているunderlying layerとしてのpeer resolverについての話だ。

WindowsだとPNRPという機能を使うこともできるし(PnrpPeerResolverBindingElement)、カスタム ピア リゾルバを自分で実装することもできる(PeerCustomResolverBindingElement)。前者はWindows onlyなのでどうでもいいとして、後者の場合は、PeerResolverクラスを派生させてNetPeerTcpBinding.Resolver.Custom.Resolverに設定することになる。

IPeerResolverContract

PeerResolverを自前で実装するのは面倒だけど、実はこれを簡単に実装するための仕組みがWCFにはある。それがCustomPeerResolverServiceとIPeerResolverContractだ。これは、peer resolverの機能そのものをWCFで実装しちゃいました、というものだ。先のPeerCustomResolverBindingElementに、このIPeerResolverContractのサービスが動いているAddress, Bindingを指定してやるだけで使える(だけで、と言っても、もうこの時点でだいぶややこしいことになっているけど…)。

IPeerResolverContractのサービスとしては、CustomPeerResolverServiceクラスが使えるので、このインターフェースを実装する必要は何もない。クライアントは、PeerCustomResolverBindingElementの機能だけでも完結するし、どうしても自分でやりたい場合はIPeerResolverContractのClientBase<T>を派生させてもいい。

IPeerResolverContractはこんな感じのインターフェースになっていて、


namespace System.ServiceModel.PeerResolvers
{
public interface IPeerResolverContract
{
ServiceSettingsResponseInfo GetServiceSettings ();
RefreshResponseInfo Refresh (RefreshInfo refreshInfo);
RegisterResponseInfo Register (RegisterInfo registerInfo);
ResolveResponseInfo Resolve (ResolveInfo resolveInfo);
void Unregister (UnregisterInfo unregisterInfo);
RegisterResponseInfo Update (UpdateInfo updateInfo);
}
}

それぞれのHogehogeInfoクラスが(SOAPなどの)message bodyとしてやりとりされることになる。興味深いのは、これらのどの型もmessage contractに基づいている、ということだ。


namespace System.ServiceModel.PeerResolvers
{
[MessageContract (IsWrapped = false)]
public class ResolveInfo
{
...
}
}

message contractを使用している場合、そのクラスのメンバでMessageHeaderMemberAttributeやMessageBodyMemberAttributeが指定されたものが、メッセージのシリアライゼーションの対象になり、それらの属性によってXML要素名なども決まる(これらのメンバの型には(通常は)さらにDataContractAttributeが指定されていることになる)。.NETのResolveInfoには、これらの属性が指定されたpublicなメンバが存在しないので、それらがどういう実装になっているかは分からないが、message contractを使用している以上、nonpublicなメンバでこれに該当するものがあるはずだ。

前提知識はこのへんまで。

本題

今回のバグは、最初に一言で書いた通り、このIPeerResolverContractをServiceMetadataBehaviorでWSDL公開仕様とするとエラーになるというものだ。具体的には、こんなエラーになる*1:


IncludeExceptionDetailInFaults=true により作成された可能性のある ExceptionDetail の値:
System.InvalidOperationException: WSDL エクスポート拡張に対する呼び出しで例外がスローされました。System.ServiceModel.Description.DataContractSerializerOperationBehavior
コントラクト: http://schemas.microsoft.com/net/2006/05/peer/resolver:IPeerResolverContract ----> System.Xml.Schema.XmlSchemaException: グローバル要素 'http://schemas.microsoft.com/net/2006/05/peer:Update' は既に宣言されています。
場所 System.Xml.Schema.XmlSchemaSet.InternalValidationCallback(Object sender, ValidationEventArgs e)
場所 System.Xml.Schema.BaseProcessor.SendValidationEvent(XmlSchemaException e, XmlSeverityType severity)
...(snip)...

最初見たとき、これは全くもって謎だったのだけど、実装していてようやく分かってきた。これはIPeerResolverContractで使われているメッセージの仕様がおかしかったのである。ただしIPeerResolverContractで使われている引数と戻り値の型には全てMessageContractAttributeが使用されていて、MessageBodyMemberAttributeが設定されたpublicなメンバは存在しないので、問題が隠蔽されていて、よほど深入りしない限りこれは見えない。

問題はRegister()メソッドの戻り値型RegisterResponseInfoにあった。他の全てのメソッドの引数・戻り値型とは異なり、この戻り値型に対応するレスポンス メッセージの要素名は(RegisterResponseではなく)Updateになっている。これはUpdate()メソッドの引数型UpdateInfoと名前がかぶっているので、WsdlExporterがエラー扱いしてしまう、というわけだ。

ちなみに、そうなっていることは、実際にRegister()のサービスメソッドを呼び出してみないと分からない。本当なら、WSDLをチェックできれば一発で分かるのだけど、そのWSDL生成が失敗してしまうので、簡単に確認する術はない。Webサービスのメソッドの呼び出しを正常に行う方法を知るには、通常はWSDLを確認するところから始まるので、こういうサービスが実際にユーザ向けに公開されていたら、これはドツボにはまることになる。

対策

さいわいなことに、IPeerResolverContractのクライアントをゼロから作らなければならない人なんてのは滅多にいないので、今回の個別具体的な問題で困る場面というのはあまり想定されないが、似たような問題はmessage contractを使ってservice contractを規定しているどんな人でも発生させうるので、気をつけた方がいいかもしれない。WSDLを公開するなら、メッセージ仕様を変更するたびに、こまめにWSDL生成をチェックしておくとか。

*1:ちなみに、エラーの詳細を見るには、ServiceDebugBehaviorのIncludeExceptionDetailInFaultsをtrueに設定してやらないと出ないはずだ