WCFの基本的な概念:Windows Communication Foundation概説(2/2 ページ)
次期コミュニケーション技術「WCF」の最重要概念“ABC”とは何か? まずはWCFの基礎の基礎を押さえよう。
3. C:コントラクト=「What」
■3-1. サービス・コントラクトおよびオペレーション・コントラクト
コントラクトとは、サービスとして何が提供されているのか(=クライアントから見るとどのサービスを利用したいのか)、また交換すべきデータは何なのか(=データのスキーマはどのようになっているのか)、といったようなものを定義することになる。
サービスおよびそのサービスが提供するオペレーションのコントラクトの定義は、ソース・コード上で属性(Attribute)を用いて記述される。例えば下記のコードの場合、MyServiceクラスにはServiceContract属性が付与されており、サービス・コントラクトの実装クラスとして定義されていることが分かる。
また、そのクラス内に実装されているGetDataメソッドにはOperationContract属性が付与されており、つまりこのメソッドがサービスのオペレーション・コントラクトとして定義されているというわけだ。
[ServiceContract]
public class MyService
{
[OperationContract]
private Product GetData()
{
Product product = new Product();
product.ProductID = 1;
product.ProductName = "商品A";
product.UnitPrice = 1000;
product.Discontinued = false;
return product;
}
}
サービス・コントラクトとして実装するクラスにはServiceContract属性を付与し、オペレーション・コントラクトとして実装するメソッドにはOperationContract属性を付与する。
ちなみにこの例では、GetDataメソッドは「private」として宣言されている。しかし、OperationContract属性が付与されているため、サービスとしては外部に公開されることになる。こういった定義が一般的かどうかは別にして、このように、クラスのローカルでの振る舞いとサービスとしての外部への公開は明確に分離されており、外部へのサービス公開の有無はOperationContract属性の有無によってのみ決定される。
さて、先ほどの例のようにサービスの実装クラス自体にServiceContract属性やOperationContract属性を付与すると、サービスのインターフェイスがその実装から分離されていないため、プログラムの保守性や可読性の点で好ましいとはいえない。従って、一般的にはサービス・インターフェイスとサービス実装を分離する方法を取る。下記のコードがその場合の例である。
// サービス・インターフェイスの定義
[ServiceContract(Namespace = "http://www.sample.com/SampleService/")]
public interface IMyService
{
[OperationContract]
Product GetData();
}
// サービスの実装
public class MyService : IMyService
{
public Product GetData()
{
Product product = new Product();
product.ProductID = 1;
product.ProductName = "商品A";
product.UnitPrice = 1000;
product.Discontinued = false;
return product;
}
}
プログラムの保守性や可読性の観点からいって、このように、サービス・インターフェイスとサービス実装を分離することが一般的である。
ServiceContract属性が付与されているIMyServiceといったインターフェイスがサービス・コントラクトを定義していることになる。また、そのインターフェイス内に定義されるGetDataメソッドにOperationContract属性が付与されており、要するにこのメソッドがサービス・オペレーションであることを宣言していることになる。
このようにWCFでは、まずインターフェイスを定義し、それにサービス・コントラクトおよびオペレーション・コントラクトの属性を付与し、実際のサービスは、このインターフェイスを継承した実装クラスの方で実装するというスタイルを取る。
いま見てきたのはサービスとそのサービスが提供するメソッドに対するコントラクトであったが、次は交換するメッセージに対するコントラクトを見ていこう。
■3-2. データ・コントラクトおよびメッセージ・コントラクト
これには大きくデータ・コントラクト(Data Contract)とメッセージ・コントラクト(Message Contract)の2種類がある。また、メッセージに対する明確なコントラクトとして定義しない、あるいは定義する必要のないパターンとして、サービス・オペレーションのパラメータや戻り値にプリミティブ型を使用する場合や、Message型を使用する場合もある。以下の表にそれぞれの違いをまとめてみた。
コントラクト | 説明 |
---|---|
プリミティブ型 | StringやInt、Boolean、DateTime、Doubleなどの基本データ型は、CLRオブジェクトとXMLとの暗黙のマッピングが用意されており明示的なコントラクト定義は不要 |
データ・コントラクト | 主にユーザー定義型に対してCLRとXMLとのマッピングを定義したもので開発者から見て最も取り扱いやすいコントラクトの定義方法 |
メッセージ・コントラクト | SOAPのヘッダとボディを意識したクラスを定義する方法(いわゆる型付メッセージ)でヘッダやボディを構成する各要素にデータ・コントラクトを併用する場合が多い |
Message | すべてをMessage型(いわゆる「型なしメッセージ」)として取り扱い、オブジェクトからXMLの生成やXMLからオブジェクトの生成をコードでハンドリングする方法 |
交換するメッセージに対するコントラクトの種類 |
WCFでのメッセージ表現はSOAPである。たとえエンコーディングにバイナリを選択していたとしても中身はSOAPメッセージである。つまり、SOAPメッセージをどのようにプログラム内部でオブジェクトとして取り扱うかがポイントとなってくる。
上に挙げたコントラクトのうち、開発者にとって最も取り扱いやすいのはデータ・コントラクトである。下記のコードはデータ・コントラクトの非常に簡単な例である。
[DataContract]
public class Product
{
[DataMember]
public int ProductID;
[DataMember]
public string ProductName;
[DataMember]
public decimal UnitPrice;
[DataMember]
public bool Discontinued;
}
データ・コントラクトとして定義するクラスにDataContract属性を付与し、各データ・メンバにDataMember属性を付与すればよい。
ちなみに、このデータ・コントラクトに基づいて交換されるメッセージ(SOAPメッセージ)は下記のように表現されることになる。
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<SetData xmlns="http://www.sample.com/SampleService/">
<Product xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Discontinued xmlns="http://schemas.datacontract.org/2004/07/ClientSide">true</Discontinued>
<ProductID xmlns="http://schemas.datacontract.org/2004/07/ClientSide">1</ProductID>
<ProductName xmlns="http://schemas.datacontract.org/2004/07/ClientSide">Name</ProductName>
<UnitPrice xmlns="http://schemas.datacontract.org/2004/07/ClientSide">100</UnitPrice>
</Product>
</SetData>
</s:Body>
</s:Envelope>
ここでシリアライゼーションに使用されるのはDataContractSerializerであり、XMLでシリアライズされた場合、特に指定しなければ要素名はメンバ変数名に、また要素の登場順はアルファベット順になってしまうなどの問題があるため、実際にはDataMember属性にNameパラメータやOrderパラメータなどを付与し、生成されるXMLの内容をある程度制御することになる。
[DataContract(Name = "Prod", Namespace =
"http://www.sample.com/SampleService/Product")]
public class Product
{
[DataMember(Name = "ProductID1", Order = 0)]
private int m_productID;
[DataMember(Name = "ProductName", Order = 1)]
private string m_productName;
[DataMember(Name = "UnitPrice", Order = 2)]
private decimal m_unitPrice;
[DataMember(Name = "Discontinued", Order = 3)]
private bool m_discontinued;
public int ProductID
{
get { return m_productID; }
set { m_productID = value; }
}
public string ProductName
{
get { return m_productName; }
set { m_productName = value; }
}
public decimal UnitPrice
{
get { return m_unitPrice; }
set { m_unitPrice = value; }
}
public bool Discontinued
{
get { return m_discontinued; }
set { m_discontinued = value; }
}
}
DataMember属性のパラメータにNameパラメータやOrderパラメータなどを付与し、生成されるXMLの内容をある程度制御する。
ちなみに、このデータ・コントラクトに基づいて交換されるメッセージは下記のように表現されることになる。
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<SetData xmlns="http://www.sample.com/SampleService/">
<Product xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<ProductID>1</ProductID>
<ProductName>Name</ProductName>
<UnitPrice>100</UnitPrice>
<Discontinued>true</Discontinued>
</Product>
</SetData>
</s:Body>
</s:Envelope>
なお、DataMember属性が付与されていれば該当するメンバ変数はたとえそのアクセス修飾子がprivateであってもXMLにシリアライズされる。つまり、シリアライズされるかどうかはDataMember属性が付いているかどうかで決定されるのである。
次に、メッセージ・コントラクトを定義する方法、つまりSOAPメッセージのヘッダとボディを意識したクラスを定義する方法(いわゆる型付メッセージ)の場合であるが、以下に簡単なコード例を記載する。
[MessageContract]
public class SetDataRequest
{
[MessageHeader(MustUnderstand=true)]
public DateTime SetDataTime;
[MessageBody(Order=0)]
public Product product;
}
メッセージ・コントラクトとして定義するクラスにMessageContract属性を付与し、SOAPメッセージのヘッダとなるメンバにMessageHeader属性を、SOAPのボディとなるメンバにMessageBody属性を付与すればよい。
ここでのProductといった型自体は先に紹介したデータ・コントラクトで定義したものを使用し、SOAPメッセージのヘッダやボディを構成する各要素にメッセージ・コントラクトを併用した形となっている。このようにデータ・コントラクトのみではボディに対しての定義のみとなるが、メッセージ・コントラクトを組み合わせて使用することで、SOAPメッセージのヘッダやボディを意識した細かなシリアライズ制御が可能になる。
さて、ここまで見てきたものがコントラクトとなる。これら以外には型なしメッセージといった方法も取り得る。これは素のSOAPメッセージをダイレクトに扱う方法だ。下記のコードのように定義し、実際のSOAPメッセージの中身をXmlReaderクラスを用いて取り出したり、Message.CreateMessageメソッドなどで返却すべきSOAPメッセージをコードで構築する手法である。
[ServiceContract]
public interface ICalculator
{
[OperationContract]
Message Sum(Message message);
}
型なしメッセージではすべてをMessage型として取り扱う。
非常に柔軟なメッセージの取り扱いが可能となるが、スキーマ・タイプは「xs:any」となるためクライアント・サイドのプロキシ生成を開発環境により自動生成しても有用なものにはならないといったデメリットもある。
【コラム】WCFとWSDL
WCFでのABCの定義は、まさしくWSDLを構築しているといっても過言ではない。つまりWCFでは、WSDLのようなサービスとクライアント間のコントラクト中心の構築スタイルを取っているわけである。WSDLをそのまま手作業で構築するのは大変だが、WCFのプログラミング・スタイルにのっとることによって、容易にコントラクトであるWSDLを形成できる。つまりWCFにおいてはコーディングやコンフィグにより適切なWSDLが構築されるといったコード・ファーストの流れを取っている。下記にWCFの各定義要素とWSDLとの対応関係を図示している。このようにWCF上の各種定義がWSDLと1:1に対応しているのが分かる。
ただしWCFにおいてWSDLが自然に形成されていったとしても、ポイントとなってくるのはプログラム中のオブジェクトとXMLとのインピーダンス・ミスマッチをどのように取り扱うかという点だ。つまり、XMLで表現されるメッセージの相互運用性を厳密に確保するためには、どのようにXMLとしてシリアライズされるのかあらかじめ知っておく必要があるし、そのうえで微調整を行う必要が発生する。
この点に関してはWCFにおいても、従来のWebサービスと同様であり、WCFでのデータ・コントラクトの定義もメッセージ・コントラクトの定義もクラス・ファースト(クラスのコードを先に作成する)であるためオブジェクトのXMLへのシリアライズを意識する必要があるといえる。
なお、このシリアライゼーションの制御に関してWCFではどういったバリエーションが考えられるかというと、主に下記のような選択肢が取り得る。
- DataContract属性のみ使用する場合
- Serializable属性を付与したクラスを使用する場合
- Serializable属性を付与したクラスを使用し、XmlSerializerを使用する場合
- MessageContract属性を使用する場合
- Message型(いわゆる型無しメッセージ)を使用する場合
- IXmlSerializableインターフェイスを実装する場合
すべて一長一短があるため意思決定は単純とはいえない。なお、各バリエーションでの実装方法やメリット/デメリットの解説は、下記のサイトで掲載されているので興味のある方は一度目を通していただきたい(ただしコードサンプルはJan CTPベースであり最新のものではない)。
さて、今回はWCFの基本概念を説明した。WCFのプログラミングあるいはコンフィグレーションに必要となるA、B、Cといった概念がある程度ご理解いただけたのではないだろうか。
次回以降では今回の基本概念に基づき、実際のWCFのプログラミング方法を解説する予定である。
Copyright© Digital Advantage Corp. All Rights Reserved.