ものがたり(旧)

atsushieno.hatenablog.com に続く

SlSvcUtilを作る方法

またここに書くネタが無かったので、SlSvcUtil.exeの作り方などという、割とどうでもいいネタを書こうと思う。

まず、SlSvcUtil.exe が何をするものなのかを説明しよう。これは、Tools for Silverlight 3に入っている、WSDL(など)をもとに、Silverlight環境向けにWCFのクライアントプロクシを自動生成するためのツールで、Visual Studioどっぷりのキミタチには全く関係のないものだ。やっていることは "Add Service Reference" の機能と変わらない。

今回は、このSlSvcUtil を自前で実装してしまおうという話だ。これが出来てしまえば、自分だけの開発環境やビルドシステムでWCFのクライアントプロクシ生成が実現できる。SharpDevelopEclipseみたいなIDEプラグインを追加したい場合なんかにも有用だろう。

.NET SDKというかWindows SDKには、SlSvcUtil.exe の前身である SvcUtil.exe というツールが存在する(どちらかと言えばこっちの方が知られていることだろう)。これは.NET 3.x向けのクライアントプロクシ(など)を自動生成するために使う。SlSvcUtilはSvcUtilの応用みたいなものなので、先にSvcUtilの作り方を説明してしまおう。ただし今回はクライアントプロクシ生成の話にしか興味が無いので、それ以外は言及しない。

SvcUtilについて書く前に、.NET 3.xのクライアントプロクシについて説明しよう。これは、IFooというコントラクトがあれば、System.ServiceModel.ClientBase<IFoo>の派生クラスであり、かつIFooを実装するものとして生成される。もっとも、IFooを実際に実装するのは、生成されたクラスであるとは言いがたい。生成されるクラスは単にClientBaseにあるIFoo型のプロパティChannelの該当メソッドを呼び出しているだけだ。そのChannelプロパティがどう生成されているかというと、ClientBaseのCreateChannel()メソッドによって生成されていて、そのメソッド自体は、標準ではChannelFactory<IFoo>のCreateChannel()メソッドを呼び出しているだけだ。そしてChannelFactory<IFoo>.CreateChannel()は、内部的に自動生成されるプロクシ型のインスタンスを返している。


public class FooClient : ClientBase, IFoo
{
public string Echo (string input)
{
return Channel.Echo (input);
}
}

// ClientBaseの実装
public class ClientBase : ...
{
public ChannelFactory<TChannel> ChannelFactory { get; private set; }

public TChannel Channel {
get {
if (channel == null)
channel = CreateChannel ();
return channel;
}
}

public virtual TChannel CreateChannel ()
{
return ChannelFactory.CreateChannel ();
}
}

つまり、SvcUtilを使おうが使うまいが、非ユーザーコードの領域ではクラスが動的に生成されているのである。実のところ、IFooのAPIを提供するプロクシを使うには、ChannelFactory<TChannel>.CreateChannel()で十分だと僕は思っている。

さて、次は生成するSvcUtilの実装について書こう。といっても、コレはWCFの機能を使うと簡単に実装できる。System.ServiceModel.Descriptionネームスペースにあるいくつかのクラスを使えばいい。MetadataExchangeClientでWSDLのURLからMetadataSetを生成して、WsdlImporterを使ってMetadataSetからContractDescriptionを生成して、ServiceContractGeneratorを使ってContractDescriptionからCodeDomを生成する。SvcUtilの大抵のクライアント生成オプションは ServiceContractGenerationOptions で指定できる。ここまでは難しいことは何も無い。

さて、SlSvcUtilで生成されるコードは、SvcUtilのそれとは少なからず異なる。最も大きいのはChannelBaseの存在だ。SilverlightのClientBase<TChannel>にはChannelBase<T>というクラスがあって、これは.NET 3.xでブラックボックスの中にあったClientBaseのChannelプロパティの実装を「公開の」型として提供するためにあるものだ。

SlSvcUtilは、ChannelBase<IFoo>から派生し、かつIFooを実装するクラスを、ClientBaseから派生するプロクシのネストしたprivateクラスとして定義する。そして、ClientBaseのCreateChannel()をオーバーライドして、そのクラスのインスタンスを返すようにする。すると、.NET 3.xで呼び出されていたコードの動的生成はもう必要ない。


public class FooClient : ClientBase, IFoo
{
...

private class FooChannel : ChannelBase, IFoo
{
...
}
}

問題は、ChannelBaseはSilverlightでのみ存在するクラスなので、これを標準で生成するわけにはいかないということだ。SilverlightのCoreCLRでは、SvcUtilを実装することはできない(CodeDomが存在しない)ので、SlSvcUtilも.NET 3.xのプロファイルで書かれている(はずだ)。何かしら、独自の拡張を追加して、このChannelクラスを実装しなければならない。

ここで使われるのがIServiceContractGenerationExtensionとIOperationContractGenerationExtensionだ。これらの無駄に名前の長いインターフェースを実装して、ContractDescriptionやOperationDescriptionのBehaviorsに追加すると(そのためにはIContractBehaviorなども実装しなければならないが)、ServiceContractGenerator.GenerateServiceContractType()が呼び出された時に、これらインターフェースのGenerateContract()メソッドなどが呼び出されることになる。GenerateContract()の引数にはServiceContractGeneratorの内部で作成されたServiceContractGenerationContextが渡され、その中には対象インターフェースのCodeDom (CodeTypeDeclaration) が含まれているので、これを加工するということになる。operationについても、GenerateOperation()メソッドの似たような呼び出しが行われる。

SlSvcUtilがやっているのは(本当にそうかどうかは分からないが)、これらのインターフェースを実装して、ChannelBaseの派生クラスをCodeDom上に追加するという作業だ、と考えられる。少なくとも、そうすればSvcUtilとあまり変わらない枠組みで無理なく実現が可能だ。

もっとも、これらの拡張が呼び出される仕組みは中途半端に不便なもので、実際にはインターフェースしか生成されていない時点で呼び出されてしまう。今回のChannelBaseの実装はプロクシクラスに追加されるものなので、これらのインターフェースのメソッドが呼び出された時点では、追加コードが単純には生成できない。これを解決するには、クラスなどの生成が全て終わった後に別途コード生成を行うとか、プロクシクラスのpartialを生成して、そこにChannelBaseを生成するといった方法が考えられる。

そんなわけで、以上のようなコードを実装してみた。といってもgmcsでgenericsまわりにバグがあるのでまだ実用できないのだけど。

まあ、コレを参考にする人は滅多にいないと思うけど、最初にも書いた通り、独自のIDEを開発したりする人には有用かもしれない。