サービスのバージョン管理
ビジネス ニーズの変化、情報テクノロジの要件、その他の問題への対処などのさまざまな理由により、サービスの初期導入後と、場合によっては有効期間中に数回、サービス (およびサービスが公開するエンドポイント) を変更することが必要になる場合があります。 変更が発生するたびに、新しいバージョンのサービスが導入されます。 このトピックでは、Windows Communication Foundation (WCF) でのバージョン管理の考え方について説明します。
サービス変更の 4 つのカテゴリ
必要となるサービスの変更は、次の 4 つのカテゴリに分類できます。
コントラクトの変更 : 操作を追加する場合や、メッセージ内のデータ要素を追加または変更する場合など。
アドレスの変更 : エンドポイントが新しいアドレスを持つ別の場所にサービスを移動する場合など。
バインディングの変更 : セキュリティ機構の変更や、その設定の変更など。
実装の変更 : 内部メソッド実装を変更する場合など。
これらの変更の中には "互換性に影響する" ものとそうでないものとがあります。"互換性に影響しない" 変更では、以前のバージョンで正常に処理されたメッセージは新しいバージョンでも正常に処理されます。 この条件を満たさない変更はすべて、"互換性に影響する" 変更です。
サービス指向とバージョン管理
サービス指向の基本思想の 1 つは、サービスとクライアントが自律 (または独立) しているということです。 これは、サービス開発者が、すべてのサービス クライアントを制御または把握するということを想定できないことを意味します。 これにより、サービスのバージョンが変更されたときに、すべてのクライアントを再構築し、再展開するという選択肢はなくなります。 このトピックでは、この基本思想に従ってサービスを想定しているため、クライアントから独立して、サービスを変更または "バージョン管理" する必要があります。
互換性に影響する変更が想定外であり、回避できない場合、アプリケーションはこの基本思想を無視し、新しいバージョンのサービスでクライアントを再構築し、再展開することを要求できます。
コントラクトのバージョン管理
クライアントが使用するコントラクトは、サービスが使用するコントラクトと同じである必要はありません。これらのコントラクトに必要なのは互換性だけです。
サービス コントラクトの場合、互換性は、サービスが公開する新しい操作は追加できても、既存の操作をセマンティックに削除または変更することはできないことを意味します。
データ コントラクトの場合、互換性は、新しいスキーマの種類の定義は追加できても、既存のスキーマの種類の定義を互換性に影響する方法では変更できないことを意味します。 互換性に影響する変更として、データ メンバーの削除や、データ型の互換性のない変更などがあります。 この特徴により、クライアントの互換性に影響しなければ、サービスはコントラクトのバージョンをある程度自由に変更できます。 次の 2 つのセクションでは、WCF のデータおよびサービス コントラクトに対して行うことができる、互換性に影響しない変更と互換性に影響する変更について説明します。
データ コントラクトのバージョン管理
ここでは、DataContractSerializer クラスと DataContractAttribute クラスを使用する場合のデータのバージョン管理について説明します。
厳密なバージョン管理
バージョンの変更が問題となる多くのシナリオでは、サービス開発者はクライアントを制御できないため、メッセージの XML やスキーマの変更に対処する方法を想定できません。 このような場合、次の 2 つの理由から、新しいメッセージが古いスキーマに対して有効であることを保証する必要があります。
スキーマが変更されることはないという前提で旧クライアントが開発されている場合、 クライアントは、その設計で対象としていないメッセージを処理できない可能性があるため。
旧クライアントがメッセージの処理を試みる前に、実際のスキーマ検証を古いスキーマに対して実行する可能性があるため。
このようなシナリオでは、既存のデータ コントラクトを変更不可として扱い、一意の XML 修飾名を持つ新しいデータ コントラクトを作成することをお勧めします。 この場合、サービス開発者は、既存のサービス コントラクトに新しいメソッドを追加するか、新しいデータ コントラクトを使用するメソッドを含む新しいサービス コントラクトを作成します。
多くの場合、サービス開発者は、データ コントラクトのすべてのバージョン内で実行するビジネス ロジックと、データ コントラクトの各バージョンに固有のビジネス コードを作成することが必要になります。 インターフェイスを使用してこの要件を満たす方法について、このトピックの最後にある付録で説明します。
厳密でないバージョン管理
他の多くのシナリオでは、サービス開発者は、新しい任意のメンバーをデータ コントラクトに追加しても、既存のクライアントの互換性には影響しないという想定を行うことができます。 この場合、サービス開発者は、既存のクライアントがスキーマ検証を実行しないかどうか、また、未知のデータ メンバーを無視するかどうかを調べる必要があります。 このようなシナリオでは、互換性に影響しない方法で新しいメンバーを追加するためのデータ コントラクト機能を利用できます。 サービスの最初のバージョンで、バージョン管理のためのデータ コントラクト機能が既に使用されている場合には、サービス開発者は確信を持ってこの想定を行うことができます。
WCF、ASP.NET Web サービス、および他の多くの Web サービス スタックは、"厳密でないバージョン管理" をサポートしています。つまり、受信したデータに新しい未知のデータ メンバーが含まれていても例外をスローすることはありません。
新しいデータ メンバーを追加しても、既存のクライアントの互換性に影響することはないと誤解しがちです。 すべてのクライアントが厳密でないバージョン管理に対処できるという確信がない場合は、厳密なバージョン管理のガイドラインに従い、データ コントラクトを変更不可として扱うことをお勧めします。
データ コントラクトの厳密でないバージョン管理、および厳密なバージョン管理の詳細なガイドラインについては、「ベスト プラクティス: データ コントラクトのバージョン管理」を参照してください。
データ コントラクトと .NET 型の区別
.NET クラスまたは構造体は、DataContractAttribute 属性をクラスに適用することにより、データ コントラクトとして投影できます。 .NET 型とそのデータ コントラクト投影は、2 つの異なる要素です。 同じデータ コントラクトが投影された複数の .NET 型を使用できます。 この区別は、投影されたデータ コントラクトを維持することによって、厳密な意味で既存のクライアントとの互換性を保ちながら、.NET 型を変更できるという点で特に有用です。 .NET 型とデータ コントラクトのこの区別を維持するには、次の 2 つのことを必ず行う必要があります。
Name と Namespace を指定します。 コントラクトに .NET 型の名前と名前空間が公開されないように、データ コントラクトの名前と名前空間を必ず指定する必要があります。 これにより、後で .NET の名前空間や型名を変更することにした場合でも、データ コントラクトの変更はありません。
Nameを指定します。 コントラクトに .NET メンバー名が公開されないように、データ メンバーの名前を必ず指定する必要があります。 これにより、後でメンバーの .NET 名を変更することにした場合でも、データ コントラクトの変更はありません。
メンバーの変更または削除
厳密でないバージョン管理が許可されている場合でも、メンバーの名前またはデータ型の変更や、データ メンバーの削除は、互換性に影響する変更です。 このような変更が必要な場合は、新しいデータ コントラクトを作成します。
サービスの互換性が非常に重要である場合は、コードで使用していないデータ メンバーを無視するよう考慮し、これらのメンバーをそのまま残しておきます。 データ メンバーを複数のメンバーに分割する場合は、必要な分割と下位レベル クライアント (最新のバージョンにアップグレードされていないクライアント) の再集約を実行できるプロパティとして、既存のメンバーをそのまま残すよう考慮します。
同様に、データ コントラクトの名前または名前空間の変更も互換性に影響する変更です。
未知のデータのラウンド トリップ
一部のシナリオでは、新しいバージョンで追加されたメンバーによって生じる未知のデータを "ラウンド トリップ" する必要があります。 たとえば、新しく追加された複数のメンバーを含むデータを、"versionNew" サービスから "versionOld" クライアントに送信するとします。 クライアントは、メッセージを処理するときに新しく追加されたメンバーを無視しますが、新しく追加されたメンバーを含む同じデータを versionNew サービスに再送信します。 この一般的なシナリオとして、サービスから取得したデータを変更して返すデータ更新があります。
特定の型のラウンド トリップを有効にするには、この型に IExtensibleDataObject インターフェイスを実装する必要があります。 このインターフェイスには、ExtensionData 型を返す ExtensionDataObject プロパティが含まれます。 プロパティは現在のバージョンでは未知の、今後使用されるデータ コントラクトの任意のデータを格納するために使用されます。 このデータはクライアントに対して不透明ですが、インスタンスをシリアル化すると、ExtensionData プロパティの内容がデータ コントラクト メンバーのその他のデータと共に書き込まれます。
今後の新しい未知のメンバーを格納するために、すべての型でこのインターフェイスを実装することをお勧めします。
データ コントラクト ライブラリ
コントラクトを中央のリポジトリに公開するデータ コントラクトのライブラリを用意できます。サービスおよび型の実装側は、このリポジトリからデータ コントラクトを実装し、公開します。 この場合、データ コントラクトをリポジトリに公開すると、そのコントラクトを実装する型の作成者を制御することはできません。 したがって、コントラクトを一度公開すると、変更することができないため、そのコントラクトは実質的に変更不可となります。
XmlSerializer を使用する場合
XmlSerializer クラスを使用する場合にも、バージョン管理の同じ原則が適用されます。 厳密なバージョン管理が必要な場合は、データ コントラクトを変更不可として扱い、新しいバージョンの一意の修飾名を持つ新しいデータ コントラクトを作成します。 厳密でないバージョン管理を使用できることが確実である場合は、新しいバージョンでシリアル化可能な新しいメンバーを追加できますが、既存のメンバーを変更または削除することはできません。
Note
XmlSerializer は、XmlAnyElementAttribute 属性と XmlAnyAttributeAttribute 属性を使用して、未知のデータのラウンド トリップをサポートします。
メッセージ コントラクトのバージョン管理
メッセージ コントラクトのバージョン管理に関するガイドラインは、データ コントラクトのバージョン管理とよく似ています。 厳密なバージョン管理が必要な場合、メッセージ本文を変更するのではなく、一意の修飾名を持つ新しいメッセージ コントラクトを作成します。 厳密でないバージョン管理を使用できることがわかっている場合は、メッセージ本文の新しい部分を追加できますが、既存の部分を変更または削除することはできません。 このガイドラインは、ベア メッセージ コントラクトとラップ メッセージ コントラクトの両方に適用されます。
厳密なバージョン管理を使用している場合でも、メッセージ ヘッダーはいつでも追加できます。 MustUnderstand フラグは、バージョン管理に影響を及ぼすことがあります。 一般に、WCF におけるヘッダーのバージョン管理モデルは、SOAP 仕様に記載されています。
サービス コントラクトのバージョン管理
データ コントラクトのバージョン管理と同様に、サービス コントラクトのバージョン管理にも、操作の追加、変更、および削除が含まれます。
名前、名前空間、およびアクションの指定
既定では、サービス コントラクトの名前はインターフェイスの名前です。 その既定の名前空間は http://tempuri.org
で、各操作のアクションは http://tempuri.org/contractname/methodname
です。 サービス コントラクトの名前と名前空間、および各操作のアクションを明示的に指定して、http://tempuri.org
を使用しないようにし、サービスのコントラクトにインターフェイス名とメソッド名が公開されないようにすることをお勧めします。
パラメーターと操作の追加
サービスが公開するサービス操作の追加は互換性に影響しない変更です。既存のクライアントは、追加された新しい操作を考慮する必要がないからです。
Note
双方向コールバック コントラクトへの操作の追加は、互換性に影響する変更です。
操作パラメーターまたは戻り値の型の変更
新しい型が古い型と同じデータ コントラクトを実装している場合を除き、パラメーターまたは戻り値の型の変更は、一般に互換性に影響する変更です。 このような変更を行う場合は、新しい操作をサービス コントラクトに追加するか、新しいサービス コントラクトを定義します。
操作の削除
操作の削除も互換性に影響する変更です。 このような変更を行う場合は、新しいサービス コントラクトを定義し、新しいエンドポイントで公開します。
エラー コントラクト
FaultContractAttribute 属性を使用すると、サービス コントラクト開発者は、コントラクトの操作から返すことのできるエラーに関する情報を指定できます。
サービスのコントラクトに記載されたエラーのリストは、完全なリストとは見なされません。 操作がコントラクトに記載されていないエラーを返す可能性は常にあります。 したがって、コントラクトに記載されたエラーのセットの変更は、互換性に影響しないものと見なされます。 たとえば、FaultContractAttribute を使用して新しいエラーをコントラクトに追加する場合や、コントラクトから既存のエラーを削除する場合などです。
サービス コントラクト ライブラリ
組織では、コントラクトを中央のリポジトリに公開する、コントラクトのライブラリを用意できます。サービス実装側は、このリポジトリからコントラクトを実装します。 この場合、サービス コントラクトをリポジトリに公開すると、そのコントラクトを実装するサービスの作成者を制御することはできません。 したがって、サービス コントラクトを一度公開すると、変更することができないため、実質的にそのコントラクトは変更不可になります。 WCF では、コントラクトの継承をサポートしています。継承を使用することにより、既存のコントラクトを拡張した新しいコントラクトを作成できます。 この機能を使用するには、古いサービス コントラクト インターフェイスから継承した新しいサービス コントラクト インターフェイスを定義し、この新しいインターフェイスにメソッドを追加します。 次に、古いコントラクトを実装するサービスを変更して新しいコントラクトを実装し、新しいコントラクトを使用するように "versionOld" エンドポイントの定義を変更します。 "versionOld" クライアントには、エンドポイントが "versionOld" コントラクトを引き続き公開しているように見えます。"versionNew" クライアントには、エンドポイントが "versionNew" コントラクトを公開しているように見えます。
アドレスとバインディングのバージョン管理
クライアントが新しいエンドポイント アドレスまたはバインディングを動的に検出できない場合、エンドポイント アドレスとバインディングの変更は互換性に影響する変更です。 新しいエンドポイントまたはバインディングを動的に検出する機能を実装する 1 つの方法として、クライアントがエンドポイントとの通信を試みるときに、UDDI (Universal Discovery Description and Integration) レジストリと UDDI 呼び出しパターンを使用します。エラーが発生した場合、現在のエンドポイントのメタデータについて既知の UDDI レジストリに照会します。 クライアントは、このメタデータからアドレスとバインディングを使用して、エンドポイントと通信を行います。 この通信が成功した場合、クライアントは今後使用できるように、そのアドレスとバインディングをキャッシュします。
ルーティング サービスとバージョン管理
サービスに対して行った変更が互換性に影響するもので、複数の異なるバージョンのサービスを同時に実行する必要がある場合、WCF ルーティング サービスを使用して、メッセージを適切なサービス インスタンスにルーティングすることができます。 WCF ルーティング サービスは、メッセージの内容に基づくルーティングを使用します。つまり、メッセージ内に含まれる情報に応じて、そのメッセージのルーティング先が特定されます。 WCF ルーティング サービスの詳細については、「ルーティング サービス」を参照してください。 WCF ルーティング サービスを使用したサービスのバージョン管理の例については、「方法: サービスのバージョン管理」を参照してください。
付録
厳密なバージョン管理が必要な場合、データ コントラクトのバージョン管理の一般的なガイダンスは、データ コントラクトを変更不可として扱い、変更が必要な場合は新しいデータ コントラクトを作成することです。 新しいデータ コントラクトごとに新しいクラスを作成する必要があります。したがって、古いデータ コントラクト クラスの観点で作成された既存のコードを取得し、新しいデータ コントラクト クラスの観点でコードを書き換える必要がないようにするための方法が必要となります。
このような方法の 1 つとして、インターフェイスを使用して各データ コントラクトのメンバーを定義し、インターフェイスを実装するデータ コントラクト クラスではなく、インターフェイスの観点で内部実装コードを作成します。 サービスのバージョン 1 の次のコードは、IPurchaseOrderV1
インターフェイスと PurchaseOrderV1
を示しています。
public interface IPurchaseOrderV1
{
string OrderId { get; set; }
string CustomerId { get; set; }
}
[DataContract(
Name = "PurchaseOrder",
Namespace = "http://examples.microsoft.com/WCF/2005/10/PurchaseOrder")]
public class PurchaseOrderV1 : IPurchaseOrderV1
{
[DataMember(...)]
public string OrderId {...}
[DataMember(...)]
public string CustomerId {...}
}
サービス コントラクトの操作は PurchaseOrderV1
の観点で作成されますが、実際のビジネス ロジックは IPurchaseOrderV1
の観点で作成されます。 次のコードに示すように、バージョン 2 には、新しい IPurchaseOrderV2
インターフェイスと PurchaseOrderV2
クラスが含まれています。
public interface IPurchaseOrderV2
{
DateTime OrderDate { get; set; }
}
[DataContract(
Name = "PurchaseOrder",
Namespace = "http://examples.microsoft.com/WCF/2006/02/PurchaseOrder")]
public class PurchaseOrderV2 : IPurchaseOrderV1, IPurchaseOrderV2
{
[DataMember(...)]
public string OrderId {...}
[DataMember(...)]
public string CustomerId {...}
[DataMember(...)]
public DateTime OrderDate { ... }
}
PurchaseOrderV2
の観点で作成された新しい操作を含めるために、サービス コントラクトが更新されます。 IPurchaseOrderV1
の観点で作成された既存のビジネス ロジックは、PurchaseOrderV2
で引き続き機能し、OrderDate
プロパティを必要とする新しいビジネス ロジックが IPurchaseOrderV2
の観点で作成されます。