7 月号 2008 7 月号 2008
データ サービス: SQL Server Data Services で堅牢かつスケーラブルなアプリケーションを開発する
標準的な Web サービス インターフェイスを通して機能を公開する SQL Server Data Services について紹介します。 David Robinson
ADO.NET: Entity Framework で柔軟なデータ モデリングを実現する
この記事で著者は、Entity Framework に関する疑問に答え、それがなぜどのようにして開発されたのかを理解できるようにしています。 Elisa Flasko
データと WPF: データ バインドと WPF でデータの表示をカスタマイズする
この記事では、Windows Presentation Foundation でのプログラムおよび宣言によるデータ バインドと表示の手法を説明します。 Josh Smith
トランザクション: データを失うことなく障害に対処するスケーラブルなシステムを構築する
データを失うことなく障害に対処するシステムの構築は、簡単ではありません。拡張性と堅牢性を兼ね備えたシステムを構築する方法について説明します。 Udi Dahan
WCF P2P: ピア ネットワークでの状態共有の設計方法
サーバーレス ピア ネットワークでビジネス アプリケーション間の状態共有を実現することによって、ビジネス アプリケーションをピア対応にする方法について説明します。 Kevin Hoffman
Columns
編集者のメモ: データを変幻自在にする: 編集長 Howard Dierking の代理を務める Ted Neward が、データの収集と操作の状況について考察します。Ted Neward | ツールボックス: 簡単な Wiki のホスティング、Scott Hanselman のブログ、および画面のキャプチャ: 今月のツールボックスでは、Wiki を簡単にホストできるソフトウェア、必要なスクリーン ショットを撮って処理するためのソフトウェア、および LINQ 関連の書籍についてご紹介します。Scott Mitchell | CLR 徹底解剖: System.AddIn を使用して Windows フォーム アプリケーションを拡張する: 今月は、新しい .NET アドイン フレームワーク (System.AddIn) を使用するために Windows フォーム アプリケーションを調整する方法を説明します。Mueez Siddiqui
データ ポイント: 階層型アーキテクチャの Entity Framework: この記事では、WCF、WPF、および MVP パターンを使用して n 層アーキテクチャに適した Entity Framework ソリューションを構築します。John Papa | Advanced Basics: LINQ Enumerable クラス (第 1 部): この記事では LINQ Enumerable クラスを紹介します。このクラスを使用すると、IEnumerable(Of T) インターフェイスを実装する任意のクラスのデータを操作できます。Ken Getz | Office Space: 自動化された SharePoint サイトのブランド化: この記事では、マスタ ページと CSS ファイルをサイト コレクションのレベルで統合する、SharePoint サイト向けのカスタム ブランド化ソリューションについて紹介します。Ted Pattison
Cutting Edge: HTML Message パターン: 今回は、BST パターンの拡張された実装について説明し、それを HTM ソリューションと比較します。Dino Esposito | テストの実行: WCF テストの概要: 今月号で、James McCaffrey は、WCF アプリケーションの性能を試すためのテスト ハーネスを構築します。Dr. James McCaffrey | セキュリティに関するブリーフィング: 脅威のモデル化プロセスをよみがえらせる: このコラムでは、脅威のモデル化を行う際にあらゆる規模の開発チームで採用できるアプローチについて説明します。Adam Shostack
基礎: WCF にコード アクセス セキュリティを追加する (第 2 部): 今月のコラムでは引き続き、WCF と部分的に信頼されたサービスにおけるコード アクセス セキュリティについて説明します。Juval Lowy | .NET の問題: WebClient の非同期 I/O: 今月の .NET の問題では、コラムニストの Stephen Toub が非同期 I/O に関する読者からの質問に答えます。Stephen Toub | { End Bracket }: Windows Home Server をカスタマイズする: Chris Gray が、Windows Home Server 用に開発したカスタム アプリケーションについて説明し、エネルギーを節約するためのアプリケーションを検証します。Chris Gray


データ サービス
SQL Server Data Services で堅牢かつスケーラブルなアプリケーションを開発する
David Robinson

この記事では、次の内容について説明します。
  • SSDS データ モデル
  • エンティティ、コンテナ、オーソリティを管理する
  • サンプル Web アプリケーションを作成する
  • クラスのシリアル化と逆シリアル化
 この記事では、次のテクノロジを使用しています。
SQL Server
このコラムは、SQL Server Data Services のプレリリース版に基づいて書かれています。ここに記載されているすべての情報は、変更される場合があります。
目次
SSDS データ モデル
広告システムを作成する
都市を追加する
カテゴリを追加する
エンティティを更新および削除する
リスト スキーマを追加および削除する
広告 Web アプリケーション
クラスの逆シリアル化
カスタム リスト スキーマを使用する
今後の展望
Microsoft データ プラットフォームは、構造化されていないバイナリ データから、それと対極にある高度に構造化されたオンライン分析処理 (OLAP) キューブまで、あらゆる種類のデータに対応する充実した製品とテクノロジで構成されています。これらのテクノロジはどのようなアプリケーション シナリオにも対応できますが、通常は、ソリューションのコーディングを始められるようになる前に、時間とハードウェア ソリューションの先行投資が伴います。
アプリケーションのライフサイクル全体を通して必要になるストレージと処理の要件を見積もることは、多くの場合、アプリケーション開発者にとって容易ではありません。特に、設立したばかりの企業の場合は、資金が限られ、ハードウェア投資を決定する基準になるデータが乏しいため、非常に困難なことがあります。このような先行投資に関する不安に対処するため、マイクロソフトは最近、既に堅牢なデータ プラットフォームに、SQL Server® Data Services (SSDS) という新しいテクノロジを追加しました。
SSDS は、従来の箱売りされているソフトウェアとしての製品ではなく、実績のある SQL Server テクノロジを内部的に使用し、業界標準の Web サービス インターフェイスを通して機能を公開する、堅牢なスケールフリーのデータ サービスです。SSDS は、オープンな業界標準のプロトコルを使用してアクセスできる、使いやすく柔軟なデータ モデルを提供します。
Microsoft® .NET Framework、Java、それ以外のテクノロジのいずれを使用して開発する場合でも、SSDS をデータ ストアとして使用できます。さらに、SSDS は、開発者が短時間でアカウントのプロビジョニングを行い、そのアカウントに対する開発をすぐに始められるように作られています。
SSDS は、使用可能なドライブ ベイやラック スペースによって制約されないデータ バック エンドを開発者に提供します。SSDS をデータ プラットフォームとして使用すると、アプリケーションは必要な量のデータを自由に使用できます。ストレージがギガバイトであってもペタバイトであっても、利用したリソース分だけのコストしか発生しません。
多くの Web サイトやアプリケーションのアクセス パターンは周期的であり、負荷のピークがたとえ短時間であっても、ピーク時の負荷に耐えられるだけの十分な処理能力が必要です。データ バック エンドとして SSDS を使用すると、アプリケーションに集中でき、データ プラットフォームの容量計画に気を遣う必要はありません。
この記事では、SSDS を中心とするデータ ソリューションの開発の基礎について紹介します。最初に、SSDS が採用しているデータ モデルについて説明します。次に、筆者が SSDS を使用して構築した簡単なオンライン広告システムをお見せしながら、開発の詳細について説明します (図 1 を参照)。
図 1 SSDS によってホストされる広告アプリケーションのサンプル (クリックすると拡大画像が表示されます)

SSDS データ モデル
SSDS は、柔軟なエンティティ ベースのデータ モデルを備えています。このモデルを構成するのは、オーソリティ、コンテナ、およびエンティティという 3 つの主要な要素であり、それらの要素は ACE 概念と呼ばれます (図 2 を参照)。SSDS のオーソリティは、リレーショナル世界のデータベースと関連付けることができます。オーソリティをプロビジョニングすると、オーソリティにアクセスするための DNS 名が SSDS によって作成されます。たとえば、ssdsdemo という名前のオーソリティをプロビジョニングした場合は、次の URI でアクセスします。
ssdsdemo.data.beta.mssds.com
図 2 SSDS の基本コンポーネント
オーソリティは、地理的な場所の単位でもあります。つまり、作成された DNS 名は、データがホストされる特定の Microsoft データセンターにマップされます。2 つのオーソリティ americasdemo.data.beta.mssds.com と europedemo.data.beta.mssds.com を作成した場合は、それぞれがユーザーに最も近い場所にある異なるデータセンターと関連付けられます。
オーソリティには、コンテナのコレクションが含まれます。コンテナは、リレーショナル データベースのテーブルと似ています。重要な違いは、データベースのテーブルにはスキーマを関連付けて、テーブル内のすべての行を同種にすることです。SSDS のコンテナにスキーマは必要なく、異種のエンティティをまとめて 1 か所に格納できるので便利です。コンテナは単なるエンティティのコレクションです。現在のリリースの SSDS では、すべてのクエリのスコープは単一のコンテナに設定されています。
もう 1 つ重要なこととして、各コンテナは SSDS クラスタ内の異なるノードに配置されます。SQL Server では、パフォーマンスを向上させるために、さまざまなテーブルを別々のスピンドルに配置し、読み取り/書き込み能力を最大化します。これは SSDS でも同様ですが、各コンテナを別々のマシンに配置する点が異なります。データを複数のコンテナに分割し、要求をマルチスレッド化すると、読み取りと書き込みが 1 つのコンピュータに制限されないので、パフォーマンスを向上できます。
コンテナに関する最後の注意点として、各コンテナが SSDS クラスタの別々のノードに配置されるだけでなく、障害復旧の目的でデータも他の複数のノードにレプリケートされます。コンテナが配置されたコンピュータで障害が発生した場合は、バックアップ レプリカの 1 つが自動的に昇格されるので、アプリケーションでデータやパフォーマンスの損失が発生することはありません。
エンティティは、リレーショナル データベースにおけるテーブルの行と対比できます。エンティティは単に、名前と値のペアからなるプロパティを収集したものです。名前/値ペアは、区別されたシステム プロパティと柔軟なプロパティという 2 つのカテゴリに分かれます。
区別されたシステム プロパティはすべてのエンティティに共通で、ID、Kind、Version などがあります。ID はエンティティを一意に識別します。ID は、対象のエンティティが存在するコンテナの内部では一意である必要がありますが、コンテナが違えば、異なるエンティティが同じ ID を持っていてもかまいません。Kind は、同種のエンティティを分類するために使用します。エンティティにスキーマはアタッチされないので、Kind が同じエンティティであっても同じ構造であるとは限りません。Version は、エンティティの現在のバージョンを示すために使用されます。この値は、操作のたびに更新されます。
柔軟なプロパティには、開発者がアプリケーション データを格納します。柔軟なプロパティは、string、decimal、bool、datetime、binary という単純な型をサポートします。柔軟なプロパティのそれぞれに先頭 256 バイトまでインデックスを作成できます。

広告システムを作成する
SSDS の機能を実演し、開発作業の概要を示すために、サンプルのオンライン広告システムである Contoso Classifieds の実装の流れを見ていきます。サンプルを構成するアプリケーションは、システムを管理するための Windows® フォーム アプリケーションと、ユーザーが訪れるメインの Contoso Classifieds サイトである ASP.NET アプリケーションの 2 つです。アプリケーションが SSDS にアクセスするには、Windows フォーム アプリケーションでは SOAP インターフェイスを使用し、ASP.NET アプリケーションでは Representational State Transfer (REST) インターフェイスを使用します。
SSDS アカウントにサインアップすると、ユーザー名とパスワードを受け取ります。そこから先は、必要に応じてオーソリティ、コンテナ、エンティティの作成を開始できます。Contoso Classifieds の場合は、contosoclassifieds というオーソリティを設定しました。SSDS のベータ リリースでのサービス URI エンドポイントは data.data.beta.mssds.com です。
contosoclassifieds オーソリティには、Categories と Cities という 2 つのコンテナが含まれます。Categories は、リスト カテゴリに関係のあるエンティティが含まれます。Cities には、システムで定義されている各都市のエンティティが含まれます。これらのエンティティは、都市固有のオーソリティを指すポインタにすぎません。都市固有の各オーソリティには、リスト カテゴリごとのコンテナがあります (図 3 を参照)。
図 3 Contoso Classifieds の要素 (クリックすると拡大画像が表示されます)
このような設計にしたのにはいくつかの理由があります。オーソリティを都市ごとに実装したのは、サービス提供の対象者に地理的に近いデータセンターでオーソリティをプロビジョニングできるようにするためです。コンテナをリスト カテゴリごとにしたのは、クエリ パターンを簡単にし、負荷をクラスタ内の複数のマシンに分散できるようにするためです (コンテナのスコープはバックエンド クラスタの特定のノードに設定されることを思い出してください)。図 4 は、Contoso Classifieds 管理クライアントのユーザー インターフェイスです。
図 4 Classifieds アプリケーションの管理クライアント (クリックすると拡大画像が表示されます)
既に説明したように、アプリケーションで使用するオーソリティ、コンテナ、およびエンティティは、開発者が作成します。サンプル アプリケーションでは、この作業はすべて、[Perform Initial Setup] ボタンのクリック イベントに含まれています (エラー処理を含む完全なコードは、この記事のコードのダウンロードに含まれています)。
SSDS のオーソリティのプロビジョニングは非常に簡単です。次のコード例では SitkaSoapServiceClient を使用しており、これには既に SSDS SOAP インターフェイスにサービス参照を追加してあります。
using (SSDSClient.SitkaSoapServiceClient ssdsProxy =
  new SSDSClient.SitkaSoapServiceClient())
オーソリティについては既に説明しましたが、SSDS のスコープとはどのようなものでしょうか。SSDS でのスコープ オブジェクトは、REST サービスで URI が使用されるのと同じように、SOAP サービスにおいてオブジェクトをアドレス指定する手段を提供します。
まず最初にサービスのユーザー名とパスワードを設定します。
ssdsProxy.ClientCredentials.UserName.UserName =
  txtUserName.Text;
ssdsProxy.ClientCredentials.UserName.Password =
  txtPassword.Text;
ACE モデルの最上位レベルであるオーソリティを作成する必要があるので、最初に空のスコープを作成します。
SSDSClient.Scope serviceScope = new SSDSClient.Scope();
次に、Contoso Classifieds オーソリティを作成します。このオーソリティは、システムの一般的な構成情報を保持します。
SSDSClient.Authority contosoAuth =
  new SSDSClient.Authority();
contosoAuth.Id = "contosoclassifieds";
それから、作成したスコープとオーソリティを SSDS に送信します。
ssdsProxy.Create(serviceScope, contosoAuth);
メインの Contoso Classifieds オーソリティを作成したので、そのオーソリティを指すようにスコープを設定し、広告を提供する都市を保持するためのコンテナを作成して、作成したコンテナを SSDS に送信します。
serviceScope.AuthorityId = contosoAuth.Id;

SSDSClient.Container citiesContainer = new SSDSClient.Container();
citiesContainer.Id = "Cities";

ssdsProxy.Create(serviceScope, citiesContainer);
コンテナを作成するプロセスは、オーソリティの作成プロセスと似ています。違いは、Authority オブジェクトの代わりに Container オブジェクトを作成し、空のスコープを作成する代わりに、コンテナを作成するオーソリティを指すようにスコープを更新することだけです。
アプリケーションでサポートするすべてのヘッダー カテゴリとリスト カテゴリを保持するコンテナを作成します。
SSDSClient.Container categoriesContainer =
  new SSDSClient.Container();
categoriesContainer.Id = "Categories";

ssdsProxy.Create(serviceScope, categoriesContainer);
オーソリティを作成したら、Web ブラウザで HTTP または HTTPS を指定することで、REST インターフェイスを使用してコンテナの内容を表示できます。オーソリティを設定するときに SSDS が作成した新しい DNS 名を使用するだけです。この場合、URL は次のようになります。
http://contosoclassifieds.data.data.beta.mssds.com/v1
この URL をブラウザに入力すると、認証を求められます。認証が成功すると、ブラウザに次のような内容が表示されます。
<s:Authority xmlns:s="http://schemas.microsoft.com/sitka/2008/03/"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:x="http://www.w3.org/2001/XMLSchema">
  <s:Id>contosoclassifieds</s:Id>
  <s:Version>11</s:Id>
</s:Authority>
オーソリティ名には小文字が使用されます。SSDS がオーソリティの DNS エントリを作成するので、DNS の命名規則に従う必要があるからです。
コンテナを作成した後、ブラウザの表示を更新して、URL に ?q="" の形式で空のクエリを追加すると、EntitySet で 1 つの Container オブジェクトが返されることがわかります。EntitySet は単に、クエリに対する応答として返されるエンティティのコレクションです。初期セットアップが完了すると、サンプル アプリケーションには 1 つのオーソリティ (contosoclassifieds) と 2 つのコンテナ (Categories と Cities) が作成されています。
<s:EntitySet xmlns:s="http://schemas.microsoft.com/sitka/2008/03/"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:x="http://www.w3.org/2001/XMLSchema">
  <s:Container>
    <s:Id>Categories</s:Id>
    <s:Version>1</s:Id>
  </s:Container>
  <s:Container>
    <s:Id>Cities</s:Id>
    <s:Version>1</s:Id>
  </s:Container>
</s:EntitySet>
これで初期セットアップは完了です。先に進み、サービス対象都市の機能を追加します。

都市を追加する
既に説明したように、Contoso Classifieds は各都市の一覧を独自のオーソリティに格納します。これにより、その都市をホストするデータセンターを選択できます (現時点の SSDS のベータではこの機能はサポートされていませんが、製品がリリースされるまでにはサポートされるようになります)。
都市を追加するためのコードは、初期セットアップで使用したコードと似ています。SOAP プロキシを使用して資格情報の設定、空のスコープの作成、およびオーソリティ ID の設定を行い、Create を発行します。
SSDSClient.Authority cityAuth =
  new SSDSClient.Authority();
cityAuth.Id = cityAuthorityName;

ssdsProxy.Create(serviceScope, cityAuth);
新しい都市オーソリティを作成したので、メインの contosoclassifieds オーソリティ内の Cities コンテナにポインタ エンティティを追加します。
serviceScope.AuthorityId = "contosoclassifieds";
serviceScope.ContainerId = "Cities";

//Create the City Entity, and set its properties appropriately
SSDSClient.Entity cityEntity = new SSDSClient.Entity();
cityEntity.Id = cityAuthorityName;
cityEntity.Kind = "CityServed";
cityEntity.Properties = new Dictionary<string, object>();
cityEntity.Properties["AuthorityUri"] =
  string.Format("{0}.data.data.beta.mssds.com/v1/", cityAuthorityName);
cityEntity.Properties["Name"] = txtCity.Text;

//Issue the create to SSDS
ssdsProxy.Create(serviceScope, cityEntity);
ここまで説明した内容をまとめてみましょう。システム全体のすべてのデータを保持するメインの contosoclassifieds オーソリティを作成しました。そのオーソリティ内に Cities コンテナがあり、そのコンテナ内には、アプリケーションがサービスを提供する各都市のエンティティが含まれます。そのエンティティには、都市の表示名と共に、すべての一覧を含むオーソリティへのポインタが含まれます。

カテゴリを追加する
ここまででオーソリティ、コンテナ、およびエンティティの追加が完了しました。次の手順はリスト カテゴリ機能の実装です。これを行うには、SSDS のクエリ、更新、および削除の機能を使用します。
Contoso Classifieds は、カテゴリ ヘッダーをサポートします。カテゴリ ヘッダーの下にはサブ カテゴリがあり、ユーザーからの実際の転記の内容を保持します。このデータにリレーショナル モデルを使用する場合、通常はデータのヘッダー/行パターンを使用します。しかし、SSDS が使用する柔軟なエンティティのデータ モデルでは、データの形態を自由に設定できます。Contoso Classifieds アプリケーションの場合は、単一のエンティティを使用して、各カテゴリ ヘッダーとそれに属するすべてのサブカテゴリを表すことにします (図 5 を参照)。
図 5 アプリケーションが使用するエンティティ データ モデル (クリックすると拡大画像が表示されます)
断っておきますが、このパターンを使用するのは、あくまでもエンティティの柔軟な性質を示すためであり、複数のエンティティが同じ Kind を持つことがあってもエンティティを同種にする必要がないことを説明するためです。エンティティの形態は自由に設定できます。
このシナリオに対するさらに優れたアプローチは、Category および Listing Category という 2 つの異なるエンティティの種類を作成し、すべてのエンティティに柔軟なプロパティ CategoryID を含めるというものです。このようにすると、次のようなクエリを発行できます。
from e in entities where e["CategoryID"] ==    "For Sale" select e
柔軟なプロパティに注意してください。既に説明したように、コンテナのエンティティは、区別されたシステム プロパティ ID の使用を制限する一意の ID を持つ必要がありました。また、発行しているクエリの構文にも注意してください。SSDS では LINQ に似たクエリ構文を使用します。その構文は、.NET Framework を使用しているほとんどの開発者にはなじみやすいものです。
図 4 の管理アプリケーションを見返すと、リスト カテゴリを表すツリー ビュー、およびカテゴリ ヘッダーとリスト カテゴリを追加するためのテキスト ボックスに気付くでしょう。カテゴリ ヘッダーを追加するときの内部の実装について説明します。
前のコードと同様に、SitkaSOAPServiceClient を使用して資格情報を設定し、contosoclassifieds オーソリティと Categories コンテナにスコープを設定します。次に行う必要があるのは、エンティティの作成だけです。
SSDSClient.Scope serviceScope = new SSDSClient.Scope();

serviceScope.AuthorityId = "contosoclassifieds";
serviceScope.ContainerId = "Categories";

SSDSClient.Entity categoryHeaderEntity = new SSDSClient.Entity();
categoryHeaderEntity.Id = CategoryID;
categoryHeaderEntity.Kind = "Category";
カテゴリ名を格納する柔軟なプロパティを 1 つ追加することもできます。これにより柔軟なプロパティのコレクションには 1 つのプロパティが追加されるだけであることに注意してください。
categoryHeaderEntity.Properties =
  new Dictionary<string, object>();
categoryHeaderEntity.Properties["CategoryName"] = CategoryName;
次の手順はリスト カテゴリの追加です。Contoso Classifieds は単一のエンティティを使用してカテゴリ ヘッダー内のリスト カテゴリのコレクション全体を表すので、カテゴリ エンティティを取得し、新しいリスト カテゴリを追加し (柔軟なプロパティとして追加されます)、更新を SSDS に送信する必要があります。
カテゴリのエンティティを取得するには、エンティティを直接指すようにスコープを設定して Get メソッドを呼び出します。このようにすると、クエリを発行しないで直接エンティティを取得できます。
SSDSClient.Scope serviceScope = new SSDSClient.Scope();
serviceScope.AuthorityId = "contosoclassifieds";
serviceScope.ContainerId = "Categories";
serviceScope.EntityId = CategoryID;
//Retrieve the Category Entity
SSDSClient.Entity categoryEntity = ssdsProxy.Get(serviceScope);
次に新しいプロパティのインデックスを特定し、新しい柔軟なプロパティとして追加したら、Update を発行します (図 6 を参照)。
図 6 柔軟なプロパティの追加
//Determine whether this is the first listing
//category being added to this header
if (categoryEntity.Properties.Count > 2) {
  propCount = ((categoryEntity.Properties.Count - 1) / 2);
  propCount++;
}
else {
  propCount = 1;
}

//Create the FlexProperties for the new listing category
string listingIdPropName = string.Format  ("ListingCategoryID{0}", propCount);
string listingNamePropName = string.Format  ("ListingCategoryName{0}", propCount);

categoryEntity.Properties[listingIdPropName] = ListingCategoryID;
categoryEntity.Properties[listingNamePropName] = ListingCategoryName;

//Issue the update to SSDS
ssdsProxy.Update(serviceScope, categoryEntity);

エンティティを更新および削除する
リスト カテゴリの更新は、新しい柔軟なプロパティを追加するだけです。更新のたびに、エンティティを取得し、ローカルで更新を行って更新を送信し、更新したエンティティをスコープと共に Update メソッドに渡します。
まず、サービス スコープをメインの contosoclassifieds オーソリティに設定します。また、Categories コンテナおよび更新するカテゴリのエンティティを指すように設定します。
SSDSClient.Scope serviceScope = new SSDSClient.Scope();
serviceScope.AuthorityId = "contosoclassifieds";
serviceScope.ContainerId = "Categories";
serviceScope.EntityId = currentHdrNode.Name;
更新するエンティティを取得します。
SSDSClient.Entity categoryEntity = ssdsProxy.Get(serviceScope);
これで、柔軟なプロパティのコレクションを再初期化し、リスト カテゴリに対するすべての柔軟なプロパティを再び追加できます。または、ループ処理で柔軟なプロパティを個別に更新することもできますが、このエンティティの構造は単純なので、図 7 で示すようにエンティティを再作成する方が簡単です。
図 7 プロパティの更新
categoryEntity.Properties = new Dictionary<string, object>();
categoryEntity.Properties["CategoryName"] = currentHdrNode.Text;

int propCount = 1;

if (e.Node.Parent != null) {
  //Loop through each node and add its category ID and
  //category name flex property.
  foreach (TreeNode node in e.Node.Parent.Nodes) {
    string listingIdPropName = string.Format      ("ListingCategoryID{0}", propCount);
    string listingNamePropName = string.Format      ("ListingCategoryName{0}", propCount);

    categoryEntity.Properties[listingIdPropName] = node.Name;
    categoryEntity.Properties[listingNamePropName] = node.Text;

    //if we are re-adding the updated category, the tree view
    //will still have its old value, so set the flex property
    //to the updated value
    if (node.Name == e.Node.Name) {
      categoryEntity.Properties[listingIdPropName] =         m_OldSelectNode.Name;
      categoryEntity.Properties[listingNamePropName] = e.Label;
    }

    propCount++;
  }
}

//Submit the update to SSDS
ssdsProxy.Update(serviceScope, categoryEntity);
エンティティまたはコンテナを削除するには、ServiceScope が削除するアイテムを指すように設定し、SitkaSoapServiceClient の Delete メソッドを呼び出します。Header の場合は、エンティティ全体を削除します。
if (m_OldSelectNode.Parent == null) {
  ssdsProxy.Delete(serviceScope);
}
他のエンティティの場合は、更新で行ったのと同様に、柔軟なプロパティのコレクションを再作成して再構築し、削除したノード以外のすべてのノードの追加を実行します。
SSDSClient.Entity categoryEntity = ssdsProxy.Get(serviceScope);
categoryEntity.Properties = new Dictionary<string, object>();
categoryEntity.Properties["CategoryName"] = CategoryName;

int propCount = 1;

foreach (TreeNode node in entityNode.Nodes) {
  string listingIdPropName = string.Format    ("ListingCategoryID{0}", propCount);
  string listingNamePropName = string.Format    ("ListingCategoryName{0}", propCount);

  if (node.Text != m_OldSelectNode.Text) {
    categoryEntity.Properties[listingIdPropName] = node.Name;
    categoryEntity.Properties[listingNamePropName] = node.Text;

    propCount++;
  }
}
最後に、更新を SSDS に発行し、ツリー ビューからノードを削除します。
ssdsProxy.Update(serviceScope, categoryEntity);

tvCategories.Nodes.Remove(m_OldSelectNode);

リスト スキーマを追加および削除する
SSDS の現在のベータは、スキーマをサポートしていません。将来的にはスキーマのサポートを追加する計画がありますが、SSDS で独自にスキーマを追加するのは簡単です。ただ 1 つ注意が必要なのは、現時点では SSDS がスキーマをサポートしないので、アプリケーションでスキーマを管理する必要があることです。
Contoso Classifieds の場合は、特定のリスト カテゴリに対して管理者がカスタム スキーマを定義しておくのは有効だと言えます。管理クライアントの内部でスキーマを定義し、後で Web サイトを実装するときにそれを使用できます。
これまでに説明した手順では、同じコンテナの中に異種のエンティティは含まれていませんでした。各エンティティには Kind プロパティがあるので、これを使用して異なる種類のエンティティを同じコンテナに格納でき、特定の種類のエンティティ (つまりこの場合は Kind) を照会することもできます。Kind に関連付けられたスキーマ コントラクトはありませんが、このメカニズムを使用して同種のデータを簡単にグループ化できます。ここでは、Kind の新しいエンティティである ListingSchema を Categories コンテナに追加します。
リスト スキーマを追加するためのダイアログは非常に簡単です (図 8 を参照)。このダイアログで管理者はリストに関連付ける新しいフィールドを定義し、特定のフィールドを必須として指定できます。詳細については、後で実際の Web サイトの実装方法を示すときに説明します。
図 8 リストのスキーマのカスタマイズ (クリックすると拡大画像が表示されます)
ListingSchema エンティティを追加するプロセスは既に説明したプロセスと同じですが、今度は異なるエンティティ (Kind) を使用します。ListingSchema を追加するコードを 図 9 に示します。For Sale Cars カテゴリのリスト スキーマを追加した後、Categories コンテナに異なる 2 種類のエンティティが含まれていることを確認します。まったく異なるデータが同じコンテナに格納されています。
図 9 スキーマの追加
listingSchemaEntity.Kind = "ListingSchema";
listingSchemaEntity.Properties = new Dictionary<string, object>();
listingSchemaEntity.Properties["ListingID"] = m_ListingID;
listingSchemaEntity.Properties["ListingName"] = m_ListingName;

int propCount = 0;
bool required = false;

foreach (DataGridViewRow row in dataGridView1.Rows) {
  if (row.IsNewRow) continue;
  propCount++;

  listingSchemaEntity.Properties[string.Format    ("Property{0}Name",propCount)] =
    row.Cells["colFieldName"].Value.ToString();
  listingSchemaEntity.Properties[string.Format    ("Property{0}DataType", propCount)] =
    row.Cells["colDataType"].Value.ToString();

  if (row.Cells["colRequired"].Value == null) {
    required = false;
  }
  else {
    required = Convert.ToBoolean(row.Cells      ["colRequired"].Value.ToString());
  }

  listingSchemaEntity.Properties[string.Format    ("Property{0}Required", propCount)] = required;
}

if (propCount > 0) {
  using (SSDSClient.SitkaSoapServiceClient ssdsProxy =     new SSDSClient.SitkaSoapServiceClient()) {
    //Set username and password for the service
    ...

    //Set service scope to the main contoso classifieds
    //authority. Point it to the categories container
    ...

    //If this entity doesn't exist, create it
    if (listingSchemaID == "" || listingSchemaID == null) {
      ssdsProxy.Create(serviceScope, listingSchemaEntity);
    }
    //Otherwise update it
    else {
      serviceScope.EntityId = listingSchemaID;
      ssdsProxy.Update(serviceScope, listingSchemaEntity);
    }
  }
  MessageBox.Show("Custom Schema Saved", "Custom Schema Saved",
    MessageBoxButtons.OK, MessageBoxIcon.Information);
  this.Hide();
}
管理クライアントの最後の部分では、リスト カテゴリが削除されたときにカスタム リスト スキーマを削除します。エンティティとコンテナの削除については既に説明しましたが、そのときは削除するエンティティまたはコンテナの ID がわかっていました。スキーマを削除するには、最初に LINQ クエリを発行して、リスト カテゴリに関連付けられているすべてのリスト スキーマを取得し、それらを削除します。クエリは次のようになります。
string.Format(@"from e in entities
  where e.Id == ""{0}"" select e", CategoryID);
クエリから ListingSchema エンティティの一覧が返されます。ここで必要なのは返されたエンティティを削除することだけです。DeleteListingSchema メソッドの全体を図 10 に示します。
図 10 DeleteListingSchema
//Retrieve the custom schema entity for this listing
string linqQuery =   string.Format(
  @"from e in entities where e.Id == ""{0}"" select e", CategoryID);

List<SSDSClient.Entity> entities =
  ssdsProxy.Query(serviceScope, linqQuery);

foreach (SSDSClient.Entity entity in entities) {

  //If more than 1 flex property on the entity, the
  //header has listing categories attached to it.
  if (entity.Properties.Count > 1) {
    //Calculate the number of Listing Categories
    int propCount = ((entity.Properties.Count - 1) / 2);
    for (int x = 1; x < propCount + 1; x++) {
      //Delete the listing schema for each listing
      //in this category
      DeleteListingSchema(entity.Properties["ListingCategoryID" +
        x.ToString()].ToString());
    }
  }
}
Contoso Classifieds 管理クライアントは以上で完成です。次に、SSDS の REST インターフェイスを使用した Web サイトの実装に進みます。

Classifieds Web アプリケーション
既に説明したように、SSDS には REST インターフェイスと SOAP インターフェイス (Contoso Classifieds 管理クライアントで使用しました) を使用してアクセスできます。どちらのインターフェイスを選択しても概念と機能は同じですが、REST では HTTP (または HTTPS) の呼び出しを直接行います。
最初に、図 1 で示した Web アプリケーションのメイン インターフェイスを見直してください。リスト カテゴリを表すツリー ビュー、メインのコンテンツ領域、アプリケーションのサポート対象として現在構成されている全都市のデータリストがあります。それぞれについて、SSDS に対する ASP.NET アプリケーションを簡単に開発できることを説明します。
ツリー ビューの ASPX コードを見ると、それが非常に単純であることがわかります。
<h3>Groups</h3>
<asp:TreeView ID="TreeView1" runat="server"
  onselectednodechanged="TreeView1_SelectedNodeChanged">
</asp:TreeView>
処理は、図 11 で示す分離コードで行われています。まず最初に関係するのは URI の作成であり、URI は Categories コンテナへのポインタです。データは都市に固有ではないので、メインの contosoclassifieds オーソリティの Categories コンテナに格納することにしました。
図 11 ツリー ビューの作成
TreeView1.Nodes.Clear();

appDataUri = string.Format(@"http://{0}.{1}{2}",
  conSSDSAuthName, conSSDSUri, "Categories");
query = @"from e in entities where e.Kind == ""Category"" select e";

UriBuilder newUri = new UriBuilder(appDataUri);
newUri.Query = String.Format("q='{0}'", Uri.EscapeDataString(query));

string xmlResults =
  HTTPHelper.GetHTTPWebRequest(newUri.Uri.ToString(),
  new System.Net.NetworkCredential(conSSDSUsername, conSSDSPassword));

XmlDocument categoriesDoc = new XmlDocument();
categoriesDoc.LoadXml(xmlResults);
XmlNodeList nodeList = categoriesDoc.SelectNodes("//Category");

int nodeIndex = 0;

foreach (XmlNode node in nodeList) {
  if (node.ChildNodes.Count > 3) {
    int propCount = ((node.ChildNodes.Count - 1) / 2);

    TreeNode tn = new TreeNode(node.ChildNodes[2].InnerText,
      node.ChildNodes[0].InnerText);
    TreeView1.Nodes.Add(tn);

    for(int x=1;x<propCount;x++) {
      tn = new TreeNode(node.ChildNodes[(x*2)+2].InnerText,
        node.ChildNodes[(x*2)+1].InnerText);
      TreeView1.Nodes[nodeIndex].ChildNodes.Add(tn);
    }
  }
  else {
    TreeNode tn = new TreeNode(node.ChildNodes[2].InnerText,       node.ChildNodes[0].InnerText);
    TreeView1.Nodes.Add(tn);
  }

  nodeIndex++;
}
次に、LINQ クエリを使用して Kind カテゴリのすべてのエンティティを取得し、UriBuilder オブジェクトを使用して集約し、必要な HTTP エスケープを追加しています。それから、GetHTTPWebRequest メソッドを呼び出し、エスケープした URI および SSDS のユーザー名とパスワードを含む NetworkCredential オブジェクトを渡します。GetHTTPWebRequest は、クエリの述部を満たすすべてのエンティティを含む EntitySet の文字列表現を返します。
GetHTTPWebRequest は、HTTP の細部の一部を隠す静的ヘルパ メソッドです。
WebRequest request =
  HttpWebRequest.Create(Uri.EscapeUriString(serviceUri));
request.Credentials = requestCredential;
request.Method = "GET";
request.ContentType = XmlContentType;

// Get the response and read it in to a string.
using (HttpWebResponse response =
  (HttpWebResponse)request.GetResponse()) {

  return ReadResponse(response);
}
次に行う必要があるのは、新しい WebRequest オブジェクトの作成、渡された Credentials の設定、適切な HTTP 動詞の設定、ContentType の XML への設定、および GetResponse メソッドの呼び出しです。次に、ReadResponse を呼び出します。これはもう 1 つの静的ヘルパ メソッドであり、ストリーム リーダーを使用して HTTPResponse を読み取り、呼び出し元に返します。その後、分離コードは返された XML を取得して XmlDocument に読み込み、それを使用してツリー ビューを読み込みます。

クラスの逆シリアル化
これまでは、コードで単純に XML を手動で操作してきましたが、簡単にエンティティを取得してクラスに逆シリアル化できます。その場合、アプリケーションでは CityServed クラスを使用します。
Entity 基本クラスは、ごく基本的なクラスです。このクラスには、すべてのエンティティに共通である ID と Version の両方に使用するプロパティが含まれます。また、クラスをシリアル化可能にするために必要な属性も含まれます。
図 12 に示すように、CityServed は基本クラスの Entity を継承します。ジェネリック メソッドの Query は、CityServed 型のエンティティのリストを返します。
foreach (CityServed i in HTTPHelper.Query<CityServed>(
  appDataUri, query, new System.Net.NetworkCredential(
  conSSDSUsername, conSSDSPassword)))
図 12 CityServed
[XmlRoot(ElementName = "CityServed", Namespace = "")]

public class CityServed : Entity {
  [XmlElement(ElementName = "AuthorityUri")]
  public object AuthorityUriField;

  [XmlElement(ElementName = "Name")]
  public object NameField;

  public override string ToString() {
    return Name;
  }

  [XmlIgnore]
  public string AuthorityUri {
    get { return (string)AuthorityUriField; }
    set { AuthorityUriField = value; }
  }

  [XmlIgnore]
  public string Name {
    get { return (string)NameField; }
    set { NameField = value; }
  }
}
この記事のコード サンプルに含まれる Query メソッドは単なるジェネリックな静的ヘルパ メソッドであり、前に使用したのと同じ GetHTTPWebRequest メソッドを呼び出します。Serialize メソッドと Deserialize メソッドを図 13 に示します。Query メソッドと Serialize および Deserialize メソッドを組み合わせて、厳密に型指定された .NET オブジェクトを使用したり SSDS に保存したりできます。
図 13 エンティティのシリアル化と逆シリアル化
private static T Deserialize<T>(Stream stm, string xmlPayload) {
  XmlSerializer ser = new XmlSerializer(typeof(T));

  T flex = (T)ser.Deserialize(stm);

  XmlDocument xDom = new XmlDocument();
  xDom.LoadXml(xmlPayload);

  return flex;
}

public static T Deserialize<T>(String xmlPayload) {
  using (MemoryStream stm = new MemoryStream()) {
    Encoding encoding = new UTF8Encoding(false);
    stm.Write(encoding.GetBytes(xmlPayload), 0, encoding.GetByteCount(xmlPayload));
    stm.Position = 0;
    return Deserialize<T>(stm, xmlPayload);
  }
}

public static string Serialize<T>(T flex) {
  using (MemoryStream stm = new MemoryStream()) {
    Serialize(stm, flex);

    stm.Position = 0;

    using (StreamReader reader = new StreamReader(stm)) {
      return reader.ReadToEnd();
    }
  }
}
private static void Serialize<T>(Stream stm, T flex) {
  XmlSerializer ser = new XmlSerializer(typeof(T));
  Encoding encoding = new UTF8Encoding(false);
  XmlWriterSettings settings = new XmlWriterSettings();
  settings.CloseOutput = false;
  settings.ConformanceLevel = ConformanceLevel.Document;
  settings.Encoding = encoding;
  settings.Indent = true;
  settings.OmitXmlDeclaration = true;

  using (XmlWriter writer = XmlWriter.Create(stm, settings)) {
    ser.Serialize(writer, flex);
  }
}

カスタム リスト スキーマを使用する
既に説明したように、SSDS のエンティティは柔軟で、必要に応じて各エンティティの形態を自由に決めることができます。たとえば、Contoso Classifieds 管理クライアントでは、リスト カテゴリにスキーマを追加できました。また、SSDS からカスタム スキーマを取得し、システムへのリストの追加に使用できる入力フォームを動的に作成することもできます。
まず最初に、Field、Value、DataType、および Required という 4 つの列を含む DataTable を作成します。このテーブルは、管理者がリストに対して定義したフィールドを保持します。次に、クエリを発行し、リストに対してカスタム スキーマが定義されているかどうかを確認します。
string appDataUri = string.Format(@"http://{0}.{1}{2}",
  conSSDSAuthName, conSSDSUri, "Categories");
string query = string.Format(
  @"from e in entities where e[""ListingID""] == ""{0}"" select e",
  listingCategoryID);

UriBuilder newUri = new UriBuilder(appDataUri);
newUri.Query = String.Format("q='{0}'", Uri.EscapeDataString(query));

string xmlResults = HTTPHelper.GetHTTPWebRequest(newUri.Uri.ToString(),
  new System.Net.NetworkCredential(conSSDSUsername, conSSDSPassword));

XmlDocument categoriesDoc = new XmlDocument();
categoriesDoc.LoadXml(xmlResults);
XmlNodeList nodeList = categoriesDoc.SelectNodes("//ListingSchema");
返された XML を DataTable に読み込んで、DataList にバインドできます。
REST インターフェイスでは、エンティティの XML 表現を作成し、エンティティを挿入するコンテナに対して HTTP POST を発行する必要があります。エンティティを作成するのは簡単です。定型のエンティティを使用して各フィールドをループ処理し、XML ドキュメントにノードとして追加するだけです。
foreach (DataListItem fieldItem in dlAddFields.Items) {
  Label lblField = (Label)fieldItem.FindControl("lblAddField");
  TextBox tbItem = (TextBox)fieldItem.FindControl("txtAddValue");
  Label lblFieldType = (Label)fieldItem.FindControl("lblAddFieldType");
  entity = entity + String.Format(
    @" <{0} xsi:type='x:{1}'>{2}</{0}>", lblField.Text.Replace(" ", ""),
    lblFieldType.Text, tbItem.Text);
}
最後に、POST の対象となるコンテナを指す URI を作成して、POST を発行します。
string serviceUri = string.Format(@"http://{0}.{1}{2}",
  postingAuthority, conSSDSUri, postingContainer);

HTTPHelper.PostHTTPWebRequest(serviceUri, entity,
  new System.Net.NetworkCredential(
  conSSDSUsername, conSSDSPassword));

今後の展望
SSDS 用の開発は簡単です。繰り返しておきたいのは、SSDS のベースになっているのは実績のある SQL Server と Windows Server® のテクノロジであるということです。SSDS の最初のベータ リリースで公開された機能はごく限られたものですが、今後は多数追加されていきます。現時点で使用できる機能のサブセットでも、ユーザーが対応する必要がある多くのシナリオに役立ちます。
さらに、SQL Server スイートの他の製品やツールにも SSDS のサポートが追加される予定です。そのため、開発に使用する言語が C#、Visual Basic®、Java、Ruby、Microsoft Office Access® のいずれであっても、SSDS とそのオープン プロトコルのサポートは、アプリケーションのデータを格納するための理想的な場所になります。詳細については、SSDS のサイト (microsoft.com/sql/dataservices) にアクセスし、ベータ版にサインアップしてください。製品への追加を希望する機能がある場合は、david.robinson@microsoft.com までご連絡ください。

David Robinson は、マイクロソフトの SQL Server Data Services チームのシニア プログラム マネージャであり、新しい魅力的な機能を製品に組み込む作業に携わっています。また、コミュニティ イベントでプレゼンテーションを行い、SSDS についてユーザーからフィードバックを得ることも楽しんでいます。


ADO.NET
Entity Framework で柔軟なデータ モデリングを実現する
Elisa Flasko

この記事では、次の内容について説明します。
  • Entity Framework の背後にある考え方
  • Entity Data Model
  • クエリ、マッピング、および n 層開発
 この記事では、次のテクノロジを使用しています。
ADO.NET、LINQ、Entity Framework

コードのダウンロード : EntityFramework2008_07.exe (6,602 KB)
オンラインでのコードの参照

目次
新たなデータ モデルが必要な理由
EDM を XML で記述する理由
新しいクエリ言語はだれのために必要か
EDM を実装する
EntityClient
マッピング
Object Services
単なる ORM の一種か
ADO.NET Entity Framework を世に送り出す日が近づいてきました。2006 年に ADO.NET vNext として初めて紹介されたこのフレームワークは、間もなく Visual Studio® 2008 SP1 のリリースにより、晴れの舞台に登場します。マイクロソフトでは、何年にもわたって同様の製品の開発に挑みながら失敗を重ねた後、Visual Studio 2008 で部分的にオブジェクト リレーショナル マッピング (ORM) 空間に適応する 2 つのテクノロジをリリースしました。それが、LINQ to SQL と ADO.NET Entity Framework です。これらのテクノロジの市場での採用に伴い、開発者は現在、それがどのように開発され、そしてマイクロソフトが次にどこに進むのかを知りたがっています。また、これらのテクノロジの裏側に何があり、Entity Framework は市場の他の ORM テクノロジと何が違い、そしてマイクロソフトがこれらのテクノロジの将来に向けて何を投資しているのかにも関心を向けています。
Visual Studio 2008 の最初のリリース以降、LINQ to SQL に焦点を合わせた記事や、どのテクノロジを使用すればよいかについて述べた記事が、数多く書かれています (msdn.microsoft.com/data を参照)。ここでは、Entity Framework に焦点を当て、開発中にどのような選択がどのような理由で行われたのかを詳しく理解できるようにしています。
ADO.NET Entity Framework を実際に背後で支えているのは、Peter Chen 博士の Entity Relationship (ER) モデルに基づく Microsoft® Entity Data Model (EDM) です。また、Entity Framework を市場の他の ORM 型テクノロジから差別化する最も大きな要因となっているのも、この EDM です。EDM は ER モデルに基づいて構築され、エントリおよびリレーションシップの概念を保持しながら、モデルの抽象レベルを論理モデルよりも 1 段階高めています。

新たなデータ モデルが必要な理由
それでは、なぜ別のモデルが必要だったのでしょうか。企業で扱うデータの量が増えると共に、それらのデータを処理するアプリケーションを理解し、開発することは、非常に困難になってきました。データベース スキーマはストレージに関する問題 (データの整合性、パフォーマンス、管理など) を考慮しながら設計され、必ずしも理解しやすいものではありません。また、多くの場合、これらのスキーマはアプリケーションの構造と合っていないため、開発や保守がさらに複雑になっています。
構築するアプリケーションからデータの構造を分離するカスタム ソリューションは、一般的に使用されていました。しかし残念なことに、利用するカスタム ソリューション、さまざまなアプローチ、およびデータのモデリングに必要な手順が、すべてアプリケーションごとに異なっていたため、問題は大きくなる一方でした。業界全体にわたって、ストアの論理モデルから明確に分離できるアプリケーション レベルのドメイン モデルの定義方法、およびそのようなモデルに対する開発方法が、絶えず求められていました。そこで登場したのが、Entity Framework です。
EDM (図 1 のサンプルを参照) では、データを格納する方法に合わせるのではなく、組織がデータについて考え、それを使用する方法に合わせて、ドメイン モデルを定義できます。また、EDM は、マイクロソフト内の一連の開発者テクノロジおよびサーバー テクノロジに共通のコア データ モデルとすることを第一の目標として開発されました。
図 1 ブログ データベースに対する Entity Data Model のサンプル (クリックすると拡大画像が表示されます)
1 つのコア データ モデルを使用することで、アプリケーションの保守が単純になります。この目標が実現されれば、EDM は、ADO.NET Entity Framework で構築されたカスタム アプリケーションに対してだけでなく、レポートおよび視覚化アプリケーション、イントラネット ポータル アプリケーション、またはワークフロー アプリケーションへの入力としても、モデルを定義できるようになります。
ER モデルと同様に、EDM では 2 つの主要な概念を使用します。1 つはエンティティ (もの) であり、もう 1 つはそれらのエンティティ間のリレーションシップ (関連付け) です。エンティティおよび関連付けのインスタンスの格納や、それらのインスタンスに対する集合演算のクロージャを考える場合には、集合の概念も必要となります。したがって、最終的な形として、エンティティは EntitySets 内に含まれ、関連付けは AssociationSets 内に含まれます。
EDM で定義される最後の構造的概念は、EntityContainer の概念です。これは、前述のインスタンスおよびリレーションシップの集合に対してクロージャを定義する概念です。これらの単純な概念を併せて使用することで、開発者は、永続化層に対して、およびアプリケーション自体で使用されるクラスに対してマッピングできる、ドメイン モデルを定義できます。(現在はそうなっていますが、EDM の永続化層は必ずしもリレーショナルでなくてもよいことに注意してください。)
EDM で定義される各エンティティ型には、2 つの異なる種類のメンバを含めることができます。エンティティを定義するプロパティ (データベースの列に相当) と、エンティティ型が含まれるリレーションシップのナビゲーションを可能にするナビゲーション プロパティ (データベースで一般に外部キーと呼ばれるもの) です。これらに加えて、各エンティティ型には、個別の ID またはキーが必要です。図 2 の XML 断片では、BlogPost というエンティティ型を記述しています。
図 2 BlogPost エンティティ型の定義
<EntityType Name="BlogPost">
   <Key>
      <PropertyRef Name="BlogPostID" />
   </Key>
   <Property Name="BlogPostID" Type="Int32" Nullable="false" />
   <Property Name="BlogEntry" Type="String" Nullable="false" MaxLength="Max"
             Unicode="true" FixedLength="false" />
   <Property Name="BlogDate" Type="DateTime" Nullable="false" />
   <Property Name="BlogTitle" Type="String" Nullable="false" MaxLength="500"
             Unicode="true" FixedLength="false" />
   <Property Name="BlogType" Type="Int32" Nullable="false" />
   <Property Name="CityVisited" Type="String" MaxLength="200"
             Unicode="true" FixedLength="false" />
   <Property Name="CountryVisited" Type="String" MaxLength="200" Unicode="true"
             FixedLength="false" />
   <Property Name="DateVisited" Type="DateTime" />

   <NavigationProperty Name="Blog" Relationship=
                       "MyTravelPostModel.FK_BlogPosts_Blogs"
                       FromRole="BlogPosts" ToRole="Blogs" />
   <NavigationProperty Name="Pictures" Relationship=
                       "MyTravelPostModel.FK_Pictures_BlogPosts"
                       FromRole="BlogPosts" ToRole="Pictures" />
   <NavigationProperty Name="Comments" Relationship=
                       "MyTravelPostModel.BlogComments"
                       FromRole="BlogPosts" ToRole="Comments" />
</EntityType>
エンティティ型のプロパティには、プリミティブ型または複合型を使用できますが (図 3 を参照)、Entity Framework 1.0 では、他のエンティティ型を使用したり、プリミティブ型や複合型のコレクションを使用したりはできません。エンティティ型のキーは、これらのプロパティのサブセットで構成されます。ナビゲーション プロパティを使用すると、1 つのエンティティから他のエンティティへとリレーションシップをナビゲーションできます。
図 3 Address 複合型の定義
<ComplexType Name="Address">
    <Property Name="StreetAddress"
              Type="String" MaxLength="50" />
    <Property Name="Address2"
              Type="String" MaxLength="50" />
    <Property Name="City"
              Type="String" MaxLength="50" />
    <Property Name="Region"
              Type="String" MaxLength="50" />
    <Property Name="PostalCode"
              Type="String" MaxLength="50" />
    <Property Name="Country"
              Type="String" MaxLength="50" />
</ComplexType>
前述のように、リレーションシップは、そのリレーションシップに含まれる各エンティティ型のナビゲーション プロパティとして公開できます。リレーションシップ自体は、EDM の標準要素であり、EDM では関連付けとして明示的に定義されています。
<Association Name="FK_Friends_People">
   <End Role="People" Type="MyTravelPostModel.Person" Multiplicity="1" />
   <End Role="Friends" Type="MyTravelPostModel.Friend" Multiplicity="*" />
   <ReferentialConstraint>
      <Principal Role="People">
         <PropertyRef Name="PersonID" />
      </Principal>
      <Dependent Role="Friends">
         <PropertyRef Name="PrimaryPersonID" />
      </Dependent>
   </ReferentialConstraint>
</Association>
それでは、手短かに言って、私たちはそもそもなぜ新しいデータ モデリング テクノロジを作成したのでしょうか。EDM 以前にもいくつかのデータ モデリング テクノロジまたは言語が存在していましたが、マイクロソフトが実現を目指していた第一目標を満足できるものはなく、多く使用されていた Entity Relationship モデルを実行可能にするために役立つものもありませんでした。開発チームでは、膨大な数の既存のデータ モデリング テクノロジを調査しましたが、そのどれもが特定の問題領域に固有または焦点を絞っている度合いがかなり高いことがわかりました。そのため、これらの目標を満足させて、一連の開発者テクノロジおよびサーバー テクノロジに共通のコア データ モデルとして真に使用可能なモデルを作成するために、EDM の開発を始めたのです。

EDM を XML で記述する理由
多くの検討を重ねた結果、EDM の最初のシリアル表現として XML が選択されました。適切に定義された XML 形式を使用することにより、開発者およびサード パーティは、XML ファイル (またはリソース) を生成するか、または動的に生成された XML 表現から読み込むことで、XML 形式への変換や Entity Framework のメタデータ ランタイムへの読み込みを実現できます。ただし、EDM の表現はそれ以外にも作成可能であり、製品がリリースを重ねる過程で他の表現が導入される可能性もあります。
現在の EDM の文法は、製品に付属の XML スキーマ定義言語 (XSD) で定義されています。ただし、ほとんどの開発者が XML を手作業で開発することは想定していません。その代わりに、Visual Studio で提供されるツールを使用します。その一方で、開発チームでは、ドメイン固有言語 (DSL) への関心や、EDM モデルに対する別の永続化メカニズム (データベースが一般的) への関心も耳にしており、将来のリリースでの拡張オプションも評価中です。

新しいクエリ言語はだれのために必要か
EDM の開発に関する最後の疑問は、なぜ新しいクエリ言語を作成したのかということです。どうして既存の言語を使用してはいけないのでしょうか。その答えは、EDM をもう少し詳しく見ていく中で、明らかになります。
ここまで、EDM が作成された理由と、EDM で使用される各種のコンストラクト、およびこのモデルが Entity Relationship モデルから派生しているという事実について説明しました。基になるデータ ストアに明確にマッピングできるだけでなく、プログラミングに適したアプリケーション レベルのドメイン モデルを表現できるようなモデルを作成する中で、EDM は継承やポリモーフィズムなどの概念をモデリングできる必要がありました。現在のリレーショナル クエリ言語は、継承に基づくクエリや、リレーションシップのナビゲーション、およびポリモーフィズムの結果を返すことをサポートしていないため、この要件を満たすために新しいクエリ言語が必要となりました。
このようにして生まれた Entity SQL (ESQL) は、以前の SQL 言語ではサポートされていない概念に基づいたクエリ機能を追加で備えた、新しい SQL 言語です。EDM がデータベースで使用されているリレーショナル モデルを拡張するのとほとんど同じ方法で、ESQL は既存の SQL 言語を拡張します。また、ESQL はどの特定のバックエンド データベースの構文にも結び付けられていないため、対象となるバックエンド データベースにかかわらず、クエリやアプリケーションを一度だけ記述すれば済みます。次の例は、1 つ以上のブログ ポストを含んでいるすべてのブログと、関連付けられた Person (このモデルの場合はブログ所有者) を取得する、単純な ESQL クエリです。
select c, c.Person
  from travelEntitiesGeneral.Blogs as c
  where c.BlogPosts.Count > 0

EDM を実装する
ADO.NET Entity Framework は、ADO.NET の進化形であり、EDM の最初の具体的な実装として、リレーショナル データベースに対する開発時に、より高い抽象レベルを提供します。バージョン 1.0 では、開発チームは単なる ORM よりもむしろプラットフォームの基盤の構築に重点を置きました。それによって開発者は、非常に柔軟性の高いマッピングと、基になるストアからの逸脱をかなりの程度許容できる能力を備えた、概念モデルまたはオブジェクト モデルを利用できるようになっています。
この高いレベルの柔軟性と、基になるレベルからの逸脱とが、データベースとアプリケーションをそれぞれ別々に進化させるための鍵となります。データベース スキーマに変更が加えられても、アプリケーションは Entity Framework によってその変更から隔離されているため、多くの場合、アプリケーションを部分的に書き換える必要はありません。その代わり、変更を反映する必要がある場合は、単純にマッピング ファイルを更新すれば済みます。
ADO.NET プラットフォームの進化を開始させるには、既存の ADO.NET 2.0 プロバイダ モデル上に Entity Framework を構築し、新しい Entity Framework および ADO.NET 3.5 機能をサポートするよう既存のプロバイダを少しだけ更新します。既存の ADO.NET プロバイダ モデル上に実装することを選択したのは、開発者コミュニティで親しまれているプロバイダ モデルをそのまま利用できるようにするためです。
このアーキテクチャを図 4 に示します。使用可能なスキーマには、概念スキーマ定義言語 (CSDL)、マッピング スキーマ言語 (MSL)、およびストア スキーマ定義言語 (SSDL) が含まれることが、この図からわかります。また、Entity Framework には、正規コマンド ツリー (CCT) をサポートする更新された SqlClient データ プロバイダが含まれていることにも注意してください。
図 4 ADO.NET Entity Framework のアーキテクチャ (クリックすると拡大画像が表示されます)

EntityClient
次に Entity Framework は、これらの ADO.NET 3.5 プロバイダ上に、新しい ADO.NET プロバイダである EntityClient を導入します。EntityClient は、皆さんが慣れ親しんでいる ADO.NET プロバイダと非常によく似ており、開発者が標準の Connection、Command、および DataReader オブジェクトを使用してクエリを EDM 式に実行できるよう、第 1 段階の抽象を提供します。また、EDM で定義されたドメイン モデルを基になるリレーショナル データベース スキーマにマッピングするために必要な、クライアント ビュー エンジンも追加しています。EntityClient は、開発者が必要に応じて、概念的スキーマを表現するクラスを生成しなくても、ESQL クエリ文字列を使用して行および列の形式でエンティティを操作することを可能にします。
図 5 での EntityClient の使い方を見れば、ESQL クエリ文字列を取得して EntityCommand が作成され、EDM に対してそのコマンドが実行されていることがわかるでしょう。EntityCommand の一部として提供されたクエリ テキストが分析され、CCT が作成されます。
図 5 EntityClient に対する ESQL を使用したクエリ
using (EntityConnection conn = new
         EntityConnection("name=travelEntitiesGeneral"))
{
      conn.Open();
      EntityCommand cmd = conn.CreateCommand();
      cmd.CommandText = @"select c.BlogID
         from travelEntitiesGeneral.Blogs as c
         where c.BlogPosts.Count > 0";
      EntityDataReader reader =
         cmd.ExecuteReader(CommandBehavior.SequentialAccess);
      while (reader.Read())
      {
         Console.WriteLine("BlogID = {0}", reader["BlogID"]);
      }
     conn.Close();
}
この最初の段階では、コマンド ツリーはまだ EDM 式に表現されています。クライアント ビュー エンジンでは、データベース システムの具体化されたビューの理論を利用していますが、これらの理論をデータ アクセス層に適用しています。ツリーにマッピング変換を適用することで、基になる論理ストレージ モデルについては同じ動作を表現しているツリーを生成しながら、リレーションシップ、継承、ポリモーフィズムなどの非リレーショナルな概念を除外します。この新しく変換されたツリーが ADO.NET 3.5 プロバイダ サービスに渡され、これらのサービスは、基になるストアに対してネイティブ SQL をカプセル化する DbCommand を返します。このコマンドが実行され、スタック経由で結果が返されて反映されます。

EDM と論理データベース スキーマの間で変換を行うためにクライアント ビュー エンジンで使用されるマッピングを定義する際には、いくつかの異なるオプションがあります。このマッピングは、宣言型の XML 文法であるマッピング仕様言語 (MSL) を使用して指定できます。これは、XML をハンド コーディングするか、または Visual Studio に含まれるエンティティ マッピング ツールを使用して作成できます (図 6 を参照)。
図 6 MSL : EntitySetMapping の例
<EntitySetMapping Name="BlogPosts">
   <EntityTypeMapping TypeName="IsTypeOf(MyTravelPostModel.BlogPost)">
      <MappingFragment StoreEntitySet="BlogPosts">
         <ScalarProperty Name="BlogPostID" ColumnName="BlogPostID" />
         <ScalarProperty Name="BlogEntry" ColumnName="BlogEntry" />
         <ScalarProperty Name="BlogDate" ColumnName="BlogDate" />
         <ScalarProperty Name="BlogTitle" ColumnName="BlogTitle" />
         <ScalarProperty Name="BlogType" ColumnName="BlogType" />
         <ScalarProperty Name="CityVisited" ColumnName="CityVisited" />
         <ScalarProperty Name="CountryVisited"
                         ColumnName="CountryVisited" />
         <ScalarProperty Name="DateVisited" ColumnName="DateVisited" />
      </MappingFragment>
   </EntityTypeMapping>
</EntitySetMapping>
MSL がコンパイルされると、Entity Framework は必要なクエリを生成してビューを更新することができます。クライアント ビュー エンジンではそれらを使用して、EDM で定義されたクエリから論理ストレージ スキーマへの変換を実現します。
マッピングまたはマッピングの一部を表現するための他のオプションとして、ESQL クエリを使用する方法もあります。その場合、開発者が ESQL を使用してクエリ ビューを記述する際には、インフラストラクチャでの要件として、付随する Create、Update、および Delete マッピングもマッピング指定に含めて定義する必要があります。これは必須です。なぜなら、クエリ ビューで ESQL の能力を活用できる場合、1 つの有効な更新ビューが存在しないようなビューを定義することが可能なため、マッピング インフラストラクチャがクエリ ビューに対して対応する更新ビューを生成できないからです。

Object Services
EntityClient によって返される型指定のないデータ レコードに対してではなく、オブジェクトに対して開発できるようにするために、Entity Framework では、EntityClient プロバイダ上に新たな抽象のセットが追加されます。これは、一般に ORM と見なされている層です。この層では、データ モデルに定義されている型の CLR インスタンスが生成され、開発者は LINQ または ESQL を使用してこれらのオブジェクトに対してクエリを実行できるようになります。また、この層は、Entity Framework の中で、市場で各種の ORM テクノロジを調査している開発者を最初にひきつける役割も担っています。
図 1 で見たように、Object Services 層の上位機能は、アプリケーションから ESQL または LINQ クエリを取得して、下層の EntityClient にクエリ式を渡し、IEnumerable<T> を返すことです。ただし、もう少し深く見てみると、Object Services 層の中核には ObjectContext があり、これはアプリケーションと基になるデータ ストアとの間の対話セッションを表しています。
ObjectContext は、開発者がエンティティのインスタンスに対してクエリ、追加、および削除を実行するときや、データベースに新しい状態を返して保存するときに使用する、主要なコンストラクトです。ObjectContext の作成、および ObjectContext を使用したエンティティのクエリ、操作、および SaveChanges のコードを図 7 に示します。この例では、クエリ言語として ESQL を使用しています。
図 7 ObjectContext を使用する
using (ObjectContext context = new ObjectContext("name=travelEntities"))
    {
        //--- create a query for customers
        ObjectQuery<Person> personQuery = context.CreateQuery<Person>(
                     @"select value c from travelEntitiesGeneral.People
                     as c where c.PersonID == 1");
        //--- by enumerating the query will be implicitly executed
        //--- against the store and you can now work with an
        //--- IEnumerable<Customer>
        foreach (Person c in personQuery)
        {
            //--- dereference anything you like from Customer
            Console.WriteLine(c.PersonID + ": " + c.Name);
            c.Name = "New Name";
        }
        try
        {
            context.SaveChanges();
        }
        catch (OptimisticConcurrencyException opt)
        {
            // catching this exception allows you to
            // refresh travelEntities with either store/client wins
            // project the travelEntities into this failed travelEntities.
            var failedEntities = from e3 in opt.StateEntries
                                 select new { e3.Entity };

            // Note: in future you should be able to just pass
            // the opt.StateEntities
            // in to refresh.
            context.Refresh(RefreshMode.ClientWins, failedEntities.ToList());
            context.SaveChanges();
        }
    }
メモリ内でオブジェクトに加えられた変更を追跡するプロセス、およびそれらの変更をデータベースに返して保存するプロセスは、Object Services の使用によって単純化されます。Object Services では、ObjectStateManager を利用して、メモリ内のインスタンスの現在の状態だけでなく、ストアから取得された時点での各インスタンスの初期状態も追跡するため、Entity Framework は、データをデータベースに返すときにオプティミスティックな同時実行を適用できます。追跡された変更は簡単に保存でき、ObjectContext に対して SaveChanges メソッドが呼び出されたときに、データ ストアに返して反映させることができます。
ここまで、ObjectContext に関する一般的な事項を述べ、基本となる ObjectContext の使用例を示してきました。これは多くの場合、EDM モデルを消費する動的なツールやアプリケーションが存在するシナリオで使用されます。ただし、開発環境として Visual Studio を使用する場合には、厳密に型指定された ObjectContext が持つ追加の利点として、対象の EDM に固有である可能性がある機能を公開するようなプロパティとメソッドを追加できます。
厳密に型指定された ObjectContext を使用して構築されたクエリを図 8 に示します。この例では、クエリ言語として LINQ が使用されています。厳密に型指定された ObjectContext を使用すると、プロパティが EntitySet ごとに公開されるため、プロパティを見つけやすくなります。たとえば、travelEntities.CreateQuery<BlogPost>("travelEntitiesGeneral.BlogPosts") の代わりに travelEntities.BlogPosts が使用されます。
図 8 厳密に型指定された ObjectContext によって構築したクエリ
using (MyTravelPostEntities travelEntities = new MyTravelPostEntities())
{
    // get the latest blog post, with the comments and the people
    // I'm querying for all the blog posts that are related to this blog.
    // I want to include the comments and the people who wrote the
    // comments.
    // I also want only the most recent posting.
    // Note: Since we use the EntityKey that is put on the EntityReference
    // we can either do a tracking query or use span.
    BlogPost post = (from bp in
        travelEntities.BlogPosts
                              .Include("Comments.Person")
                              .Include("Blog")
                     where bp.Blog.BlogID == requestedBlog.BlogID
                     orderby bp.BlogDate descending
                     select bp).First();
    return post;
}
LINQ to Entities は Object Services の上にある比較的薄い層と考えることができ、文字列ベースのクエリの代わりに、プログラミング言語内で直接クエリ機能を提供します (図 8 を参照)。ここでは、ObjectQuery クラスが IQueryable を実装して、LINQ の式ツリーを取得可能にし、Object Services が EntityClient プロバイダに ESQL クエリを渡すのと同じ方法で、Entity Framework を通してクエリを CCT クエリ式として渡しています。
Entity Framework による n 層開発
n 層開発に完全に対応することは私たちの第一の目的ではありませんが、Entity Framework での開発に関する興味深いシナリオの 1 つとして、それに触れておくことにします。バージョン 1.0 では、Entity Framework はいくつかの主要なシナリオで n 層開発をサポートしています。これには、ADO.NET データ サービスの使用、またはエンティティをシリアル化して ObjectContext にアタッチおよびデタッチする能力による Windows® Communication Foundation (WCF) の使用が含まれます。これらは明らかに、n 層開発に対する唯一のアプローチではありませんが、開発チームが V1 で焦点を当てることを選んだソリューションであり、V2 以降では、よりデータセットに似た体験など、新たなシナリオが追加されています。図 9 に、これらの内容を示します。
図 9 n 層アプリケーションでの ADO.NET データ サービス
static Uri baseService = new
   Uri("http://localhost:17338/MyTravelPostService.svc");
MyPeople2Entities context = new MyPeople2Entities(baseService);
    // get the comment that is being marked for deletion
    // and get the view state blog post.
BlogPost post = (BlogPost)ViewState["BlogPost"];

    // move the comment to the deleted comment selection.
Comment deletedComment = post.Comments[e.RowIndex];

    // call the DeleteComment service
context.AttachTo("Comments", deletedComment);
context.DeleteObject(deletedComment);
DataServiceResponse r = context.SaveChanges();

    // reload page so that F5, refresh doesn't update all this data.
ReloadPage();
ADO.NET データ サービスは、Representational State Transfer (REST) アーキテクチャ スタイル (各リソースがシステム内の "名詞"、つまり Uniform Resource Identifier (URI) で一意に扱えるものを表す) の具体的な実現であり、任意の IQueryable 実装上での n 層アプリケーション開発を可能にします。ADO.NET データ サービスでは、インスタンスに対する単なるクエリ以上のことを、ネットワーク経由で実行できます。ADO.NET データ サービスは、Create、Read、Update、および Delete に対する各種の HTTP 動詞をサポートし、開発者がソリューションを実装する際に役立つクライアント側抽象を提供します。
n 層シナリオの第 2 のオプションは、エンティティをシリアル化して ObjectContext にアタッチおよびデタッチする能力を利用し、Entity Framework で WCF を使用することです。図 10 に、このシナリオで ObjectContext にアタッチする方法を示します。
図 10 ObjectContext にアタッチする
// the creation of the travel MyTravelPostEntities opens the connection
// and sets up all the metadata information automatically for you.
using (MyTravelPostEntities travelEntities = new MyTravelPostEntities())
{
    // attach the comment and delete.
    travelEntities.Attach(deleteComment);

    // call delete on the object
    travelEntities.DeleteObject(deleteComment);

    try
    {
       travelEntities.SaveChanges();
    }
    catch (OptimisticConcurrencyException opt)
    {
      // catching this exception allows you to
      // refresh travelEntities with either store/client wins
      // project the travelEntities into this failed travelEntities.
      var failedEntities = from e3 in opt.StateEntries
                           select new { e3.Entity };

     travelEntities.Refresh(RefreshMode.ClientWins, failedEntities.ToList());
     travelEntities.SaveChanges();
    }
}
既定では、Visual Studio または edmgen.exe (Entity Framework に付属するコマンドライン ツール) を使用して EDM から生成される任意の CLR クラスは、XML シリアル化およびバイナリ シリアル化が可能であり、既定の属性が DataMembers であるナビゲーション プロパティを持ったデータ コントラクトです。それにより、ASMX Web サービスの作成や、Entity インスタンスをビュー ステートまたは WCF サービスで使用することが可能になります。
ほとんどの ORM と同様に、現時点で Entity Framework は、データ操作言語 (DML) での作成、更新、または削除操作をサポートしていません。変更はメモリ内のオブジェクトに適用する必要があり、永続化するグラフ全体を構築するためには、データベースへのラウンドトリップが何回か必要になる可能性があります。
これを避けることができる方法の 1 つは、ObjectContext によって提供されるアタッチ機能を使用することです。Attach を使用すると、Entity が既に存在すること、および操作のセットをメモリ内で実行してから変更を反映する必要があることを、インフラストラクチャに指示できます。Entity Framework を使用した n 層開発の詳細については、MSDN® ライブラリを検索してください。近いうちに内容が追加される予定です。

単なる ORM の一種か
これまで、Entity Framework は多くの人々から、単に市場に出ている ORM の 1 つであると考えられており、製品の最初のバージョンを単純に見る限り、それは理解できることです。その方向について言えば、これまでに製品に搭載された機能の多くにより、ORM が取り組み始めたシナリオの中心的な部分は実現されています。ただし、これまで行われた多くの分析では、市場内の他のいくつかの ORM で可能な要素を Entity Framework が必ずしもすべての場合にカバーしていないことが指摘されており、それは妥当な見解と言えます。
マイクロソフトがこの領域で行っている投資は、従来の ORM 製品の投資をはるかに上回るよう意図されており、これから簡単に述べるように、Entity Framework は、EDM に関する幅広い戦略における最初の一歩なのです。この記事の冒頭で述べたように、EDM は単なる Entity Framework や従来の ORM の世界を飛び越えて適用できる、より高いレベルのドメイン モデルを作成します。Microsoft .NET Framework、Visual Studio、SQL Server®、および他のマイクロソフト テクノロジの次のいくつかのリリースで、EDM の採用が段階的に増えていくでしょう。
そのような見通しと、EDM の行く先についての全体的ビジョンこそが、この記事で述べてきた製品に関する決定事項の多くに見られたように、最も大きな影響を与えてきた要素です。多くの決定が、Reporting Services や Analysis Services などのテクノロジの採用を実現するという明確な意図に基づいて行われました。それにより、一般的な一貫したドメイン モデルでサービスを提供できるため、顧客に対して強力な利点がもたらされます。
Visual Studio 2008 SP1 では、Entity Framework と共に、このビジョンの最初の実現が ADO.NET データ サービスとしてリリースされます。REST ベースのアプリケーションに対して開発者に魅力的な体験を提供する ADO.NET データ サービスは、メタデータ交換形式として EDM を使用して構築される、(Entity Framework 外部では) 初めてのリリース製品となります。
このリリースに合わせて、マイクロソフトではいくつかの異なる Windows Live™ プロパティのデモを MIX 2008 で行いました。これらのプロパティは、ADO.NET データ サービス プロトコルおよび EDM を使用してデータを公開するものです。同様に、開発チームでは、SQL Server および Visual Studio の次回のリリースに対する計画を開始しており、コアにおける EDM および Entity Framework のエンドツーエンドの開発者体験の向上に向け、日々努力を重ねています。

Elisa Flasko は、マイクロソフトのデータ プログラマビリティ チームのプログラム マネージャであり、ADO.NET、XML、および SQL Server 接続テクノロジを主に担当しています。blogs.msdn.com/elisaj で彼女のブログをご覧いただけます。


データと WPF
データ バインドと WPF でデータの表示をカスタマイズする
Josh Smith
この記事では、次の内容について説明します。
  • WPF データ バインド
  • データ表示と階層データ
  • テンプレートの使用
  • 入力の検証
 この記事では、次のテクノロジを使用しています。
WPF、XAML、C#

コードのダウンロード : AdvancedWPFDatabinding2008_07.exe (171 KB)
オンラインでのコードの参照
目次
コードでバインドする
テンプレートを使用する
継承された DataContext を使用する
コレクション ビューを使用する
階層データを使用する
多数のコントロールを使用して XML データを表示する
多数のコントロールを使用してビジネス オブジェクトを表示する
階層全体を 1 つのコントロールで表示する
階層データ テンプレートを使用する
ユーザー入力を処理する
ValidationRules による入力検証
検証エラーを表示する
IDataErrorInfo による入力検証
まとめ
バーチャル ラボでの実験 :
Windows® Presentation Foundation (WPF) が .NET の領域に初めて出現したとき、WPF に関する記事やデモ アプリケーションは、WPF のすばらしいレンダリング エンジンと 3D 機能を強調するものがほとんどでした。このような例は、読むのも実行するのも楽しいですが、現実の世界での WPF の強力な機能を示してはいません。たいていの場合、開発者が作成する必要があるのは、クリックすると花火が上がる回転ビデオ キューブを使ったアプリケーションではありません。職業として開発を行うほとんどの開発者が作成するのは、大量の複雑なビジネス データや科学データの表示および編集用のソフトウェアです。
さいわいにも、WPF は、表示の管理と複雑なデータの編集に対する優れたサポートを提供しています。2007 年 12 月の MSDN® Magazine で、John Papa は「WPF でのデータ バインド」(msdn.microsoft.com/magazine/cc163299) を執筆しました。これは、WPF データ バインドの基本概念を説明するすばらしい記事でした。ここでは、John が前述のデータ ポイントのコラムで説明した内容を基に、より高度なデータ バインドのシナリオを検証します。最終的には、ほとんどの基幹業務アプリケーションに見られる一般的なデータ バインド要件のさまざまな実装方法を理解できます。

コードでバインドする
WPF がデスクトップ アプリケーション開発者にもたらす最大の変化の 1 つは、宣言型プログラミングの広範な使用とサポートです。WPF のユーザー インターフェイスとリソースは、XML ベースのマークアップ言語である XAML (Extensible Application Markup Language) を使用して宣言できます。WPF データ バインドに関する説明のほとんどは、XAML でバインドを使用する方法しか示していません。XAML でできることはコードでも実現可能なので、プロフェッショナルな WPF 開発者は、データ バインドを宣言で使用する方法だけでなく、プログラムで使用する方法を理解することが重要です。
多くの場合、XAML でバインドを宣言する方が便利であり、適切です。システムが複雑で動的になるに従い、コードでバインドを使用する方が有効な場合があります。先に進む前に、まず、プログラムによるデータ バインドに関連した一般的なクラスとメソッドをいくつか確認しておきましょう。
WPF 要素は、SetBinding メソッドと GetBindingExpression メソッドの両方を FrameworkElement または FrameworkContentElement から継承します。これらは、BindingOperations ユーティリティ クラスの同じ名前のメソッドを呼び出す便利なメソッドです。次のコードは、BindingOperations クラスを使用してテキストボックスの Text プロパティを別のプロジェクトのプロパティにバインドする方法を示しています。
static void BindText(TextBox textBox, string property)
{
      DependencyProperty textProp = TextBox.TextProperty;
      if (!BindingOperations.IsDataBound(textBox, textProp))
      {
          Binding b = new Binding(property);
          BindingOperations.SetBinding(textBox, textProp, b);
      }
}
次に示すコードを使用して、プロパティを簡単にバインド解除できます。
static void UnbindText(TextBox textBox)
{
    DependencyProperty textProp = TextBox.TextProperty;
    if (BindingOperations.IsDataBound(textBox, textProp))
    {
        BindingOperations.ClearBinding(textBox, textProp);
    }
}
バインドをクリアすると、ターゲット プロパティからもバインド値が削除されます。
データ バインドを XAML で宣言すると、基礎になる詳細情報の一部が隠されます。コードでバインドを使用するときに詳細情報が明らかになります。詳細情報の 1 つは、バインドのソースとターゲット間の関係が、実際には Binding 自体ではなく BindingExpression クラスのインスタンスによって維持されるという事実です。Binding クラスには、複数の BindingExpression で共有できる上位レベルの情報が含まれますが、バインドされた 2 つのプロパティ間でのリンクの適用は基礎になる式に基づきます。次のコードは、テキストボックスの Text プロパティが検証されるかどうかを、BindingExpression を使用してプログラムでチェックする方法を示しています。
static bool IsTextValidated(TextBox textBox)
{
    DependencyProperty textProp = TextBox.TextProperty;

    var expr = textBox.GetBindingExpression(textProp);
    if (expr == null)
        return false;

    Binding b = expr.ParentBinding;
    return b.ValidationRules.Any();
}
BindingExpression が検証されるかどうかを BindingExpression 自体は認識していないので、親バインドに問い合わせる必要があります。入力検証の手法については後で説明します。
バーチャル ラボ : 高度な WPF データ バインド
WPF は、表示の管理と複雑なデータの編集に対する優れたサポートを提供しています。比較的少ない行数の XAML を使用して、階層データ構造の表示やユーザー入力の検証を行うことができます。あらかじめ構成されているバーチャル ラボで WPF データ バインドの実験ができます。この記事で説明しているプロジェクトを含めて、すべてのインストールと準備が完了しています。ラボを起動し、記事を読み進めながらコードを実行してください。

バーチャル ラボでの実験 :
go.microsoft.com/fwlink/?LinkId=120301

テンプレートを使用する
効果的なユーザー インターフェイスでは、表示された生データから、そこに含まれる有意義な情報をユーザーが直感的に発見できます。これはデータ ビジュアライゼーションの重要な部分です。データ バインドは、データ ビジュアライゼーションというパズルの 1 つのピースにすぎません。よほど単純なプログラムでない限り、すべての WPF プログラムには、単にコントロールの 1 つのプロパティをデータ オブジェクトの 1 つのプロパティにバインドするだけでなく、もっと強力な方法でデータを表示する手段が必要です。実際のデータ オブジェクトには多数の関連する値があり、それらのさまざまな値を 1 つのまとまったビジュアル表現に集約する必要があります。そのために、WPF にはデータ テンプレートがあります。
System.Windows.DataTemplate クラスは、WPF のテンプレートの 1 つの形式です。一般に、テンプレートはクッキーの抜き型のようなものであり、組み込みのビジュアル表現がないオブジェクトのレンダリングを支援するビジュアル要素を作成するために WPF フレームワークで使用されます。カスタム ビジネス オブジェクトなど、組み込みのビジュアル表現がないオブジェクトを要素で表示する場合は、DataTemplate を指定することによって、オブジェクトのレンダリング方法を要素に指示できます。
DataTemplate では、データ オブジェクトを表示するためのビジュアル要素を必要に応じていくつでも生成できます。生成したビジュアル要素でデータ バインドを使用して、データ オブジェクトのプロパティ値を表示します。レンダリングを指示されたオブジェクトの表示方法が不明な場合、要素は単純に ToString メソッドを呼び出して結果を TextBlock に表示します。
人物の名前を格納する FullName という簡単なクラスがあるとします。名前の一覧を表示し、各人物の姓を名前の他の部分よりも目立つようにします。このために、FullName オブジェクトのレンダリング方法を記述した DataTemplate を作成できます。図 1 のコードは、FullName クラスと、名前の一覧を表示するウィンドウの分離コードを示しています。
図 1 DataTemplate を使用して FullName を表示する
public class FullName
{
    public string FirstName { get; set; }
    public char MiddleInitial { get; set; }
    public string LastName { get; set; }
}

public partial class WorkingWithTemplates : Window
{
    // This is the Window's constructor.
    public WorkingWithTemplates()
    {
        InitializeComponent();

        base.DataContext = new FullName[]
        {
            new FullName
            {
                FirstName = "Johann",
                MiddleInitial = 'S',
                LastName = "Bach"
            },
            new FullName
            {
                FirstName = "Gustav",
                MiddleInitial = ' ',
                LastName = "Mahler"
            },
            new FullName
            {
                FirstName = "Alfred",
                MiddleInitial = 'G',
                LastName = "Schnittke"
            }
        };
    }
}
図 2 に示すように、ウィンドウの XAML ファイルには ItemsControl があります。ItemsControl は、ユーザーが選択も削除もできないアイテムの単純な一覧を作成します。ItemsControl の ItemTemplate プロパティには DataTemplate が割り当てられています。ItemsControl はその ItemTemplate プロパティを使用して、ウィンドウのコンストラクタで作成された各 FullName インスタンスをレンダリングします。DataTemplate の TextBlock 要素の大部分で、表示する FullName オブジェクトのプロパティに Text プロパティをバインドしている方法に注目してください。
図 2 DataTemplate を使用して FullName オブジェクトを表示する
<!-- This displays the FullName objects. -->
<ItemsControl ItemsSource="{Binding Path=.}">
  <ItemsControl.ItemTemplate>
    <DataTemplate>
      <StackPanel Orientation="Horizontal">
        <TextBlock FontWeight="Bold" Text="{Binding LastName}" />
        <TextBlock Text=", " />
        <TextBlock Text="{Binding FirstName}" />
        <TextBlock Text=" " />
        <TextBlock Text="{Binding MiddleInitial}" />
      </StackPanel>
    </DataTemplate>
  </ItemsControl.ItemTemplate>
</ItemsControl>
このデモ アプリケーションを実行すると、結果は図 3 のようになります。DataTemplate を使用して名前をレンダリングすると、対応する TextBlock の FontWeight が太字になり、各人物の姓を簡単に強調できます。この簡単な例は、WPF のデータ バインドとテンプレートの基本的な関係を示しています。このトピックをさらに掘り下げて、これらの機能と、複雑なオブジェクトをビジュアル化するさらに強力な方法を結合します。
図 3 DataTemplate によってレンダリングした FullName

継承された DataContext を使用する
特に明記しない限り、すべてのバインドは要素の DataContext プロパティに対して暗黙的に行われます。要素の DataContext は、いわばデータ ソースを参照します。DataContext の動作には認識しておく必要がある特別な点があります。DataContext のこの微妙な側面を理解すると、複雑なデータバインド ユーザー インターフェイスのデザインが大幅に簡略化されます。
データ ソース オブジェクトを参照するために要素の DataContext プロパティを設定する必要はありません。要素ツリー (技術的には論理ツリー) にある先祖要素の DataContext にデータ コンテキストの値が指定されている場合、値はユーザー インターフェイスのすべての子孫要素に自動的に継承されます。つまり、ウィンドウの DataContext が Foo オブジェクトを参照するように設定されている場合、既定ではウィンドウ内のすべての要素の DataContext がその同じ Foo オブジェクトを参照します。ウィンドウ内の任意の要素に異なる DataContext 値を簡単に指定でき、それによってその要素のすべての子孫要素が新しい DataContext 値を継承します。これは、Windows フォームのアンビエント プロパティと同様です。
前のセクションでは、DataTemplate を使用してデータ オブジェクトのビジュアライゼーションを作成する方法を検証しました。図 2 のテンプレートで作成した要素は、プロパティが FullName オブジェクトのプロパティにバインドされています。それらの要素は DataContext に暗黙的にバインドされます。DataTemplate で作成した要素の DataContext は、FullName オブジェクトなど、テンプレートを使用する対象のデータ オブジェクトを参照します。
DataContext プロパティの値の継承には魔法を使っているわけではなく、WPF の継承された依存関係プロパティに対する組み込みサポートを利用しています。どの依存関係プロパティでも、そのプロパティを WPF の依存関係プロパティ システムに登録するときに提供されるメタデータにフラグを指定するだけで、継承されたプロパティにすることができます。
継承された依存関係プロパティの別の例として FontSize があります。FontSize は、すべての要素に存在するプロパティです。FontSize 依存関係プロパティをウィンドウに対して設定した場合、既定では、そのウィンドウのすべての要素にそのサイズのテキストが表示されます。要素ツリーの下位へ FontSize 値を伝播するために使用されるのと同じインフラストラクチャによって、DataContext が伝播されます。
ここで使用している "継承" という用語の意味は、オブジェクト指向の "継承" とは異なります。オブジェクト指向では、親クラスのメンバをサブクラスが継承します。プロパティ値の継承は、実行時に要素ツリーの下位に対して行われる値の伝播を意味しているにすぎません。当然ながら、クラスに継承された依存関係プロパティが、オブジェクト指向で意味するところの値の継承をたまたまサポートしていることはあり得ます。

コレクション ビューを使用する
WPF コントロールをデータのコレクションにバインドするとき、WPF コントロールはコレクション自体に直接バインドされるのではなく、そのコレクションに自動的にラップされるビューに対して暗黙的にバインドされます。ビューは ICollectionView インターフェイスを実装し、ListCollectionView などの複数の具体的な実装のいずれかになります。
コレクション ビューは複数の機能を果たします。まず、コレクション内の現在のアイテムを追跡します。通常、コレクション内の現在のアイテムはリスト コントロール内のアクティブなアイテムまたは選択済みアイテムに変換されます。また、コレクション ビューは一覧でのアイテムの並べ替え、フィルタ処理、グループ化のための汎用的な手段を提供します。複数のコントロールを 1 つのコレクションの同じビューにバインドし、すべてのコントロールを相互に連携させることができます。次のコードは、ICollectionView の一部の機能を示しています。
// Get the default view wrapped around the list of Customers.
ICollectionView view = CollectionViewSource.GetDefaultView(allCustomers);

// Get the Customer selected in the UI.
Customer selectedCustomer = view.CurrentItem as Customer;

// Set the selected Customer in the UI.
view.MoveCurrentTo(someOtherCustomer);
コレクション ビューの CurrentItem プロパティと同期を保つために、リストボックス、コンボボックス、リスト ビューなどのすべてのリスト コントロールで、IsSynchronizedWithCurrentItem プロパティが true に設定されている必要があります。そのプロパティは Selector 抽象クラスで定義します。true に設定されていない場合は、リスト コントロール内のアイテムを選択しても、コレクション ビューの CurrentItem は更新されず、CurrentItem への新しい値の割り当てはそのリスト コントロールに反映されません。

階層データを使用する
現実の世界は階層データに満ちています。客は 1 人で複数の注文を行い、分子は多数の原子から構成されます。部門は大勢の従業員で構成され、太陽系には複数の天体が存在します。このありふれたマスタ/詳細の構造は、間違いなくユーザーになじみのあるものです。
WPF では、さまざまな方法で階層型データ構造を処理でき、それぞれの状況に応じて適切な方法を使用できます。選択肢は基本的に、多数のコントロールを使用してデータを表示するか、1 つのコントロールでデータ階層の複数のレベルを表示するかに要約されます。次に、これらの両方のアプローチについて詳しく検討します。

多数のコントロールを使用して XML データを表示する
階層データを処理するとき、ごく一般的な方法は、階層の各レベルを別々のコントロールで表示する方法です。たとえば、顧客、注文、および注文の詳細を表すシステムがあるとします。このような場合は、コンボボックスで顧客を表示し、選択した顧客のすべての注文をリストボックスで表示し、選択した注文に関する詳細を ItemsControl で表示することができます。これは階層データを表示する優れた方法であり、WPF での実装はかなり簡単です。
前に説明したシナリオに基づいて、WPF XmlDataProvider コンポーネントにラップしてアプリケーションで処理するデータの簡略化されたサンプルを図 4 に示します。図 5 のようなユーザー インターフェイスでそのデータを表示できます。注意すべき点として、顧客と注文は選択可能ですが、注文の詳細は読み取り専用リストに表示されます。これは適切なことです。ビジュアル オブジェクトは、アプリケーションの状態に影響する場合または編集可能な場合にのみ選択可能にする必要があるからです。
図 4 顧客、注文、および OrderDetails XML 階層
<XmlDataProvider x:Key="xmlData">
  <x:XData>
    <customers >
      <customer name="Customer 1">
        <order desc="Big Order">
          <orderDetail product="Glue" quantity="21" />
          <orderDetail product="Fudge" quantity="32" />
          </order>
          <order desc="Little Order">
            <orderDetail product="Ham" quantity="1" />
            <orderDetail product="Yarn" quantity="2" />
          </order>
        </customer>
        <customer name="Customer 2">
          <order desc="First Order">
            <orderDetail product="Mousetrap" quantity="4" />
          </order>
        </customer>
      </customers>
    </x:XData>
</XmlDataProvider>
図 5 XML データを表示する 1 つの方法
図 6 の XAML では、これらのさまざまなコントロールを使用して、示されている階層データを表示する方法を説明します。このウィンドウにコードは不要であり、ウィンドウ全体が XAML になっています。
図 6 階層 XML データを UI にバインドする XAML
<Grid DataContext=
    "{Binding Source={StaticResource xmlData},
    XPath=customers/customer}"
    Margin="4"
  >
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto" />
    <RowDefinition />
    <RowDefinition />
  </Grid.RowDefinitions>

  <!-- CUSTOMERS -->
  <DockPanel Grid.Row="0">
    <TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Customers" />
    <ComboBox
      IsSynchronizedWithCurrentItem="True"
      ItemsSource="{Binding}"
      >
      <ComboBox.ItemTemplate>
        <DataTemplate>
          <TextBlock Text="{Binding XPath=@name}" />
        </DataTemplate>
      </ComboBox.ItemTemplate>
    </ComboBox>
  </DockPanel>

  <!-- ORDERS -->
  <DockPanel Grid.Row="1">
    <TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Orders" />
    <ListBox
      x:Name="orderSelector"
      DataContext="{Binding Path=CurrentItem}"
      IsSynchronizedWithCurrentItem="True"
      ItemsSource="{Binding XPath=order}"
      >
      <ListBox.ItemTemplate>
        <DataTemplate>
          <TextBlock Text="{Binding XPath=@desc}" />
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>
  </DockPanel>

  <!-- ORDER DETAILS -->
  <DockPanel Grid.Row="2">
    <TextBlock DockPanel.Dock="Top" FontWeight="Bold"
      Text="Order Details" />
    <ItemsControl
      DataContext=
         "{Binding ElementName=orderSelector, Path=SelectedItem}"
      ItemsSource="{Binding XPath=orderDetail}">
      <ItemsControl.ItemTemplate>
        <DataTemplate>
          <TextBlock>
            <Run>Product:</Run>
            <TextBlock Text="{Binding XPath=@product}" />
            <Run>(</Run>
            <TextBlock Text="{Binding XPath=@quantity}" />
            <Run>)</Run>
          </TextBlock>
        </DataTemplate>
      </ItemsControl.ItemTemplate>
    </ItemsControl>
  </DockPanel>
</Grid>
短い XPath クエリを何回も使用して、WPF に対してバインド値の取得先を指示していることに注目してください。Binding クラスで公開されている XPath プロパティに、XmlNode.SelectNodes メソッドがサポートしている XPath クエリを割り当てることができます。WPF が XPath クエリを実行するために実際に使用されるのはそのメソッドです。あいにく、XmlNode.SelectNodes は現在 XPath 関数の使用をサポートしていないので、WPF データ バインドでも XPath 関数はサポートされません。
顧客のコンボボックスと注文のリストボックスのバインド先は、どちらも、ルート Grid の DataContext バインドによって実行される XPath クエリから返された一連のノードです。リストボックスの DataContext は、Grid の DataContext に対して生成される XmlNode のコレクションにラップされたコレクション ビューの CurrentItem を自動的に返します。つまり、リストボックスの DataContext は、現在選択されている Customer です。そのリストボックスの ItemsSource は独自の DataContext に暗黙的にバインドされ (その他のソースが指定されていないので)、ItemsSource バインドは XPath クエリを実行して DataContext から <order> 要素を取得し、ItemsSource は事実上、選択した顧客の注文のリストにバインドされます。
XML データにバインドするときは、実際には XmlNode.SelectNodes の呼び出しによって作成されたオブジェクトにバインドしているのだということに注意してください。気を付けないと、論理的に同じでも物理的に異なる一連の XmlNode に対して複数のコントロールがバインドされる結果となります。その理由は、XmlNode.SelectNodes を呼び出すたびに、たとえ毎回同じ XPath クエリを同じ XmlNode に渡したとしても、新しい一連の XmlNode が生成されるためです。これは XML データ バインドに固有の注意事項であり、ビジネス オブジェクトにバインドする場合はさいわいにも無視できます。

多数のコントロールを使用してビジネス オブジェクトを表示する
ここでは、前の例と同じデータにバインドし、データが XML ではなくビジネス オブジェクトとして存在すると想定します。データ階層のさまざまなレベルにバインドする方法はどのように変化するでしょうか。バインド方法にどのような類似点と相違点があるでしょうか。
図 7 のコードは、バインド先のデータを格納するビジネス オブジェクトの作成に使用される簡単なクラスを示しています。これらのクラスは、前のセクションで使用した XML データと同じ論理スキーマを構成します。
図 7 ビジネス オブジェクトの階層を作成するクラス
public class Customer
{
    public string Name { get; set; }
    public List<Order> Orders { get; set; }

    public override string ToString()
    {
        return this.Name;
    }
}

public class Order
{
    public string Desc { get; set; }
    public List<OrderDetail> OrderDetails { get; set; }

    public override string ToString()
    {
        return this.Desc;
    }
}

public class OrderDetail
{
    public string Product { get; set; }
    public int Quantity { get; set; }
}
これらのオブジェクトを表示するウィンドウの XAML を図 8 に示します。これは図 6 に示した XAML と非常によく似ていますが、特筆すべき重要な違いがいくつかあります。XAML に見られないのは、ウィンドウのコンストラクタでデータ オブジェクトを作成し、DataContext を設定する部分です。これは、XAML で DataContext をリソースとして参照するのとは違っています。どのコントロールにも DataContext が明示的に設定されていないことに注目してください。すべてのコントロールが同じ DataContext を継承します。それは List<Customer> インスタンスです。
図 8 階層ビジネス オブジェクトを UI にバインドする XAML
<Grid Margin="4">
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto" />
    <RowDefinition />
    <RowDefinition />
  </Grid.RowDefinitions>

   <!-- CUSTOMERS -->
   <DockPanel Grid.Row="0">
     <TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Customers"
     />
     <ComboBox
       IsSynchronizedWithCurrentItem="True"
       ItemsSource="{Binding Path=.}"
       />
   </DockPanel>

   <!-- ORDERS -->
   <DockPanel Grid.Row="1">
     <TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Orders" />
     <ListBox
       IsSynchronizedWithCurrentItem="True"
       ItemsSource="{Binding Path=CurrentItem.Orders}"
       />
   </DockPanel>

   <!-- ORDER DETAILS -->
   <DockPanel Grid.Row="2">
     <TextBlock DockPanel.Dock="Top" FontWeight="Bold"
        Text="Order Details" />
     <ItemsControl
       ItemsSource="{Binding Path=CurrentItem.Orders.CurrentItem.
       OrderDetails}"
       >
       <ItemsControl.ItemTemplate>
         <DataTemplate>
           <TextBlock>
             <Run>Product:</Run>
             <TextBlock Text="{Binding Path=Product}" />
             <Run>(</Run>
             <TextBlock Text="{Binding Path=Quantity}" />
             <Run>)</Run>
           </TextBlock>
         </DataTemplate>
       </ItemsControl.ItemTemplate>
     </ItemsControl>
   </DockPanel>
</Grid>
XML ではなくビジネス オブジェクトにバインドする場合のもう 1 つの大きな違いは、注文の詳細をホストする ItemsControl を注文のリストボックスの SelectedItem にバインドする必要がないことです。このアプローチは、XML バインド シナリオでは必要でした。ローカル XPath クエリからアイテムを取得したリストの現在のアイテムを参照する汎用的な方法がないためです。
XML ではなくビジネス オブジェクトにバインドする場合、選択したアイテムのネストされたレベルをバインドすることは簡単です。ItemsControl の ItemsSource バインドでは、バインド パスに CurrentItem を 2 回指定することでこの便利な機能を利用します。1 回目は選択した顧客について指定し、2 回目は選択した注文について指定します。この記事で前に説明したように、CurrentItem プロパティは、データ ソースにラップされた、基礎になる ICollectionView のメンバです。
XML アプローチとビジネス オブジェクト アプローチの動作の違いについて、興味深い点がもう 1 つあります。XML の例では XmlElements にバインドするので、顧客と注文のレンダリング方法を説明した DataTemplate を指定する必要があります。カスタム ビジネス オブジェクトにバインドする場合は、Customer クラスと Order クラスの ToString メソッドを単純に上書きし、WPF でこれらのオブジェクトのメソッドの出力を表示できるようにすることで、このオーバーヘッドを回避できます。この方法は、単純なテキスト表現を持つことができるオブジェクトの場合に限り、十分に目的を果たします。複雑なデータ オブジェクトを扱う場合は、この便利な手法を使用することに意味がない場合があります。

階層全体を 1 つのコントロールで表示する
ここまでは、各階層レベルを別々のコントロールで示すことによって階層データを表示する方法のみを見てきました。同じコントロール内に階層データ構造のすべてのレベルを示すことは、多くの場合に便利であり、また必要なことです。このアプローチの一般的な例は、ネストされたデータの任意のレベルの表示と移動をサポートする TreeView コントロールです。
2 つの方法のいずれかで、WPF TreeView にアイテムを挿入できます。1 つのオプションは、コードまたは XAML でアイテムを手動で追加することで、もう 1 つのオプションは、データ バインドを使用してアイテムを作成することです。
次の XAML は、いくつかの TreeViewItem を XAML の TreeView に手動で追加する方法を示しています。
<TreeView>
  <TreeViewItem Header="Item 1">
    <TreeViewItem Header="Sub-Item 1" />
    <TreeViewItem Header="Sub-Item 2" />
  </TreeViewItem>
  <TreeViewItem Header="Item 2" />
</TreeView>
TreeView にアイテムを手動で作成する手法は、コントロールが常に小さく静的な一連のアイテムを表示する場合に有効です。時間の経過に伴って変化する可能性のある大量のデータを表示しようとすると、より動的なアプローチを使用することが必要になります。その場合、2 つのオプションがあります。データ構造を検索し、見つかったデータ オブジェクトに基づいて TreeViewItem を作成し、さらにそれらのアイテムを TreeView に追加するコードを作成できます。または、階層データ テンプレートを利用し、WPF ですべての作業を行うこともできます。

階層データ テンプレートを使用する
WPF で階層データ テンプレートに従って階層データをレンダリングする方法を宣言によって表現できます。HierarchicalDataTemplate クラスは、複雑なデータ構造とそのデータのビジュアル表現の間のギャップを埋めるツールです。これは標準の DataTemplate とよく似ており、しかもデータ オブジェクトの子アイテムをどこから取得するかを指定できます。取得したの子アイテムのレンダリングに使用する HierarchicalDataTemplate テンプレートも指定できます。
ここで、図 7 に示したデータを 1 つの TreeView コントロール内に表示するとします。TreeView は、図 9 のようになります。この実装には、2 つの HierarchicalDataTemplate と 1 つの DataTemplate を使用します。
図 9 TreeView にデータ階層全体を表示する
2 つの階層テンプレートは、Customer オブジェクトと Order オブジェクトを表示します。OrderDetail オブジェクトには子アイテムがないので、非階層型の DataTemplate でレンダリングできます。Customers は TreeView のルート レベルに含まれるデータ オブジェクトであり、TreeView の ItemTemplate プロパティでは Customer 型のオブジェクトのテンプレートを使用します。図 10 の XAML は、このパズルのすべてのピースがどのように組み合わされるかを示しています。
図 10 TreeView 表示の背後にある XAML
<Grid>
  <Grid.DataContext>
    <!--
    This sets the DataContext of the UI
    to a Customers returned by calling
    the static CreateCustomers method.
    -->
    <ObjectDataProvider
      xmlns:local="clr-namespace:VariousBindingExamples"
      ObjectType="{x:Type local:Customer}"
      MethodName="CreateCustomers"
      />
  </Grid.DataContext>

  <Grid.Resources>
    <!-- ORDER DETAIL TEMPLATE -->
    <DataTemplate x:Key="OrderDetailTemplate">
      <TextBlock>
        <Run>Product:</Run>
        <TextBlock Text="{Binding Path=Product}" />
        <Run>(</Run>
        <TextBlock Text="{Binding Path=Quantity}" />
        <Run>)</Run>
      </TextBlock>
    </DataTemplate>

    <!-- ORDER TEMPLATE -->
    <HierarchicalDataTemplate
      x:Key="OrderTemplate"
      ItemsSource="{Binding Path=OrderDetails}"
      ItemTemplate="{StaticResource OrderDetailTemplate}"
      >
      <TextBlock Text="{Binding Path=Desc}" />
    </HierarchicalDataTemplate>

    <!-- CUSTOMER TEMPLATE -->
    <HierarchicalDataTemplate
      x:Key="CustomerTemplate"
      ItemsSource="{Binding Path=Orders}"
      ItemTemplate="{StaticResource OrderTemplate}"
      >
      <TextBlock Text="{Binding Path=Name}" />
    </HierarchicalDataTemplate>
  </Grid.Resources>

  <TreeView
    ItemsSource="{Binding Path=.}"
    ItemTemplate="{StaticResource CustomerTemplate}"
    />

</Grid>
TreeView を含む Grid の DataContext に Customer オブジェクトのコレクションを割り当てます。これは、XAML からメソッドを呼び出す便利な方法である ObjectDataProvider を使用して XAML で行うことができます。DataContext は要素ツリーの下位へと継承されるので、TreeView の DataContext は、その一連の Customer オブジェクトを参照します。そのため、その ItemsSource プロパティには "{Binding Path=.}" のバインドを指定できます。これは、ItemsSource プロパティが TreeView の DataContext にバインドされることを示す方法です。
TreeView の ItemTemplate プロパティを割り当てなかった場合、TreeView は最上位レベルの Customer オブジェクトだけを表示します。WPF は Customer のレンダリング方法に関する情報を持っていないので、各 Customer に対して ToString を呼び出し、各アイテムにそのテキストを表示します。各 Customer に Order オブジェクトの一覧が関連付けられていること、および各 Order に OrderDetail オブジェクトの一覧があることを調べる方法はありません。データ スキーマが魔法のように WPF に認識される方法はないので、WPF に対してスキーマを説明し、WPF がデータ構造を正しくレンダリングできるようにする必要があります。
データの構造と外観を WPF に対して説明するには、HierarchicalDataTemplate が役に立ちます。このデモで使用するテンプレートに定義されているビジュアル要素ツリーはごく簡単なものであり、その大部分は少量のテキストを含む TextBlock です。より複雑なアプリケーションでは、テンプレートに対話的な回転 3D モデル、イメージ、ベクタ グラフィックスの描画、複雑な UserControl、または基礎になるデータ オブジェクトをビジュアル化するためのその他の WPF コンテンツを含めることができます。
重要なのは、テンプレートが宣言される順序に注意することです。テンプレートを参照する前に、StaticResource 拡張機能を使用してそのテンプレートを宣言する必要があります。これは XAML リーダーによって課される要件であり、テンプレートだけでなくすべてのリソースに適用されます。
代わりに DynamicResource 拡張機能を使用してテンプレートを参照できます。その場合、テンプレート宣言の辞書式の順序は意味を持ちません。ただし、StaticResource 参照ではなく DynamicResource 参照を使用すると、リソース システムの変更が監視されるので、実行時にオーバーヘッドが発生します。実行時にテンプレートを置換することはないので、そのオーバーヘッドは必要のないものです。したがって、StaticResource 参照を使用し、テンプレート宣言の順序を適切に並べることをお勧めします。

ユーザー入力を処理する
ほとんどのプログラムでは、データの表示は処理の半分にすぎません。残りの大きな課題は、ユーザーによって入力されたデータの分析、受け入れ、および拒否です。すべてのユーザーが常に論理的で正確なデータを入力する理想的な世界では、これは簡単なタスクです。しかし、現実の世界では必ずしもそうはいきません。実際のユーザーは、入力ミスをし、必要な値の入力を忘れ、間違った場所に値を入力します。また、削除してはならないレコードを削除し、追加してはならないレコードを追加します。そして一般に、人知の及ぶ限りマーフィーの法則に従います。
ユーザーによる避けられない誤りと悪意のある入力に対処するのは、私たち開発者と設計者の仕事です。WPF バインド インフラストラクチャは、入力検証をサポートしています。この記事の次のいくつかのセクションでは、WPF の検証サポートを使用する方法と、検証エラー メッセージをユーザーに表示する方法を説明します。

ValidationRules による入力検証
Microsoft® .NET Framework 3.0 の一部であった WPF の最初のバージョンには、限られた入力検証サポートしかありませんでした。Binding クラスには ValidationRules プロパティがあり、任意の数の ValidationRule 派生クラスを格納できます。これらの各ルールには、バインドされた値が有効かどうかをテストするロジックを含めることができます。
当時、WPF には ExceptionValidationRule という 1 つの ValidationRule subclass だけが付属していました。開発者は、バインドの ValidationRules にそのルールを追加でき、データ ソースに対して行われた更新中にスローされた例外をキャッチできたので、UI で例外のエラー メッセージを表示できました。優れたユーザー エクスペリエンスの基盤は、ユーザーに技術的な詳細を不必要に知らせないようにすることだと考えた場合、入力検証に対するこのアプローチが便利であるかどうかについては議論の余地があります。データ解析例外のエラー メッセージは一般にほとんどのユーザーにとって技術的すぎますが、本題からそれるのでその問題は取り上げません。
たとえば次に示す単純な Era クラスのような、年代を表すクラスがあるとします。
public class Era
{
    public DateTime StartDate { get; set; }
    public TimeSpan Duration { get; set; }
}
ユーザーが開始日と年代を編集できるようにする場合は、2 つのテキストボックス コントロールを使用し、Text プロパティを Era インスタンスのプロパティにバインドできます。ユーザーがどのようなテキストをテキストボックスに入力するかわからないので、入力テキストを DateTime または TimeSpan のインスタンスに変換できるかどうは不確実です。このシナリオでは、ExceptionValidationRule を使用してデータ変換エラーを報告し、ユーザー インターフェイスに変換エラーを表示できます。図 11 の XAML は、このタスクを実現する方法を示しています。
図 11 年代を表す単純なクラス
<!-- START DATE -->
<TextBlock Grid.Row="0">Start Date:</TextBlock>
<TextBox Grid.Row="1">
  <TextBox.Text>
    <Binding Path="StartDate" UpdateSourceTrigger="PropertyChanged">
      <Binding.ValidationRules>
        <ExceptionValidationRule />
      </Binding.ValidationRules>
    </Binding>
  </TextBox.Text>
</TextBox>

<!-- DURATION -->
<TextBlock Grid.Row="2">Duration:</TextBlock>
<TextBox
  Grid.Row="3"
  Text="{Binding
         Path=Duration,
         UpdateSourceTrigger=PropertyChanged,
         ValidatesOnExceptions=True}"
  />
この 2 つのテキストボックスは、ExceptionValidationRule を XAML のバインドの ValidationRules に追加できる 2 つの方法を示しています。Start Date テキストボックスでは、冗長なプロパティ要素構文を使用してルールを明示的に追加します。Duration テキストボックスでは略式の構文を使用し、バインドの ValidatesOnExceptions プロパティを true に設定します。どちらのバインドでも UpdateSourceTrigger プロパティが PropertyChanged に設定されているので、コントロールがフォーカスを失うまで待つのではなく、テキストボックスの Text プロパティに新しい値が指定されるたびに入力が検証されます。プログラムのスクリーンショットを図 12 に示します。
図 12 ExceptionValidationRule で検証エラーを表示する

検証エラーを表示する
図 13 に示すように、Duration テキストボックスには無効な値が含まれています。そのテキストボックスに含まれる文字列を TimeSpan インスタンスに変換することはできません。テキストボックスのツールヒントにはエラー メッセージが表示され、小さな赤いエラー アイコンがコントロールの右側に表示されます。この動作は自動的には行われませんが、実装とカスタマイズは簡単です。
図 13 ユーザーに入力検証エラーを表示する
<!--
The template which renders a TextBox
when it contains invalid data.
-->
<ControlTemplate x:Key="TextBoxErrorTemplate">
  <DockPanel>
    <Ellipse
      DockPanel.Dock="Right"
      Margin="2,0"
      ToolTip="Contains invalid data"
      Width="10" Height="10"
      >
      <Ellipse.Fill>
        <LinearGradientBrush>
          <GradientStop Color="#11FF1111" Offset="0" />
          <GradientStop Color="#FFFF0000" Offset="1" />
        </LinearGradientBrush>
      </Ellipse.Fill>
    </Ellipse>
    <!--
    This placeholder occupies where the TextBox will appear.
    -->
    <AdornedElementPlaceholder />
  </DockPanel>
</ControlTemplate>

<!--
The Style applied to both TextBox controls in the UI.
-->
<Style TargetType="TextBox">
  <Setter Property="Margin" Value="4,4,10,4" />
  <Setter
    Property="Validation.ErrorTemplate"
    Value="{StaticResource TextBoxErrorTemplate}"
    />
  <Style.Triggers>
    <Trigger Property="Validation.HasError" Value="True">
      <Setter Property="ToolTip">
        <Setter.Value>
          <Binding
            Path="(Validation.Errors)[0].ErrorContent"
            RelativeSource="{x:Static RelativeSource.Self}"
            />
        </Setter.Value>
      </Setter>
    </Trigger>
  </Style.Triggers>
</Style>
静的な Validation クラスは、いくつかのアタッチされたプロパティと静的メソッドを使用して、コントロールとそれに含まれる検証エラーの関係を形成します。これらのアタッチされたプロパティを XAML で参照して、ユーザー インターフェイスが入力検証エラーをユーザーに示す方法に関するマークアップのみの記述を作成できます。図 13 の XAML は、前の例の 2 つのテキストボックス コントロールに対して入力エラー メッセージをレンダリングする方法を説明します。
図 13 の Style は、UI のテキストボックスのすべてのインスタンスを対象とします。テキストボックスに 3 つの設定を適用します。最初の Setter は、テキストボックスの Margin プロパティに影響します。Margin プロパティの値は、右側にエラー アイコンを表示できるだけの十分なスペースが確保されるように設定されます。
Style の次の Setter は、テキストボックスに無効なデータが含まれる場合にそのテキストボックスのレンダリングに使用される ControlTemplate を割り当てます。アタッチされた Validation.ErrorTemplate プロパティを、Style の上で宣言された ControlTemplate に設定します。テキストボックスに 1 つ以上の検証エラーがあることが Validation クラスによって報告されると、テキストボックスはそのテンプレートに従ってレンダリングされます。ここで、図 12 に示すような赤いエラー アイコンが表示されます。
Style には、テキストボックスのアタッチされた Validation.HasError プロパティを監視する Trigger も含まれます。Validation クラスがテキストボックスのアタッチされた HasError プロパティを true に設定すると、Style の Trigger がツールヒントをアクティブにし、テキストボックスに割り当てます。ツールヒントのコンテンツは、テキストボックスのテキストをソース プロパティのデータ型のインスタンスに解析しようとしたときにスローされる例外のエラー メッセージにバインドされます。

IDataErrorInfo による入力検証
Microsoft .NET Framework 3.5 の導入によって、WPF による入力検証のサポートが大幅に向上しました。ValidationRule のアプローチは単純なアプリケーションには有効ですが、実際のアプリケーションでは実社会のデータとビジネス ルールの複雑さを扱います。ビジネス ルールを ValidationRule オブジェクトにエンコードすると、そのコードが WPF プラットフォームに拘束されるだけでなく、ビジネス ロジックをそれが属する場所 (ビジネス オブジェクト) に存在させることもできません。
多くのアプリケーションにはビジネス層があります。この層には、ビジネス ルールの処理の複雑さが一連のビジネス オブジェクトに含まれています。Microsoft .NET Framework 3.5 を対象にコンパイルする場合は、IDataErrorInfo インターフェイスを使用して、ビジネス オブジェクトが有効な状態かどうかを WPF で確認できます。これにより、ビジネス層とは別のオブジェクトにビジネス ロジックを配置する必要がなくなり、UI プラットフォームに依存しないビジネス オブジェクトを作成できます。IDataErrorInfo インターフェイスは何年も前からあるものなので、これによってレガシ Windows フォームまたは ASP.NET アプリケーションのビジネス オブジェクトの再利用もはるかに簡単になります。
ユーザーのテキスト入力がソース プロパティのデータ型に変換可能であることを確認するだけでなく、年代を検証する必要があるとします。まだ存在していない年代については情報がないので、年代の開始日として将来の日付を指定できないようにすると有効です。また、年代が少なくとも 1 ミリ秒は続いていることを要件にするのも有効です。
これらのような種類のルールは、両方ともドメイン ルールの例であるという点で、ビジネス ロジックの汎用的な概念に似ています。状態を格納するオブジェクト (ドメイン オブジェクト) にドメイン ルールを実装することをお勧めします。図 14 のコードは、IDataErrorInfo インターフェイスを使用して検証エラー メッセージを公開する SmartEra クラスを示しています。
図 14 検証エラー メッセージを公開する IDataErrorInfo
public class SmartEra
    : System.ComponentModel.IDataErrorInfo
{
    public DateTime StartDate { get; set; }
    public TimeSpan Duration { get; set; }

    #region IDataErrorInfo Members

    public string Error
    {
        get { return null; }
    }

    public string this[string property]
    {
        get
        {
            string msg = null;
            switch (property)
            {
                case "StartDate":
                    if (DateTime.Now < this.StartDate)
                        msg = "Start date must be in the past.";
                    break;

                case "Duration":
                    if (this.Duration.Ticks == 0)
                        msg = "An era must have a duration.";
                    break;

                default:
                    throw new ArgumentException(
                        "Unrecognized property: " + property);
            }
            return msg;
        }
    }

    #endregion // IDataErrorInfo Members
}
WPF ユーザー インターフェイスから SmartEra クラスの検証サポートを使用するのは非常に簡単です。必要なのは、バインド先のオブジェクトで IDataErrorInfo インターフェイスを受け入れる必要があることをバインドに指示することだけです。図 15 に示すように、これは 2 つの方法のいずれかで行うことができます。
図 15 検証ロジックを使用する
<!-- START DATE -->
<TextBlock Grid.Row="0">Start Date:</TextBlock>
<TextBox Grid.Row="1">
  <TextBox.Text>
    <Binding Path="StartDate" UpdateSourceTrigger="PropertyChanged">
      <Binding.ValidationRules>
        <ExceptionValidationRule />
        <DataErrorValidationRule />
      </Binding.ValidationRules>
    </Binding>
  </TextBox.Text>
</TextBox>

<!-- DURATION -->
<TextBlock Grid.Row="2">Duration:</TextBlock>
<TextBox
  Grid.Row="3"
  Text="{Binding
         Path=Duration,
         UpdateSourceTrigger=PropertyChanged,
         ValidatesOnDataErrors=True,
         ValidatesOnExceptions=True}"
  />
ExceptionValidationRule を明示的または暗黙的にバインドの ValidationRules コレクションに追加する方法と同様に、DataErrorValidationRule をバインドの ValidationRules に直接追加するか、ValidatesOnDataErrors プロパティを true に設定するだけで済みます。どちらのアプローチでも実質的な効果は同じであり、バインド システムはデータ ソースの IDataErrorInfo インターフェイスに検証エラーを照会します。

まとめ
多くの開発者が、WPF について気に入っている機能はデータ バインドの豊富なサポートであると言うのには理由があります。WPF でのバインドは高機能で幅広く使用されており、多くのソフトウェア開発者は、データとユーザー インターフェイスとの関係について考えを改める必要があります。WPF の中核機能が多数連携して、テンプレート、スタイル、アタッチされたプロパティなどの複雑なデータ バインド シナリオをサポートします。
比較的少ない行の XAML によって、階層データ構造を表示する方法およびユーザー入力を検証する方法について開発者の意図を表現できます。高度な環境では、プログラムによるアクセスを通してバインド システムの機能を最大限に活用できます。これほど強力なインフラストラクチャが使用できるようになったことで、最新のビジネス アプリケーションを作成する開発者にとっては、優れたユーザー エクスペリエンスと説得力のあるデータ ビジュアライゼーションの実現という永続的な目標に、ついに手が届くところまで来たと言えます。

Josh Smith は、WPF を使用して優れたユーザー エクスペリエンスを実現することに情熱を注いでおり、WPF コミュニティにおける活動に対して Microsoft MVP の称号を授与されました。Josh は、Experience Design Group の Infragistics に勤めています。コンピュータの前にいないときは、ピアノを弾いたり、歴史書を読んだり、ガールフレンドとニューヨーク シティを散策したりしています。Josh は joshsmithonwpf.wordpress.com でブログを公開しています。


トランザクション
データを失うことなく障害に対処するスケーラブルなシステムを構築する
Udi Dahan

この記事では、次の内容について説明します。
  • 永続性のあるメッセージング
  • トランザクションとシステムの一貫性
  • エラー キューを使用した問題の処理
  • メッセージのサイズとタイミング
 この記事では、次のテクノロジを使用しています。
WCF、MSMQ
目次
HTTP とメッセージの損失
永続性のあるメッセージング
システムの一貫性
トランザクション メッセージング
一時的な条件
逆シリアル化のエラー
エラー キュー内のメッセージ
時間とメッセージの損失
TimetoBeReceived
コール スタックの問題
サイズの大きいメッセージ
サイズの大きいメッセージをサイズの小さいメッセージに分割する
アイデムポテントなメッセージング
実行時間の長いプロセス
失敗から学ぶ
分散システムの設計は、かつてないほど簡単になりました。バージョンを重ねるごとに高まった CLR の機能、Visual Studio® の生産性、および Windows® Communication Foundation (WCF) のようなフレームワークが持つ細かい制御能力を入手した開発者は、スケーラブルなシステムの構築に必要なあらゆるツールを手にしていると言えます。ただし、それだけでは十分ではありません。
大規模な分散システムに取り組んでいるとき、私のチームは、データを失うことなく障害シナリオに対処する堅牢なシステムの開発が決して簡単な作業ではないことを実感しました。ただし、それはツールセットに欠けているものがあるからではありません。拡張性と堅牢性を兼ね備えたシステムを構築するには、非常に特殊なパターンでこれらのツールを使用する必要があるのです。

HTTP とメッセージの損失
チームが最初に直面した問題は、メッセージの損失でした。初めてそのシステムを設計したとき、私たちは、WCF ベースのサービスのトランスポートとして HTTP を使用することに決めていました。メッセージ処理の一環として、私たちのサービスはデータを頻繁にデータベースに書き込みます。ごく簡単に言うと、多くのシステムはこのように設計されます。
システムをステージング環境に移す前に、厳しいストレス テストが行われました。システムを 1 週間、高い負荷を加えた状態で実行し続けたのです。ストレス テストの結果、システムでメッセージが失われることが判明しました。
この状況の分析中に、私たちは主に注文情報を含むメッセージが失われていることを突き止めました。システムに対する負荷のほとんどは注文情報に関連するものであったため、これは意外ではありませんでした。ただし、このメッセージの損失が特定の 10 分間に集中していることには驚きました。それ以降は、システムの動作に問題はありませんでした。
各種ログ ファイルを確認したところ、その週、Windows の "緊急" の修正プログラムが公開されており、ステージング ラボのサーバーがその修正プログラムを自動的にインストールした後、再起動していたことがわかりました。各種ログ ファイルを確認したところ、その週に Windows の "緊急" の修正プログラムが公開されており、ステージング ラボのサーバーが修正プログラムの自動インストール後に再起動されていたことがわかりました。サーバーがときどき自動的に再起動するという状況は、その影響の大きさを考えると、無視できる問題ではありませんでした。システムは、何年もの間動作し続ける必要があり、その間にサーバーで何度も行われるアップグレードおよび再起動に対応できなければなりません。
HTTP 経由のメッセージを処理するとき、サービスはデータベースに対してトランザクションを開始し、データの書き込みを試行し、トランザクションをコミットします。通常の状況であれば、この処理は成功するでしょう。メッセージを処理するサーバーがトランザクション中に再起動した場合、データベースはトランザクションのタイムアウトを検出し、その変更をロールバックすることで一貫した状態を保ちます。ところが、サーバーの再起動が行われると、サーバーのネットワーク処理スタックにもメモリにも元のメッセージのデータがありません。つまり、メッセージは失われてしまいました。
注文処理システムの場合、これはお金を失うことと同義です。このシナリオが航空管制システムで発生したときの悪夢のような事態、つまり "航空機 A と航空機 B が衝突進路上にある" というメッセージが失われるような事態は、だれも想像したくはないでしょう。

永続性のあるメッセージング
HTTP から永続性のあるトランスポートに変更することを決めた後、私たちは Microsoft® メッセージ キュー (MSMQ) と SQL Server® Service Broker のどちらかを選ばなければなりませんでした。MSMQ に比べて Service Broker が優れている点は、メッセージングをデータベースに保管でき、メッセージング層とデータベース間での分散トランザクションを回避できるため、パフォーマンスが高くなる可能性があるという点でした。Service Broker に比べて MSMQ が優れていた点としては、Windows 上で直接 (無料で) 使用できること、WCF との緊密な統合、既にオーバーロードしているデータベースを介さずにサーバー間でピアツーピア通信を実行できることなどが挙げられます。最終的に、私たちは各サーバーでプライベート MSMQ キューを使用することにしました。
MSMQ を使用しても、すべてのメッセージが自動的にディスクに書き込まれるわけではありません。書き込みにはパフォーマンス オーバーヘッドが伴うため、これは好ましい特性です。メッセージを永続的なものとして定義するには、MSMQ を使用する際に、Message クラスの Recoverable プロパティを直接 true に設定するか、NetMsmqBinding を使用してください (このクラスは、Durable プロパティが既定で true に設定されているため)。
ソリューション全体で NetMsmqBinding を使用する場合は、永続メッセージングを必要としないメッセージは別のエンドポイントに転送されるようにする必要があります。それらのエンドポイントを非トランザクション キューとして構成し、<netMsmqBinding> セクションの Durable プロパティを false に設定します。
障害が発生した際にキューでメッセージをロールバックするには、トランザクション内で Receive 操作が行われる必要があります。これは、OperationBehaviorAttribute の TransactionScopeRequired プロパティを使用するだけで実現できます。メッセージをキューから取り出すために使用されるトランザクションは、キューに登録されたトランスポートと共に、メッセージを処理するために使用されます。

システムの一貫性
メッセージの処理の結果、サービスは 1 つ以上のメッセージで応答したり、それ以外の任意の数のメッセージを他のシステムに送信したりできます。サービスの任意のポイントでメッセージングを通じたコードの通信を実現できれば、非常に有益な柔軟性を得ることができます。ただし、これを HTTP や TCP などのトランザクション非対応のテクノロジで行うのは危険です。
メッセージの処理に関連するトランザクションは、データベースのデッドロックの結果としてロールバックされることがあります。トランザクションがロールバックされると、他のシステムに送信した情報が適正な内容ではなくなる可能性があります。図 1 のシナリオについて考えてみましょう。複数のスレッドが同じテーブルの情報を処理するデータベースでは、デッドロックが発生するおそれがあります。あるスレッドが正常にテーブル 1 をロックし、テーブル 2 のロックを試みるとします。それと同時に、別のスレッドが正常にテーブル 2 をロックし、テーブル 1 のロックを試みるとします。データベースはこのとき発生するデッドロックを検出し、一方のスレッドを犠牲にしてそのトランザクションを中止します。
図 1 データベースのデッドロックが発生したトランザクション (クリックすると拡大画像が表示されます)
ここでの問題点は、ロールバックされたテーブル 1 のデータに対する変更をシステム A に通知したことです。これにより、システム A がその内部データを変更し、誤った情報をさらに伝達する可能性があるので、最終的にはグローバルな不整合が生じるおそれがあります。
注文処理シナリオでは、処理は次のような流れになりました。
  • 発注書が到着する。
  • 発注書のテーブルを更新する (ステータスを受信する)。
  • 保留中の注文を倉庫システムに通知する。
  • 顧客の情報を更新するためにカスタマ リレーションシップ マネジメント (CRM) データベースにアクセスする。
  • CRM データベースのデッドロックが検出される。
  • トランザクションがロールバックされる。
トランザクションが再試行されたとき、まだ支払いが行われていなかったため、顧客データにはその顧客に対して処理する予定の取引がないことが示されていました。そのため、CRM データベースと同様に、注文システムは一貫性のある状態に保たれました。
ところが、倉庫システムには他のデータベースのロールバックが通知されていませんでした。なぜこのようなことが起きたのでしょうか。結局、担当者が電話をかけて注文がどうなっているかを確認するまで、冷蔵設備から取り出された商品が 1 時間以上も発送センターに放置されていました。商品はその時点で販売できなくなり、在庫を補充するのに時間がかかったため、他のいくつかの注文品を届けるのが何時間も遅れました。
この 1 件のミスにかかったコストにより、その週の収益は 30% 低下しましたが、こうした目立たない技術的な問題がコア ビジネス並みに注目されるようになりました。次のセクションで説明するように、MSMQ に移行したことで、チームで開発作業を行うことなくこれらの問題を解決できました。

トランザクション メッセージング
MSMQ を使用してメッセージをキューに送信した場合、メソッド呼び出しが返されたときにはメッセージは送信されません。これは、HTTP や他の接続テクノロジの動作とは大きく異なります。MSMQ を使用すると、メッセージは送信される前に、同じコンピュータ上の発信キューにローカルに格納されます。メッセージがトランザクションのコンテキストで送信された場合、メッセージはトランザクションがコミットされて初めてキューから出され、MSMQ で実際に送信できるようになります。
前の例では、注文システムが新しい注文を知らせるメッセージを倉庫システムに送信しても、そのメッセージはすぐには送信されませんでした。メッセージは、メッセージ処理トランザクション全体がコミットされて初めて、MSMQ により倉庫システムに送信されました。データベース デッドロックの結果、またはその他の理由でトランザクションが中止された場合、倉庫システム宛てのメッセージは MSMQ 発信キューから削除されます。つまり、全体としてのシステムの一貫性が失われるのを防ぐためには、トランザクション メッセージングが不可欠なのです。
トランザクション メッセージングでは、メッセージの送信側とそのメッセージを処理するサービスがトランザクションを共有するわけではないことに注意してください。メッセージが処理を行うサービスに到着したという事実は、送信側で別のトランザクションが正常にコミットされたことを表しています。ただし、トランザクションがコミットされたという事実は、受信側のサービスがそのメッセージを処理する際に発生することについては、何も示していません。
メッセージを受信するサービスによっては、そのメッセージの処理により例外が発生することがあります。この例外については数多くの根本原因が考えられますが、さまざまな原因の相違点とその処理方法の把握することが重要です。例外がメッセージの処理過程で 1 回か 2 回だけ発生するケースもあれば、サービスが何度そのメッセージの処理を試みても必ず失敗に終わるようなケースもあります。
後者のグループに属していることが明らかになったメッセージは有害メッセージと呼ばれ、特別な処理が必要になります。さまざまな根本原因を見ていく過程で、単一のシンプルなソリューションでそれらすべての原因に効果的に対処できることがおわかりになるでしょう。

一時的な条件
普段は正常に処理される適切な形式のメッセージが、データベースのデッドロックが発生した場合や、他のトランザクションの処理のためにデータベース接続プールのサイズが限界に達している場合などに、例外を引き起こすことがあります。これらすべてのシナリオに共通しているのは、一時的な環境条件で例外が発生しているということです。
例外がトランザクション スコープでバブルアップしたときのトランザクション メッセージングの既定の動作は、すべての処理をロールバックするというものです。これが実行されると、元のメッセージはキューに戻ります。その時点で、スレッド (おそらく、例外の生じた同じスレッド) はもう一度メッセージの処理を行うことができます。トランザクションのロールバックには少し時間がかかるため、以前の環境条件が変化し、メッセージの処理をトランザクションで正常に完了できる可能性が高くなります。
メッセージの処理に失敗するケースのうち見過ごされがちなのは、単にサービスによって使用されるデータベースが使用不可能になっている状態です。すべてのデータベースが 100% の可用性を備えているわけではありません。データベースを使用するコードの観点からすると、この問題は一時的なものとは言えません。データベースの使用不可能な状態がどのくらい続くかがわからないためです。単純にメッセージをロールバックし、もう一度処理しても、おそらく同じ例外が発生します。
メッセージ自体は完全に有効で、非常に重要なデータ (逃すわけにはいかない 100 万ドルの注文など) が含まれている可能性もあります。メッセージを処理のロールバック ループに入れたままにしておくのもメッセージを保つ方法の 1 つですが、別の問題が発生します。
サービス ロジックが複数のデータベースを使用するケース、または複数のテーブルを使用するケースを考えてみましょう。たとえば、あるデータベースが使用不可能な状態で、別のデータベースが使用可能であるというような場合があります。また、あるテーブルをバックアップしたデータ ファイルが、ダウンしているリモート サーバーに格納されている一方で、別のテーブルをバックアップしたファイルが利用可能な場合もあります。ここで、タイプ A のメッセージの処理にはテーブル A だけが必要で、タイプ B のメッセージの処理にはテーブル B だけが必要であると考えてみましょう。テーブル B (またはデータベース B) が使用不可能になると、どのような事態が発生するでしょうか。
タイプ A のメッセージは正常に処理されますが、タイプ B のメッセージはロールバックされ続けます。メッセージを処理するスレッドよりもサービスの入力キュー内にあるタイプ B のメッセージの方が多くなるまで、この状態は続きます。その時点で、すべてのリソースがオンラインに戻るまで、サービスはタイプ A のメッセージに対しても実質的に使用不可能になります。つまり、すべてのリソースが使用可能な状態でなければ、サービスは機能しません。これはシステムの可用性という観点からすると、気がかりな (コストのかかる) 特性です。
メッセージの処理が同じように何度も失敗したとき、サービスで実行できる最善の処理は、そのメッセージを入力キューから別のキュー (ここではエラー キューと呼びます) に移すことです。ただし、エラー キューと他のキューの間には本質的な違いはありません。このシナリオでは、あるリソースがオフラインになると、タイプ B のメッセージはロールバックが n 回実行された後でエラー キューに移されます。一方、タイプ A のメッセージは引き続き正常に処理されます。このようにリソースが使用不可能になることでサービスの待ち時間が長くなり、スループットが低下する可能性はありますが、サービスは引き続き利用できます。

逆シリアル化のエラー
多数のバイトがエンドポイントに到着すると、サービスの基になっているテクノロジは、それらのバイトを標準のオブジェクト、つまりメッセージに変換しようとします。この逆シリアル化処理は、さまざまな理由で失敗することがあります。その理由としては、XML を通じてテクノロジに作成を指示するデータ型がそのエンドポイントで展開されていないことや、そのデータ型の不適切なバージョンが展開されていることなどが考えられます。より一般的なのは、既存のクライアントが最近アップグレードされたサービスのエンドポイントにメッセージを送信したが、新しいバージョンのデータ型に古いバージョンとの互換性がないというケースです。
問題なのは、サービスが受信メッセージを理解できていない場合も、そのメッセージには重要なデータが含まている可能性があるために破棄できないようなケースです。同時に、そのメッセージでサービスの入力キューをいっぱいにしてはならない場合もあります。
この場合のソリューションは、データベースが使用不可能な場合に使用するソリューションと非常によく似ています。単純に、そのメッセージを別のキュー (まったく同じエラー キューの場合もあります) に移してください。このケースで若干異なるのは、このメッセージの処理の結果がどうなるかが明確であるという点です。つまり、常に例外が発生します。そのため、メッセージをエラー キューに移す前に、n 回ロールバックされるのを待つ必要はありません。逆シリアル化できないメッセージは、エラー キューに直接移動する必要があります。

エラー キュー内のメッセージ
前に説明したシナリオからは、多くの状況でさまざまなメッセージを有害メッセージと判断できることがおわかりになるでしょう。これらのメッセージの中には、破棄するのが一番な本当に不要なものもありますが、ミスなくその判断を下すには、人間のオペレータが確認するしかありません。
逆シリアル化の例外が発生した場合、オペレータは、旧バージョンのソフトウェアを実行しているエンドポイントにメッセージをルーティングするか、メッセージの形式を新しいバージョンに変換してそれを入力キューに戻すことができます。
オペレータは、データベースがオフラインになったことを把握できればよいのですが、1 つのデータ ファイルが使用不能になり、わずか数個のテーブルがその影響を受けているようなケースもあります。このとき、エラー キュー内のメッセージと対応するログ ファイルに、こうした目立たない問題に関するフラグを設定することができます。これを基に、問題を迅速に特定して解決できます。
有害メッセージのトピックから学ぶことができるのは、受け取ったメッセージにサービスが応答するまでには、かなり時間がかかることがあるということです。実際に、時間に関する問題は、メッセージベースのシステムを設計するうえで主な懸案事項となっています。応答が遅い場合、返されない方がよいこともあります。

時間とメッセージの損失
永続性のあるトランザクション メッセージングを使用すれば、メッセージの損失を完全に抑えることができると思われるかもしれませんが、やはり現実はそれほど単純ではありません。
顧客への注文品の出荷を処理するため、注文システムが外部の運送会社と通信するような企業間のシナリオについて考えてみましょう。注文を受けるたびに、メッセージを出荷システムに送信する必要があります。一方のシステムがダウンしてもメッセージが失われないようにするため、メッセージのトランザクション処理と共に永続性のあるメッセージングを使用して、注文情報を転送するとします。
このシナリオの詳細を分析したところ、メッセージの損失は、回避不可能であるだけでなくコスト効率の高いものであることがわかりました。送信される注文ごとのメッセージの平均サイズが 1 MB であり、注文システムで 1 秒あたり 10 件の注文を処理しているとします (これは決して大きな数字ではありません)。このとき、出荷サーバーが使用不可能になると、これらのメッセージは発信キューに永続的に格納されます。I/O の使用量という点から見ると、1 秒あたり 10 MB、つまり 1 分あたり 600 MB のメッセージが蓄積されます。
この事態は、通信が 3 時間途絶えた後、初めて認識されました。その間、システムは 100 GB を超えるデータを、容量が部分的にいっぱいになった RAID 5 アレイ (3 つの 36 GB の SCSI ディスクで構成) に書き込もうとしていました。サーバーは、ハード ドライブへの書き込みができなくなると不安定になる傾向にあります。また、データのバックアップがほぼ不可能になります。
そのとき私たちは、3 時間分の未処理の注文については、問題の原因であるにもかかわらずあまり心配しませんでした。その一方、システムのバックアップにかかる 2 時間のために何十万ドルもの注文を逃しているという事実は、私たちに緊急の問題を解決し、再発を防がなければならないという管理上の大きなプレッシャーをかけることになりました。このことから得た教訓は、永続性のあるメッセージングは完全無欠な方法などではなく、また永続性に伴ってコストが発生する場合があるということです。
安定性を維持するために、サーバーでは宛先に正しく送信されなかったメッセージは破棄する必要があります。難しいのは、どのメッセージを保持する (長時間保持する) 必要があるのかを判断することでした。

TimetoBeReceived
注文システムは他の多数のシステムから要求を受信し、イベントをパブリッシュし、他のシステムからイベントを受信しました。これらの各処理の結果、メッセージは着信キューまたは発信キューに入れられます。一部のサービスでは、フィードに入れられるデータを使用していました。フィードでデータの有効性が保たれるのは、わずか数分間です。それらのメッセージをそれより長い期間保持しても、意味はありませんでした。あらゆるサーバー プロセス (IIS、SQL Server、その他の Windows サービス) の発生にかかる時間より長い場合も同じです。私たちのシステムで送出したデータ フィードについても、同じことが当てはまりました。メッセージのデータが有効な期間よりもサブスクライバがオフラインである時間の方が長い場合、そのメッセージを発信キューに入れても意味はありませんでした。
各サービスのコントラクトを構成するさまざまなメッセージを調べていくうちに、データが必ずしも有効期間を保持していないメッセージについては、サービスの応答時間がそのサービスレベル契約に準拠している必要があることに気が付きました。つまり、サービスが所定の時間 (メッセージがキューで待機している時間も含む) 内にメッセージの処理を終了できない場合、ビジネスの観点からすると、そのサービスは既に正常に機能していないことになります。
これが意味するのは、有効期間は各メッセージ タイプに対して定義できるということです。この有効期間には、送信側の発信キューに入れられている間に経過する時間、移動中の時間、受信側のキューに入れられている間に経過する時間を含める必要があります。MSMQ では、Message クラスの TimeToBeReceived プロパティを使用してこの値を定義できます (実装の詳細については、「TimeToBeReceived の内部」のリンクを参照してください)。
この時点で明らかだったのは、メッセージの損失は不可避の現実であるということです。クライアント側からは、メッセージの損失、応答の損失、単なるサーバー パフォーマンスの低下を区別することはできません。メッセージ処理の一部として、自分のサービスで他のサービスをクライアントとして使用している場合は、サービスレベル契約で指定されている適切な応答時間を実現するために、自分のサービスでそれ自体を保護する必要があります。そこで、私たちはタイムアウトを採用することにしました。応答が指定した時間内に到着しない場合に、最低限の対策として、要求が受信されており、応答は後ほど送信されることを示すメッセージをサービスで送信できるようにするためです。ユーザーと対話している場合、このメッセージには電子メールを使用してもかまいません。
TimeToBeReceived の内部
プログラマによるこれらの API の使用方法について調べ始めたころ、TimeToBeReceived の値はサービスのコントラクトの一部として定義され、フレームワークはメッセージの送信時にこのメタデータにアクセスできるため、プログラマはメッセージの送信のたびにこの値を指定しなくてもよいということがわかりました。
私たちは、メッセージ コントラクト (WCF データ コントラクトでも可) の一部としてクラスに追加できる TimeToBeReceivedAttribute を定義しました。SendMessage メソッドの実装の一部として、フレームワークは渡されるメッセージ オブジェクトの型にアクセスし、リフレクションを使用してその型のカスタム属性をループ処理します。データを属性から取得した後、フレームワークは、送信される MSMQ メッセージの TimeToBeReceived プロパティにそのデータを渡しました。

コール スタックの問題
技術的にもう 1 つ問題だったのは、図 2 に示すように、単にプロセスを管理するスレッドのコール スタックという形で外部システムとの対話の状態を保っていたという事実です。
図 2 外部システムとの対話 (クリックすると拡大画像が表示されます)
この技術的な問題は、やはりサーバーの再起動とかかわりがありました。サーバーが再起動したときに、スレッドが内部の CRM システムまたは外部の出荷パートナーのシステムからの応答を待機していた場合、その状態が失われるのです。つまり、システムが応答を最大で 5 分間待機することになっているとき、経過時間を把握しているサーバーが再起動し、その時間枠内で要求を処理する状態に戻った場合、システムはビジネス アクティビティ監視 (BAM) システムに通知する必要があることを認識できません。
元の要求が失われたか、破棄されたために応答が到着しない場合、この事態により不定期の注文は処理されなくなります。システムは他のシステムからの応答がないと処理を進めることができませんが、再起動のために応答を待機していたことを忘れてしまったのです。
今にして思えば、この状況は頻繁に生じていたわけではありません。しかしその時点では、私たちはこうした状況が生じていることにまったく気が付いていませんでした。BAM システムには何の問題も示されていませんでした。ただし、IT 部門に姿を見せた COO から、戦略パートナーからの急ぎの注文品が予定どおりに納品されておらず、だれもそのことに気付いてさえいないと叱責を受けたため、その問題を解決するためにもう一度リファクタリングを行うことになりました。
こうした種類のシステム間の統合プロセスを堅牢にするには、状態管理、メッセージング、および "時間" を単一の統合モデルに組み込む必要があることがわかります。

サイズの大きいメッセージ
実行時間の長いプロセスをソリューションに結び付ける方法を説明する前に、サイズの大きいメッセージがシステムのパフォーマンスと可用性に与える影響について理解する必要があります。
私たちが直面した最も厄介なパフォーマンス上の問題の 1 つは、最も収益の多い市場セグメントに属する戦略パートナーから受信したメッセージにその原因がありました。ほとんどの顧客から受信する発注書のサイズはかなり小さかったのですが、戦略パートナーからは多数のアイテムの注文を受信していました。しかも、各アイテムの種類が多岐にわたっていたのです。戦略パートナーによって頻繁に行われるアイテムのカスタマイズに対処する必要があったため、問題はさらに厄介になりました。これらの要因が組み合わされた結果、戦略パートナーから送られてくる XML メッセージのサイズは、容易に 20 MB ～ 50 MB に達していました。
この程度の分量の XML を逆シリアル化するだけで、Xeon コア プロセッサは最大 1 分間占有されます。私たちは、スキーマの検証をハードウェアの XML アプライアンスに肩代わりさせようとしましたが、全体的な問題の解決には至りませんでした。こうしたサイズが非常に大きなメッセージの処理には、対応するサイズの小さいメッセージが線的に増加した場合の処理よりも、桁違いに時間がかかりました。
TimeToProcess(BigMessage) >> N x TimeToProcess(SmallMessage) where
Size(BigMessage) == N x Size(SmallMessage)
私たちは常々、1 秒あたりにより多くのメッセージを処理できるようにスケール アップする能力の観点から、スケーラビリティについて考えていました。また、この能力についてテストを行っていました。ところが、サイズの大きいメッセージに対処するためのスケール アップは、はるかに難しい課題であることが明らかになりました。
ネットワークの輻輳がサイズの小さいメッセージに影響を及ぼすことはほとんどありませんが、サイズの大きいメッセージの場合、その影響を受ける可能性ははるかに高くなります。これは、永続性のあるメッセージングを使用しているときにこうしたデータをディスクに書き込む場合にも当てはまります。サイズの小さいメッセージを書き込むためにディスクの連続した空き領域を探すのは簡単ですが、ディスクの最適化を行わずにシステムをしばらくの間実行した後では、サイズの大きいメッセージを書き込むのにかなりの時間がかかることがあります。ユーザーは、コンピュータ間でサイズの大きいファイルをコピーする際に "99% 完了現象" に直面することがよくあります。サーバーも同じ問題に見舞われます。
こうした状況が続いている間、よりサイズの小さいメッセージも同様に影響を受けます。全体としての収益性から考えた場合はそれほど大きな問題ではありませんが、スループットの問題のために扱う注文の量が 10% 減るような事態は、最終的な収益に大打撃を与えます。そこで、サイズの大きい 1 つのメッセージを、それよりもサイズの小さい多数のメッセージに分割する何らかの手段が必要でした。

サイズの大きいメッセージをサイズの小さいメッセージに分割する
問題なのは、それらのメッセージ中のデータにビジネスの観点から固有の要件があるという点でした。発注番号は重複してはいけません。銀行システムを設計するとき、取引番号は連続している必要があります。並列処理には、テクノロジのレベルだけで対処することはできません。テクノロジを中心に、ビジネス ルールも設計する必要があります。
こうしたサイズの大きい発注データを分析し、戦略パートナーの開発チームと話し合った結果、これらのサイズの大きい発注書は時間をかけて段階的に作成され、完成した時点で送信されていることが判明しました。私たちが聞いた内容の中でも特に興味深いのは、こうしたサイズの大きいメッセージは送信側でも同様に問題になっており、複数の小さいメッセージに分割する方法を探すことに同じく関心を抱いているという点です。
問題なのは、現在のインターフェイスと処理ロジックが 1 つの発注番号を 1 つのメッセージに結び付けていることでした。その結び付きを壊すことのできるソリューションが見つかれば、他の問題も解決できることは明らかでした。
ある単純なソリューションが考案されましたが、そのソリューションは、これまでのステートレスなアーキテクチャをステートフルなアーキテクチャに転換するものでした。そのため、簡単に決定を下すわけにはいきません。ステートレスなアーキテクチャでは、すべての状態はデータベースに格納されます。構築しやすく、ツールのサポートを十分得ることができますが、このアーキテクチャではデータベースに対する負荷が大きくなります。
プロトタイプを構築して一連のストレス テストを行った結果、待ち時間、スループット、および全体的なリソース使用量が大きく改善することがわかったため、そのアーキテクチャで進めることにしました。
新しいアーキテクチャでは、パートナーは同じ発注番号を持つ複数のメッセージを私たちに送信できるようになりました。それらのメッセージには、既に送信済みのデータに対する変更データか、追加のデータだけが含まれています。発注書のすべてのデータが送信されると、パートナーのシステムは、現在のメッセージが発注書の最後のメッセージであることを示す Boolean フィールドを設定します。メッセージ コントラクトの点から見ると、変更はほんのわずか (フィールドの追加だけ) です。ただし、注文処理ロジックは根本的に変更する必要がありました。
通常の要求/応答セマンティクスの代わりに、私たちのサービスは "複数の要求/単一の応答" セマンティクスをサポートするようになりました。ロジックは、特定の発注書についていくつの要求を受信することになるかは把握していません。さらに、それらの要求は、発注書完了フィールドが true に設定された要求が到着するまでは処理されません。ただし、それらの要求のデータは、最後の要求が到着するまで保管されている必要があります。
私たちは、データベースに対する負荷をこれ以上増やさないために、この段階でデータのストレージを分離しました。この状態のデータ アクセス パターンを分析してみたところ、その時点ですべての発注書のレポートを端から端まで確認する必要はないことがわかりました。つまり、データを格納するためにリレーショナル データベースを選んだことは、トランザクションの一貫性の保証という観点からはやりすぎであり、"書き込みから読み出し" の割合が高いこの種類のロジックを処理するには時間がかかりすぎるということです。
分散キャッシュ ソリューションに移行したところ、パフォーマンスは大幅に改善されました (分散キャッシュ製品では、フォールト トレランスと高い可用性を確保するため、データは複数のサーバーのメモリで保持されます。また、これらの製品は、従来のデータベース テクノロジに代わる高パフォーマンスなテクノロジを備えています)。
注文処理コントラクトの静的な構造は大きく変わりませんでしたが、そのメッセージ交換パターンの動的な振る舞いは大きく変化しました。サービスは 1 つの発注書を複数のメッセージで受信できるようになったため、インフラストラクチャを決定するうえで、その強化されたプロトコルを活かすことができるようになりました。

アイデムポテントなメッセージング
戦略パートナーからの発注書に永続性のあるメッセージングを使用するという当初の決定に大きな影響を与えたのは、メッセージの損失を防ぐことができるという点です。ところが、私たちは、ディスクに書き込まれるメッセージに必ずしも 100% の保証がないことを事実と考えていました。ただし、新しいメッセージ プロトコルを使用することで、パートナーが私たちに発注書メッセージを送信したにもかかわらず妥当な時間内に応答がないとき、パートナーはもう一度同じメッセージを送信できるようになりました。
私たちは、アイデムポテントな (何度処理しても結果が同じになる) メッセージ コントラクトの作成にきちんと取り掛かっていたわけではありませんでしたが、そのメリットはごく短期間で実感できました (アイデムポテントなメッセージとは、サービスに複数回送信できるメッセージのことです。その結果のサービスの状態は、メッセージが 1 回だけ処理された場合と同じです)。永続性のあるメッセージングを使用すると、各メッセージはディスクに書き込まれます。高価でパフォーマンスの高いディスクでも、メッセージをメモリに格納する場合に比べると、処理がはるかに遅くなります。送信および処理するメッセージのサイズが小さいうえに、メッセージをディスクに書き込む必要がまったくなかったため、パフォーマンスの向上は 2 倍になりました。メッセージ ストレージのコストの削減は非常に重要です。
アイデムポテントなメッセージングの導入に伴って、さらに別の問題が発生します。アイデムポテントなコントラクトの設計が難しいのは言うまでもありませんが、クライアントには、サービスへのメッセージの送信以外の処理も求められるようになります。クライアントは、サーバーから応答が時間内に返されなかった場合に再度同じメッセージを送信できるように、そのエンドでタイマを管理する必要があります。つまり、サーバーにメッセージをステートフルに処理する必要があるだけでなく、クライアントにもメッセージをステートフルに送信する必要があるのです。

実行時間の長いプロセス
サイズの大きいメッセージを複数の小さいメッセージに分割する場合、さらに別のアーキテクチャ上の変化が生じます。それは、これまではシンプルだった要求/応答セマンティクスも実行時間の長いプロセスになるということです。実行時間の長いプロセスとは、複数のメッセージを処理するプロセスのことです。多くの場合、あるメッセージの状態は、次のメッセージを処理するロジックで利用できる必要があります。
既に説明したように、単一のメッセージの処理は単一のトランザクション作業単位であり、他のメッセージの送信だけでなく、ローカル状態の変更も伴います。実行時間の長いプロセスでは複数のメッセージを処理しますが、その処理それぞれがトランザクション作業単位ということになります。前に送信した要求に対する応答が到着すると、その応答は、それ自体の作業単位内で処理されます。
ただし、メッセージの処理間で実行時間の長いプロセスの状態をメモリで保持しておく必要はないため、その状態は可用性の高い何らかのストア (通常はデータベースですが、分散キャッシュの場合もあります) に保存されます。プロセス ID を含むメッセージがエンドポイントに到着すると、可用性の高いストアからデータが取得され、実行時間の長いプロセスがそのメッセージにディスパッチされます。
開発者を実行時間の長いプロセスの設計に向かわせるもう 1 つの一般的な要件は、応答時間を順守することと外部のシステムからの応答を確実に受け取ることです。既に説明したように、外部のシステムと通信する場合や、別のコンピュータ上のプロセスと通信する場合でさえも、問題が発生する可能性のある要素は数多くあります。コンピュータのダウン、プロセスの停止、メッセージの損失や破棄などが起きるかもしれません。
前に説明した、注文品を顧客に出荷するプロセスについて考えてみましょう。会社は複数の出荷パートナーと提携していますが、通常はあるパートナーを優先的に利用しているとします。その優先的に利用するパートナーのシステムがダウンしたり、パフォーマンスが低下したりする事態や、単に応答しなくなるような事態も考えられます。そのような場合には、最大 5 分間応答を待ち、応答が到着しない場合は別の出荷パートナーを選択するようにロジックで指示します。特定の製品の在庫がない場合は、注文品の一部だけを出荷し、在庫が補充されてから出荷プロセスに処理を続行させます。
System.Threading.Timer などの通常のメモリ内コンストラクトを使用してこれらのタイマを実装する際の問題点については、既に説明しました。それは、サーバーが再起動した際に、経過時間に関するデータが失われるという問題です。私たちが直面した課題は、可用性が高く、フォールト トレラントな方法で時間に関する情報を格納することでした。それに関連して、プロセスにどのような方法でタイムアウト イベントを通知するかという問題もあります。
タイムアウト イベントはシステムにおける他のビジネス イベントとよく似ており、ビジネス イベントはプロセスに到着するメッセージとして表されていることが判明したため、時間の問題を同様に処理するためのメッセージングを採用することにしました。実行時間の長いプロセスは、タイムアウト メッセージを Timeout Manager Service に送信します。このメッセージには、実行時間の長いプロセスの ID と、通知を実行する日時が含まれています。
Timeout Manager Service は、特定の間隔でポーリングを行うデータベースに格納するか、単にメッセージを永続性のあるキューに登録しておくことで、このデータを保持します。メッセージに指定されている時間が経過すると、Timeout Manager Service は、そのメッセージの送信元のエンドポイント (実行時間の長いプロセスのエンドポイント) にまったく同じメッセージを送信します。実行時間の長いプロセスは、他のメッセージと同じように、そのメッセージをそれ自体の作業単位内で処理します。タイムアウト メッセージの処理中、プロセスは、以前のすべての対話における状態を利用できます。
この出荷プロセスを調べてみると、タイムアウト メッセージを処理するロジックは、優先的に利用する出荷パートナーから応答を受け取っているかどうかをチェックすることがわかります。こうしたチェックを行うのは、タイムアウトが発生する直前にパートナーのシステムから応答を受け取っている可能性があるためです。これで、プロセスは残りの在庫が補充されるのを待機している状態になりました。この方法でのタイムアウト メッセージは、必ずしも何かに問題が生じていることを示すものではありません。むしろ、プロセスのウェイクアップ コールであると言えます。単一のプロセスで、複数のタイムアウト (各タイムアウトは応答時間やサービスレベル契約に関連するビジネス要件に対応) を使用することができます。
これらのタイムアウトは、データベースへの接続を試行したときや、HTTP Web サービスの呼び出しがタイムアウトしたときに発生するような技術的な現象ではないことを理解しておくことが重要です。これらのタイムアウトは、数日から数週間にも及ぶ可能性のあるビジネス イベントです。
対話における最も重要度の高い課題として時間に取り組み始めると、これまで重要ではなかった同期のブロッキング要求/応答セマンティクス自体がプロセスに変わりました。各対話には、待機する必要のある最長時間に応じて、最終的に何らかの制限が生じました。私たちは、シンプルな要求/応答の対話メカニズムを手に入れたと考えるたびに、「応答が返されるのに 2 日かかる場合はどうなるだろうか。それが 2 週間の場合はどうだろう。このシステムで、そのサービスレベル契約を遵守し続けることができるだろうか」と自問自答しました。その答えはいつも同じで、「時間は無視できない」というものでした。

失敗から学ぶ
信頼性の高いサービスの開発は簡単ではありません。私たちのチームが直面した問題は、一見、そのテクノロジが抱える短所のように見えるかもしれませんが、問題領域を再調査し、運用環境に置かれたシステムに対する苛酷な現実を考慮に入れることから解決策が導き出されることはよくあります。私たちはプロジェクトに繰り返し取り組んで、永続性のあるメッセージング テクノロジのテスト、作業単位に収まるトランザクション メッセージ処理の追加、バージョン管理の問題と有害メッセージへの対処、サイズの大きいメッセージの分割、そして実行時間の長いプロセスを使用した、時間への確実な対処を行いました。明らかになったことが 1 つあります。それは、基本的なパターンは同じだということです。
  • 要求を送信したからといって、必ず応答が返されるとは考えないでください。
  • 時間はビジネス イベントであり、他のメッセージと同様に処理できる他のイベントに類似しています。
  • あらゆるメッセージ (クライアントからの要求、別のシステムからの応答、パブリッシュされたメッセージ (以前サブスクライブしていたもの) の到着) の処理は、すべてのローカル アクティビティやメッセージの送信も伴う単一の作業単位です。
決定を行う各ポイントで役立ったのは、「これはどのように失敗するだろうか」と問いかけることでした。これにより、テクノロジや、ステートフルな対話に対応したサービス コントラクトの設計方法を選択する際に、適切な判断を下すことができるようになります。経験の積み重ねが適切な判断につながり、不適切な判断の積み重ねが経験を形作るのです。私たちは、以前の設計上の決定のために痛手を負ったからこそ、設計時の決定が実行時の動作に与える影響について、はっきり見通すことができるようになりました。

Udi Dahan は、ソフトウェア シンプリストです。また、MVP であり、WCF、WindowsWF、および "Oslo" を担当する接続型テクノロジのアドバイザでもあります。彼は、サービス指向でスケーラブルな、セキュリティ保護された .NET アーキテクチャ設計を専門としており、トレーニング、メンタリング、およびハイエンド アーキテクチャ コンサルティング サービスを提供しています。Udi の連絡先は Udi@UdiDahan.com です。


WCF P2P
ピア ネットワークでの状態共有の設計方法
Kevin Hoffman

この記事では、次の内容について説明します。
  • 固定状態サーバー
  • 選択状態サーバー
  • サーバーレス データ ブラスト
  • 最近ピア同期
 この記事では、次のテクノロジを使用しています。
WCF

コードのダウンロード : P2P2008_07.exe (171 KB)
オンラインでのコードの参照

目次
固定状態サーバー
一時的選択状態サーバー
純粋なピア サーバーレス データ ブラスト
最近ピア同期
WeSpend サンプル アプリケーション
次のステップ
近年、ピア ネットワーク経由で通信するアプリケーションが提供できる優れた処理能力と機能が注目されつつあります。いわゆるピア アプリケーションは、単純なファイル共有から、インスタント メッセージング (IM) や、共有ホワイトボード、Voice over IP (VoIP) 電話と会議、ソーシャル ネットワーキングなどの高度なコラボレーション アプリケーションまで、あらゆる範囲をカバーします。
Xbox 360® が Windows Media® Center を実行している別のコンピュータの存在を自動的に検出し、そのコンピュータとのオーディオやビデオの共有をすぐに開始して、Xbox® のコンピュータで再生できることを考えてみてください。ビジネス アプリケーションが同じようなピア認識能力と機能を備えていればすばらしいと思いませんか。あるユーザーが自分のデスクトップで新しいレコードを作成した場合に、そのアプリケーションを使用している組織内の別のユーザーが新しいレコードをすぐに見ることができたら、そして、それを実現するために中央サーバーをインストールして構成する必要がないとしたら、顧客にとってどれほど喜ばしいかを考えてみてください。
この記事では、ビジネス アプリケーションがサーバーレスのピア ネットワークで状態を共有できるようにすることで、ビジネス アプリケーションをピア対応にする方法について詳細に説明します。サンプル アプリケーションについて詳しく説明する前に、ピア ネットワークで状態共有のためによく試されるさまざまな方法について説明し、それらの方法の長所と短所を指摘します。最後に、WeSpend という名前のサンプル アプリケーションを示します。これは、預金口座の取引を追跡するためのツールです。このアプリケーションは、ホーム ネットワーク上で、そのアプリケーション自体のその他すべてのインスタンスとデータを自動的に同期します。
ピア ネットワークを実装する方法は、考えられるアプリケーションのアイデアと同じ数だけあります。どのような実装にも、なんらかの長所と短所があります。そのような長所や短所は、インフラストラクチャに関係していたり、ソリューションの長期的な維持管理の容易さに関係することであったり、そうでなければソリューションの初期開発を容易にすることに関係しています。ここでは、ピア ネットワークの開発に最もよく使用される方法をいくつか見ていきます。

固定状態サーバー
多くの開発チームが、ピア対応アプリケーションの初期概念フェーズを切り抜けて実装の検討を始めたときに最初に思い浮かべる問題の 1 つは、通常、"どうやって状態を共有するのか" ということです。
最も簡単な解決方法の 1 つは、ハイブリッド ピア ネットワークを作成することです。このソリューションでは、すべてのブロードキャストとピア トラフィックはピア メッシュで発生しますが、既知の中央サーバーが存在し、通常は Web サービスまたは別の Windows® Communication Foundation (WCF) サービスを利用して、状態を維持しています。これにより、ネットワーク上のピアはアプリケーション全体に対して単一の状態を認識でき、一方で最適化されたピア メッシュ通信を利用できます。
この種のネットワークの最も一般的な例は、ソーシャル ネットワークや IM アプリケーションです。IM アプリケーションでは、通常、ログインしているユーザーのリスト、ユーザーの所在と対応可能性、および全登録ユーザーの友人リストを管理する中央サーバーが必要です (図 1 を参照)。ネットワーク上のユーザー間の通信は、ピアツーピア方式で行われます。従来のクライアント サーバー機能とピアツーピア機能の間には、非常に便利で強力な機能の分割が見られます。
図 1 ピア ネットワークにおける中央状態サーバー (クリックすると拡大画像が表示されます)
このパターンの長所は、簡単に実装できることです。ピア ネットワーク コンポーネントは複雑さが少なく、しかも近年では Microsoft® .NET Framework と WCF のおかげで中央サーバーとの通信が非常に簡単になっています。
短所は、インフラストラクチャに中央サーバーが必要なことです。ピア アプリケーションは中央サーバーのアドレスを知る必要があり、これは構成とメンテナンスの問題の原因になる可能性があります。また、多くのアプリケーション開発者には、中央状態サーバーをグローバルに展開するためのリソースがなく、中央状態サーバーの要件はアプリケーションの仕様では受け入れられないものであることがよくあります。

一時的選択状態サーバー
共有状態パターンの進化における次のステップは、ピア メッシュ内のノードの 1 つを状態サーバーとして選択することによって、前述のパターンからインフラストラクチャの関与を軽減するというものです。このパターンはネットワークを利用したリアルタイム戦略ゲームによく見られ、ゲームを始めたプレーヤーがゲームの状態の "真なる唯一のコピー" を持ち、他のすべてのプレーヤーは同期化された複製を持ちます。
このパターンは、1 つのノードをメッシュの状態サーバーとして選択するアルゴリズムを使用することで機能します。このノードが状態サーバーになった後は、メッシュ内の他のすべてのノードは固定状態サーバーのシナリオと同じパターンに従い、単一の状態を更新および照会します。
この方法では固定中央サーバーが必要であるという問題はなくなりますが、その代わりに、選択システムの複雑さが問題になります。このピア ネットワークを実装するには、状態サーバーの選択方法を決定する必要があります。非常に簡単な解決方法の 1 つは、最も古いノード (メッシュに最も長く存在するもの) または最も新しいノードを選択することです。この方法では、ノードがメッシュに存在しなくなったときに問題が発生します。選択した状態サーバーがオフラインになると、どうなるでしょう。オフラインになることをメッシュが認識した場合は、ピアは新しい状態サーバーを選択できます。しかし、状態サーバーのパフォーマンスが低下しただけで、オフラインにはなっておらず、メッシュが再選択を決定しない場合は、どのようなことが発生するでしょうか。
このように、この解決方法の欠点は、インフラストラクチャから中央サーバーを不要にするのと引き換えに、プロジェクトの進捗に影響を与えかねないほど複雑な選択システムの作成が必要になることです。この選択システムは、単に最善の状態サーバー候補を選択するだけでなく、状態サーバーのブラックホール化 (メッセージを吸い込むだけで吐き出さない状態であり、単なるオフライン状態とは少し違います) や、状態サーバーのオフライン化に対処できるだけの回復力を備えている必要があります。
選択システムの複雑さを実際にうまく処理できるのであれば、この方法は強力で汎用性の高いシステムになる可能性があります。一方、選択システムがうまく機能しないと、この種のネットワークは悲惨な状態になり、状態共有には問題が山積することになります。

純粋なピア サーバーレス データ ブラスト
最初の 2 つのシナリオは、状態の正式なコピーが 1 つだけ存在するソリューションを提供しようとするものです。状態サーバーの場所が固定されるか選択されるかの違いはあっても、ピアが状態を照会および操作する方法は同じであり、単一のリモート サーバーに接続します。
中央状態サーバーの代わりによく使われるのは、筆者が "データ ブラスト" ネットワークと呼ぶネットワークです。このシナリオでは、メッシュに新しいピアが参加するたび、またはピアからデータ更新の要求が明示的にあった場合に、ピアはメッシュ内の他のすべてのピアに対してデータのコピーをブロードキャストします。各ピアは、状態データのブロードキャストを受信するたびに、ブロードキャストの内容とローカル状態を比較し、必要に応じて更新します。
この方法には、比較的大きな欠点が 2 つあります。1 つ目の問題は、ネットワーク上を送信される冗長なデータがかなり大量になる可能性があることです。ピア ネットワークのコンピュータが増えるほど、無駄に消費される帯域幅も多くなります。事実、このシナリオにおける冗長なデータ送信の量は、メッシュ内のノードの数が増えるにつれて急激に増加します。
この方法のもう 1 つの欠点は、各アプリケーションがそれぞれのデータのコピーをブロードキャストするので、データのブロードキャストを受信したら、同期操作を行って受信したデータとローカル データを比較し、差異を調整する必要があります。ピア ネットワークのサイズが拡大すると、やがては到着するメッセージより状態ブロードキャストの方の処理時間が長くなり、ボトルネックが発生して、アプリケーション自体のパフォーマンスが低下する可能性があります。ピア メッシュ内の各ノードはデータが重複しているかどうかを確認する必要があるので、ブロードキャスト送信データが一意であることを識別するためのなんらかの手段が必要です。
少々無駄なネットワーク リソースの浪費に対するトレードオフは、中央状態サーバーがなく、選択パターンに伴う複雑さに苦しまなくて済むことです。ある種の状況においては、特にノードの数が一定のサイズより増加しないことがわかっている場合は、重複するデータ送信が増えても、実装作業が減ることの方に大きな価値があります。

最近ピア同期
筆者は選択シナリオの実装に関係のあるさまざまな方法を調査しているときに、WCF には特定のメッセージが移動する最大ホップ数を示す属性があることを発見しました。このことがわかった後で、中央サーバーが不要なだけでなく、ノードの機能低下に対応でき、選択の必要もない、ピア ネットワークでの状態共有方法が存在することが明らかになりました。筆者は、この方法を最近ピア同期 (Nearest Peer Synchronization) と呼んでいます。
ピア メッシュ内の 2 つのノード間をメッセージが移動するときに使用するルートは、WCF によって完全に指示されます。これにより、WCF はネットワークでの通信パスを最適化できます。このことと、メッセージが移動する距離を 1 ホップだけに制限できることがわかっていれば、状態共有のための隣接同期パターンを実装できます。
まず、メッシュに新しく接続するノードは、接続した後で、1 ホップ メッセージを送信して、共有状態レコードの一意なレコード識別子のリストを要求します。メッシュ内で 1 ホップの距離にあるすべてのノードが、このメッセージを受信します。これらのノードは、コールバック コントラクトを使用して、メッシュに直接応答します。応答には、そのノードが保持している全データ レコードの一意な ID のリストが含まれます。最初に応答したノードが "勝者" となり、その他すべての応答は無視されます。重複するメッセージを無視するコードを作成する必要があることに注意してください。WCF による自動処理は行われません。
新しく接続したノードは、受信した一意な ID のリストと、自分が持っている一意な ID のリストを比較します (ノードは、オフライン状態をディスクに保持し、共有状態の部分的なビューを持っている可能性があります)。その後、応答で受信した一意な ID のうち、ローカル状態に含まれなかった ID を 1 ホップ メッセージでメッシュに送信します。そのメッセージに応答する最初のノードは (やはり、コールバック コントラクトを使用して)、これらの一意な各 ID の詳細を返送します。通常、この詳細は完全に構成されたシリアル化可能なオブジェクトです。
このパターンが優れているところは、障害のあるノードの消失を自動的に修正できる点です。メッシュ内のノードで障害が発生した場合、そのノードはデータの要求に応答しません。それ以外のすべての 1 ホップ ノードは、そのデータに応答する可能性があります。メッシュの構成により、新しく接続するノードが、障害の発生したノードとしか通信できない場合、WCF は、それまで 2 ホップの距離にあったノードを、1 ホップの距離であると見なすようになります。選択ロジックは必要なく、自動フェールオーバーは WCF のピア チャネル層において直接処理されるので、そのための追加コードを記述する必要はありません。各隣接ノードは各隣接ノードの状態と同期していることが保証されるので、状態はメッシュ内のすべてのピアに自動的に伝達されます。図 2 は、隣接するノードどうしが自動的に同期する純粋なサーバーレス ピア メッシュを示しています。
図 2 状態サーバーを持たないピア メッシュ (クリックすると拡大画像が表示されます)

WeSpend サンプル アプリケーション
このアイデアを実行するため、個人用資産マネージャのシミュレーションを見てみましょう。このアプリケーションを使用すると、ユーザーは、当座預金口座に対して行われた小切手取引やデビット取引などの金融取引を追跡できます。
WeSpend は、ネットワーク上で動作するアプリケーションのコピーどうしが状態を自動的に共有するので、コラボレーション ピア アプリケーションです。ユーザーが認識できるこの効果は、たとえば、下の階にいる家族が WeSpend で取引を入力すると、上の階にいる別の家族がこの新しい取引をすぐに見ることができます。同じ家の中で 3 人目の家族が新しく WeSpend を起動すると、アプリケーションを実行していなかった間に作成された取引が自動的に受信されます。この種のピア認識およびコラボレーション機能を追加することによって、優れた人気のあるアプリケーションと、単に可もなく不可もないアプリケーションとの差が生まれます。
WeSpend は、最近隣接ノード同期共有状態パターンを使用します。このアプリケーションは、いくつかの重要な技術コンポーネントを使用して実現されます。第 1 のコンポーネントは既に説明した PeerHopCount という属性であり、この属性を使用すると、メッセージのコンテンツを一定の回数だけ送信してから削除できます。この属性によって、データ メッセージの特性が IP の有効期限 (TTL) の概念とよく似たものになります。
次に示すのは WeSpend アプリケーションでの WCF メッセージ コントラクトのサンプルであり、ピア ホップ カウントを制御するフィールドを含みます。ピア ホップ カウント属性は NetPeerTcpBinding プロトコル バインドによってのみ使用されることに注意してください。その他すべてのバインドはこの属性を無視します。
[MessageContract]
public class TransactionIdRequest {
    [MessageBodyMember]
    public string Requester;
    [PeerHopCount]
    public int Hops;

    public TransactionIdRequest() {
        Hops = 1;
    }
}
このメッセージをメッシュに送信しても、使用可能なすべてのノードにメッセージを送信する通常のフラッド動作は発生しません。代わりに、メッセージは 1 回だけ配信された後、存在しなくなります。これにより、メッシュ内の最も近い隣接ノードに対してのみ、取引 ID のリストを要求できます。
このシナリオを可能にするもう 1 つの重要なコンポーネントは、コールバック コントラクトです。WCF ピアに対してメソッドが別のピアによって呼び出されると、ターゲット コードから、メソッド呼び出しを開始したピア メッシュへのコールバックを実行できます。その結果、単一のノードが隣接ノードの要求に応答でき、すべてのピア ノードで個別に WCF サービスをホストする必要がなくなります。コールバック コントラクトを使用して取引 ID の要求に応答するコードの例を次に示します。
public void RequestTransactionIds(TransactionIdRequest request) {
    List<Guid> outboundIds = new List<Guid>();

    foreach (Transaction tx in ModelRoot.Current.Transactions) {
        outboundIds.Add(tx.TransactionID);
    }

    CallbackChannel.TransactionIdsReply(new TransactionIdReply() {
        ReplyFrom = AppController.Current.Username,
        TransactionIDs = outboundIds
    });
}
ここでは、コールバック チャネルを使用して、メソッド呼び出し RequestTransactionIds を開始したピアに応答しています。TransactionIdRequest メッセージは 1 ホップのみに制限されていることを思い出してください。最大規模のピア ネットワークであっても、開始ピアに応答が殺到することはありません。
TransactionIdsReply メソッドを呼び出して取引 ID のリストを受信したピア ノードは、再びコールバック コントラクトを使用して、ローカル記憶域にまだ存在しない ID の詳細を要求します (図 3 を参照)。
図 3 取引の詳細の要求
public void TransactionIdsReply(TransactionIdReply reply)
{
    // Compare the list of TX IDs with the list of IDs in local storage
    // for each item that is "missing" from local storage, put that item
    // in the request for details.

    List<Guid> output = new List<Guid>();
    foreach (Guid id in reply.TransactionIDs)
    {
        if (!ModelRoot.Current.Transactions.Any(
                tx => tx.TransactionID.Equals(id)))
        {
            output.Add(id);
            Console.WriteLine("[LTS] Need to request detail for TX " +
                id.ToString());
        }
    }
    CallbackChannel.RequestTransactionDetails(output);
}
同期ペアのもう一方は、RequestTransactionDetails メソッドを呼び出すと、やはりコールバック コントラクトを使用して、メッシュに応答します (図 4 を参照)。
図 4 詳細情報の応答
public void RequestTransactionDetails(List<Guid> transactionIds)
{
    List<LedgerTransaction> output = new List<LedgerTransaction>();
    foreach (Guid id in transactionIds)
    {
        Transaction tx = (from Transaction lt in
        ModelRoot.Current.Transactions
                         where lt.TransactionID.Equals(id)
                         select lt).FirstOrDefault();
        if (tx != null)
            output.Add(tx.ToLedgerTransaction());
    }
    CallbackChannel.TransactionDetailsAck(new TransactionDetailReply()
    {
        ReplyFrom = AppController.Current.Username,
        Transactions = output
    });
}
前述のコード リストで使用しているプロパティ CallbackChannel は、WCF ピア サービス コントラクトの実装で次のように定義されています。
protected ILedgerTransactionServiceChannel CallbackChannel {
    get {
        return OperationContext.Current.GetCallbackChannel<
            ILedgerTransactionServiceChannel>();
    }
}
図 5 は、メッシュに接続したばかりのピアと最も近い隣接ノードの間で、同期操作を開始する呼び出しに応答するメッセージの流れを示しています。
図 5 最近隣接ノード同期の流れ (クリックすると拡大画像が表示されます)
前述のメソッドの実装に欠落していることの 1 つは、再入実行に対する防御策です。つまり、複数のピアが同時に同期操作を開始しようとした場合、実稼働コードではこれを防止する必要があります。また、このコードは同期操作のためにコールバック コントラクトを多用することにも注意してください。ピア ノードは他のピア ノードが同時に同期を実行できないように保証する必要がありますが、これは簡単なセマフォを使用して実現できます。

アプリケーションにピア ネットワーク機能を追加すると、ソフトウェアのユーザー エクスペリエンスが大幅に向上します。しかし、たいていの場合は、そうすることで発生する複雑さが障害になり、ピア ネットワーク機能が製品の設計から除外されてしまうこともあります。実装によってはメンテナンス上の悪夢にたちまち陥り、ピア ネットワークが仕様から削除されることになり、複雑さはまさに手に負えなくなります。
この記事の目的は、ピア ネットワークが構築可能で簡単に実装できるだけでなく、高い効率とフォールト トレラント機能を備えたコーディングしやすいサーバーレス状態共有システムを実装することさえ可能であると示すことです。また、ここで解説したパターンにはほとんど無限のバリエーションがあることも覚えておいてください。
コード サンプルをダウンロードし、試してみることをお勧めします。コンピュータを 2 台用意し、2 つの異なるユーザー名をシミュレートするように app.config を変更するのが最良の方法です。txhost のアプリケーション設定を true (アプリケーションはいくつかのサンプル取引を作成します) に設定したインスタンスを起動した後、txhost を false (取引の自動作成を行いません) に設定したもう 1 つのインスタンスを起動します。1 つ目のインスタンスの取引が 2 つ目のインスタンスに自動的に伝達されることがわかります。ボタンをクリックして新しい取引を作成すると、サンプル アプリケーションの両方の実行インスタンスにその取引が表示されるのがわかります。同期の効果をさらにはっきり確認するには、2 つ目のインスタンスをいったん終了してから再起動してください。2 つ目のインスタンスを再起動すると、すべての取引が表示されます。コンピュータでのピア名解決プロトコル (PNRP) の動作によっては、サンプル アプリケーションが最初の通信を開始するまでに最大 30 秒ほどかかります。

Kevin Hoffman は、Liquidnet Holdings, Inc. の Research Developer であり、dotnetaddict.dotnetdevelopersjournal.com のブログにも頻繁に書いています。彼は、ネットワーク、通信、および分散アプリケーション アーキテクチャに対して絶えず情熱を持っています。彼は、WCF がまだコードネームの "Indigo" と呼ばれていた初期の段階から WCF に携わっています。


編集者のメモ
データを変幻自在にする
Ted Neward

ページを調整しないでください。これは間違いではありません。今月は Howard Dierking が休みなので、「編集者のメモ」はゲスト編集者の Ted Neward に執筆を依頼しました。Howard は、TechEd および 8 月号には間に合うように戻ってきます。—編集者
今月号では、データのストレージ、操作、およびアクセスについて説明します。プログラミングに関するトピックがデータ アクセスの問題のように議論の的になることはまれです。オブジェクト リレーショナル マッパーの効率、アドホック クエリ構文の必要性、リレーショナルな正規形のパフォーマンス上の欠点など、データに関する議論は尽きません。
低価格ストレージの絶え間ない進歩とアクセス速度の向上により、数年前には先見の明のある業界通でも想像できなかったほど、個人がアクセスするデータが増えています。筆者は、自分が所有している 1 台目の 1 GB ドライブがいっぱいになることなどあり得るものかと思ったときのことを思い出します。それが、今や、1 TB の壁もまたたく間に侵食されていきます。
データの性質も変わりました。かつては、データ ストレージの大部分は発注書や顧客名などの単純なビジネス情報が占めていました。ところが、最近では、個人情報や、写真、歌、ボイスメールなどの大容量の複雑なファイル形式が含まれるようになりました。このようなデータをリレーショナル形式でキャプチャすることは容易ではありません。私たちは、この新しい種類のデータを従来のビジネス領域 (インデックス付け、検索、他の重要なデータへの関連付けの方法) に統合するという 1 つの課題に直面しています。
この課題に対処するには、リレーショナル データベースを発展させ、これらの新しいデータ形式を保存してクエリで使用できるようにするか、リレーショナル以外の新しいデータ保存方法を見出す必要があります。David Robinson が今月の SQL Server Data Services に関する記事で 1 つの方法を提示しています。
今後のデータ ストレージは、一元的に配置、管理、および最適化されたリレーショナル データベースに限定されません。実際、ネットワークの末端 (ユーザーのコンピュータまたはその他のローカル ストア) に置かれるデータが増える傾向にあります。そのため、デバイスや階層の境界を越えてクエリを行う新しい方法を考案する必要があります。この問題については、John Papa が今月のデータポイントで取り上げています。
自分独自のデータを所有することに慣れてくると、ユーザーのデータ管理能力も向上します。たとえば、私の母親の携帯音楽プレーヤーが壊れたら、そのプレーヤーに保存されている音楽も消失します。技術に疎い母でも、このことは承知しています。データをバックアップしておく責任はプレーヤーの販売元ではなく自分にあるということを母はわかっています。データ管理の負担の一部が、開発者からユーザーに移行しつつあります。この移行により、その他にも変更が必要な事項が生じます。
デバイス上のデータをデスクトップ コンピュータや外部ドライブなどと自動的に同期し、バックアップする自動化シナリオを作成する必要があります。このような自動化シナリオは、データの安全性を簡単に確保する手段として、ユーザーにとっては魅力的です。
今月号のページを繰っていくうちに、明らかになりつつあるデータのプログラミングおよび管理に関するあらゆる変化がわかります。これを読んで少しでも読者の関心がわき、将来のデータ計画に対する考え方が変わることを願っています。
もう 1 つお知らせがあります。今月号では 2 つの新しいバーチャル ラボを用意しています。その 1 つには、Joshua Smith の今月の記事「データと WPF: データ バインドと WPF でデータの表示をカスタマイズする」(go.microsoft.com/fwlink/?LinkId=120301) で説明しているプロジェクトが含まれています。ラボを起動し、記事を読み進めながらコードを実行してください。
MSDN Magazine 2008 年 4 月号の Dr. James McCaffrey の SQL ストアド プロシージャのテストの実行に関するコラムに対応するバーチャル ラボも提供しています。お使いのブラウザで go.microsoft.com/fwlink/?LinkId=120461 にアクセスすると、このバーチャル ラボを開始できます。
Access、Active Directory、Excel、IntelliSense、Internet Explorer、Microsoft、MSDN、PowerPoint、SharePoint、Silverlight、SQL Server、Visio、Visual Basic、Visual Studio、Windows、Windows Media、Windows Server、Windows Vista、Xbox、および Xbox 360 は、米国またはその他の国におけるマイクロソフトの登録商標または商標です。ここに記載されている商標または会社名は各社の所有物です。

msdn.microsoft.com/msdnmag をご覧ください。MSDN Magazine に関するご質問、ご意見、ご提案は、編集者まで英語でお送りください (mmeditor@microsoft.com)。

今月号の記事の執筆に携わった次のマイクロソフトの技術支援スタッフの皆様に感謝します。Melitta Andersen、Spotty Bowles、Alex DeJarnatt, T.J.Goltermann、Kapil Gupta、Michael Howard、Shalini Joshi、Steve Maine、Tim Mallalieu、Kevin Ransom、Daniel Simmons、Beatriz Stollnitz、Jon Wheeler、および Jesse Yurkovich

Ted Neward は、大規模エンタープライズ システムを専門とするフリーのコンサルタントです。複数の著書および共著書があり、Microsoft MVP アーキテクト、BEA の技術ディレクター、INETA の講演者、Pluralsight のインストラクタでもあります。Ted の連絡先は、ted@tedneward.com です。また、blogs.tedneward.com にブログを公開しています。


ツールボックス
簡単な Wiki のホスティング、Scott Hanselman のブログ、および画面のキャプチャ
Scott Mitchell

目次
Wiki をホストする
注目のブログ
スクリーンショットをキャプチャ、編集、および共有する
推薦図書

Wiki をホストする
Wiki は、ユーザーがコンテンツを共同で追加、更新、および編成する Web アプリケーションです。ブログや他のコンテンツ管理システムと同様に、Wiki のコンテンツも Web ページ インターフェイスを使用して編集できます。Wiki とブログや従来のコンテンツ管理システムとの違いは、Wiki には本質的に一定のスタイルがないという点です。サイトのユーザーは、コンテンツを作成し、関係を定義し、サイトの Web ページ間のリンクを設定します。最もよく知られた Wiki は、おそらく Wikipedia.org でしょう。
Wiki はナレッジ マネジメント ポータルを作成する際に特に便利です。会社内部では、進行中の開発プロジェクトとその状況、コード作成規則、ベースライン開発環境の設定方法についての新規雇用者向けチュートリアルなどに関する情報を収めた Wiki を、開発者チームが管理することがあります。
顧客向けサポート サイトには、信頼を受けたサポート スタッフがよく寄せられる質問に対する回答を事前に作成および管理することのできる Wiki が含まれていることがあります。また、多くのコミュニティ指向のサイトには、メンバによるサイトの更新やナレッジ ベースへの投稿が可能な Wiki が含まれています。たとえば、CodePlex.com でホストされているオープンソース コード プロジェクトは、Wiki として管理されています。
さまざまな市販の Wiki プラットフォームやオープンソースの Wiki プラットフォームがあります。ASP.NET および C# で作成されたオープンソースの Wiki の 1 つが、ScrewTurn Wiki Version 2.0 です。既定では、ScrewTurn Wiki のページ、変更履歴、およびユーザー アカウントは、Web サーバーのファイル システムに保存されます。つまり、この Wiki にはデータベースが必要ありません。そのため、Web サーバーへの ScrewTurn Wiki の展開は、コピー アンド ペースト操作と同じくらい簡単です。
ただし、Wiki のストレージ システムはプロバイダ モデルの上に構築されるので、代替のプロバイダをプラグインとして利用し、システム情報を他の場所でも保持できます。ScrewTurn Wiki Web サイトでは、SQL Server® ストレージ プロバイダや MySQL ストレージ プロバイダが使用されています。
ScrewTurn Wiki をインストールした時点では、だれでもアカウントを作成でき、認証されたユーザーであればコンテンツを作成、編集、または削除することができます。これらのアクセス レベルは Wiki の管理ページで制限可能です。
訪問者は、登録が禁止されることもあれば、ログインするために管理者の承認が必要なこともあります。管理者は、ユーザーが実行できる操作を制限できます。たとえば、ユーザーにページの作成および編集を許可するが削除は許可しない、という制限をサイトに設定できますもちろん、他の Wiki と同様に、ScrewTurn Wiki でもページの変更履歴は完全に保持されます。異なるバージョンの比較を行うツールも用意されています。また、管理者はページを以前のバージョンにロールバックできます。
ScrewTurn Wiki では、複数の言語でコンテンツを作成および表示することができます。英語、フランス語、スペイン語、ドイツ語などがサポートされています。
価格 : 無料 (オープン ソース)
screwturn.eu
オープンソースの ScrewTurn Wiki (クリックすると拡大画像が表示されます)

注目のブログ
他の開発者のソース コードを読むことは、自分自身のプログラミング スキルを磨くうえで非常に役立ちます。一般的なタスクを実行するさまざまな方法を学ぶことができるためです。マイクロソフトのシニア プログラム マネージャであり、精力的なブロガーでもある Scott Hanselman もこの考えに賛同しており、一般の人々が新聞を読むようにソース コードに目を通しています。彼は、その中でも最も興味深いコードについて、「The Weekly Source Code (今週のソース コード)」というタイトルのブログ エントリでコメントを付けて紹介しています。
開発者の皆さんは、「The Weekly Source Code」に加えて、Scott の「Ultimate Developer and Power Users Tool List (開発者とパワー ユーザー向けの究極のツール リスト)」(hanselman.com/tools) もぜひご覧ください。これは、Scott が日常的に使用しているツールやユーティリティを列挙したリストです。この記事では、トピックごとに分類されたお勧めのツールやユーティリティが多数紹介されています (Visual Studio® の生産性向上ツールに対する XML の操作から、電子メールの受信トレイの管理や Web ブラウザの活用まで)。「Ultimate Developer and Power Users Tool List」の投稿は 2003 年に始められ、それ以降毎年更新されています。
Scott は頻繁にブログ記事を投稿していますが、それに加えて、「Hanselminutes」というオーディオ ポッドキャストも毎週ホストしています。Scott はこのポッドキャストで、開発者ツールをレビューし、プログラミングに関する助言を行い、苦労話を紹介しています。また、主要なマイクロソフトのスタッフや Microsoft® .NET Framework 開発者コミュニティのメンバへのインタビューが収録されていることもあります。ポッドキャストの各エピソードは 30 分程度なので、通勤途中に聞くのに最適です。
ブログ : hanselman.com/blog
ポッドキャスト : hanselminutes.com

スクリーンショットをキャプチャ、編集、および共有する
通常の作業日に、私はスクリーンショットを何十枚も撮ります。そのほとんどは執筆中のブログ エントリ、記事、または書籍のためのものですが、同僚や顧客に送信することもあります。
多くの場合、電子メールを使用して問題の説明を試みるよりも、問題のある UI のスクリーンショットを送信する方が簡単です。Windows® では、Print Screen キーで現在の画面をコピーできるほか、Alt キーを押しながら Print Screen キーを押すことで現在のウィンドウをコピーできます。そのスクリーンショットは、電子メール メッセージ、画像編集プログラム、Microsoft Office Word 文書などに貼り付けることができます。
最近になって、Print Screen キーよりも堅牢な画面キャプチャ手法にアップグレードする必要性を感じたため、TechSmith 社の SnagIt Version 8.2 を購入することにしました。この製品の最初のレビューが MSDN Magazine の 2003 年 7 月号に掲載されていますが、それ以降、多数の変更が加えられました。最新の SnagIt は、ユーザー インターフェイスが更新され、画像編集ツールが多数追加され、さまざまな新しいファイル形式で画像を保存する機能も備えています。
SnagIt では、ユーザー指定の領域、指定したウィンドウ、または画面全体のスクリーンショットを撮ることができます。また、スクロール ウィンドウをキャプチャする際にドキュメント全体が自動的にスクロールされるよう構成できるため、コンテンツ全体を 1 枚の画像にキャプチャできます。この機能は、長い Web ページをキャプチャする際に特に役立ちます。
ウィンドウまたは選択した領域をキャプチャすると、SnagIt はそのスクリーンショットを強力な編集/共有ツールに読み込みます。マウスを数回クリックするだけで、テキストの追加、図形の描画、領域の切り取りと移動、領域の強調表示、境界線の色と幅の指定、コールアウトの追加などが可能です。キャプチャした画像をトリミング、サイズ変更、回転することで、スクリーン キャプチャのサイズを詳細に調整できます。
透かしの追加、重要な領域の強調表示や拡大、ホットリンクの追加など、より高度な作業も実行できます。何よりすばらしいのは、SnagIt はシンプルで使いやすいという点です。デジタル画像の編集エキスパートでなくても使いこなせます。
編集したスクリーンショットは、ボタンをクリックするだけで共有できます。さらに、キャプチャの画像としての保存、クリップボードへのコピー、印刷、電子メールでの送信、FTP 経由の送信、インスタント メッセンジャーでの送信、Microsoft Office Excel®、Word、または PowerPoint® ファイルへの読み込みなどをワンクリックで行うことができます。
また、キャプチャはさまざまなファイル形式で保存できます。SnagIt では、BMP、JPG、PNG などの標準的な画像形式に加えて、PDF 文書、Web アーカイブ ファイル (.MHT)、または Adobe Flash ファイルとして画面のキャプチャを保存できます。こうしたオプションも、現在のバージョンの新機能です。
価格 : 1 ユーザー ライセンス - 4,680 円 (本体価格 (税抜))
techsmith.com/screen-capture.asp
SnagIt は最新ユーザー インターフェイスを備えている (クリックすると拡大画像が表示されます)

推薦図書
データのクエリは、プログラマにとって最も一般的なタスクの 1 つです。ただし、さまざまなデータ型にアクセスするには、固有の構文とセマンティクスを学習する必要があります。また、XML ファイルやデータベースなどの外部データ ソースのクエリでは、通常、クエリ構造を文字列にシリアル化する作業が伴います。
コンパイラではコードを解析できますが、文字列リテラルは解析できないため、IntelliSense® やコンパイル時の型チェックなどの機能は利用できません。このような問題を解決するため、マイクロソフトでは、統合されたデータ クエリ構文を開発者に提供する LINQ を作成しました。LINQ では、C# および Visual Basic® プログラミング言語が拡張され、オブジェクトのコレクション、XML ドキュメント、データベースなどに対するクエリに使用できる、SQL に似たリッチな構文が追加されます。
LINQ は、.NET Framework におけるさまざまな新しい言語拡張、および新しい名前空間とクラスによって実現されました。LINQ に関連するトピックは広範で内容の難解さも伴うため、LINQ を徹底的に学習するにはかなり多くのオンライン記事を読む必要があります。LINQ 関連の書籍の 1 つに、Fabrice Marguerie 氏、Steve Eichert 氏、および Jim Wooley 氏の共著である『LINQ in Action』があります。この書籍では、読み進めるうちに深まる読者の理解度を踏まえて豊富な例を提示することで、LINQ のさまざまな側面を説明しています。
『LINQ in Action』は、私がこれまでに読んだ LINQ の入門書の中でも最良の部類に入ります。この本では、最初に LINQ の目的および設計の目標が示され、LINQ に対応して C# や Visual Basic に追加される言語拡張が紹介されています。次に、オブジェクト、リレーショナル データ、および XML データでの LINQ クエリを可能にするライブラリとして、LINQ to Objects、LINQ to SQL、および LINQ to XML の説明があります。最後の 2 章では、LINQ を拡張して Web サービスなどの代替データ ソースでクエリを実行する方法と、アプリケーション アーキテクチャのさまざまなレイヤで LINQ を最大限に活用する方法が解説されています。
『LINQ in Action』で私が一番気に入っているのは、最も重要な機能に重点を置いた、簡単で理解しやすい例が、各章の導入部分として使用されている点です。その後で、LINQ のクエリと機能が実際にはどのように実装されるかが説明されています。
土台を築いたうえで、内容はより高度な機能やシナリオに進みます。たとえば、LINQ to SQL を扱った章は、オブジェクト マッピングの設定方法と DataContext クラスを使用したデータの読み取りおよび更新の方法から始まります。その後で、LINQ のクエリ構文が T-SQL にどのように変換されるかが説明されています。最後に、同時実行の制御、ストアド プロシージャやユーザー定義関数の使用など、高度な LINQ to SQL 機能の説明で締めくくられています。
価格 : 44.99 ドル
manning.com

ご意見やご質問は、Scott (toolsmm@microsoft.com) までお送りください。

Scott Mitchell は、多数の著書を出版している執筆者であり、4GuysFromRolla.com の創設者です。また、1998 年からは Microsoft MVP としてマイクロソフトの Web テクノロジに携わっています。現在は、フリーのコンサルタント、トレーナー、およびライターとして活躍しています。Scott の連絡先は、Mitchell@4guysfromrolla.com です。また、ScottOnWriting.NET にブログを公開しています。


CLR 徹底解剖
System.AddIn を使用して Windows フォーム アプリケーションを拡張する
Mueez Siddiqui

コードのダウンロード : CLRInsideOut2008_07.exe (472 KB)
オンラインでのコードの参照
目次
ホストを調整する
オブジェクト モデルを公開する
アダプタ、コントラクト、およびアドイン ビュー
アドインを記述する
コラボレーション アドイン
まとめ
新しい Microsoft® .NET アドイン フレームワーク (System.AddIn) を使用して、Windows® フォーム アプリケーションに拡張性を付与できることをご存じでしょうか。今回のコラムでは、このアドイン フレームワークを通してオブジェクト モデルを公開できるように、ShapeApp という描画アプリケーションに変更を加えます。これにより、ホスト アプリケーション上でタスクを自動的に実行するアドイン (自動化アドイン) を作成できます。また、このシナリオで一般的に直面する問題の種類について説明し、その解決策を提示します。
記事を読み始める前にアドイン フレームワークに関する背景知識を必要とされる方は、最初に CLR アドイン チームのブログのリソース ページ (go.microsoft.com/fwlink/?LinkId=117519) をお読みになることをお勧めします。また、「CLR 徹底解剖」コラムで以前に取り上げられたアドイン フレームワークに関する記事も参考になります。
ShapeApp は、基本的な図形を使用して描画を行うことができる描画アプリケーションです。新しい図形を挿入して、それらを移動したり、色やサイズを変更したりできます。描画をファイルに保存することや、複数の描画をタブで開くこともできます。ShapeApp で作成した簡単な描画を図 1 に示します。
図 1 ShapeApp のスクリーンショット
ShapeApp は、Visual Studio® Tools for Applications (VSTA) チームにより、Visual Studio IDE をアプリケーションに埋め込むことができるサンプルとして開発されました。埋め込んだ IDE は、アプリケーションに対する拡張の記述に使用できます。このアプリケーションは、拡張性コード付きのバージョンと拡張性コードなしのバージョンが提供されています。このサンプルでは、基本バージョンから始めて、アドイン フレームワークと共に動作するよう調整を加えます。

ホストを調整する
このセクションでは、HTML のドキュメント オブジェクト モデル (DOM) に似たオブジェクト モデルを公開できるように、アプリケーションを調整します。それによって、アプリケーションのタスクを自動化するアドインを作成できます。
ホストを調整する最初の手順は、ホストが公開するオブジェクト モデルを定義することです (まだコードを記述する必要はないことに注意してください)。アドイン フレームワークの分離機能およびバージョン管理機能により、オブジェクト モデルに制限が加えられます。一般に、オブジェクト モデルは、アドインが使用する型の中で .NET Framework に組み込まれていないものをすべて定義する必要があります。つまり、ビュー アセンブリは、.NET Framework アセンブリ以外のアセンブリを参照しないようにする必要があります。MarshalByRefObject 型は、明示的に禁止されます。使用可能な型の詳細については、コントラクトに関するブログ記事 (go.microsoft.com/fwlink/?LinkId=117520) を参照してください。
ShapeApp のオブジェクト モデルの型の一覧を図 2 に示します。ここに示されるすべての型は、ホスト アプリケーションの内部に存在します。後で、これらの型をパイプライン経由で公開します。
図 2 ShapeApp の型
型 | 説明
ShapeApplication | メインのアプリケーション オブジェクト
Drawing | アプリケーション内の描画を表すオブジェクト
Shape | 描画内の図形を表すオブジェクト
DrawingCollection | 描画オブジェクトのコレクション
ShapeCollection | 図形オブジェクトのコレクション
EventArgs 関連の型 | カスタム イベント引数に使用される、System.EventArgs から派生したいくつかの型
次の手順として、ホストがアドインを検出してアクティブ化するための手段を追加します。そのために、ホスト上で単純なアドイン マネージャを使用します。これには、アドインを読み込んで追跡する静的クラス AddInManager と、ユーザーがアドインを管理するためのフォームが含まれます。このフォームは、ホスト上のメニュー項目を通してアクティブ化され、ホストに対して必要な変更は最小限で済みます。このフォームを図 3 に示します。
図 3 アドイン マネージャ フォーム
アドイン マネージャのコードは非常に単純です。使用可能なアドインを見つけるために必要なコードは、次の 3 行です。
// update the add-in store to include add-ins located in the
// current program folder
string[] warnings = AddInStore.Update(PipelineStoreLocation.ApplicationBase);
// get the view type defined in the host views assembly
System.Type hostViewOfAddIn = typeof(ShapeAppHostViews.IShapeAddIn);
// find all add-ins that implement the view
ICollection<AddInToken> addIns = AddInStore.FindAddIns(hostViewOfAddIn,
    PipelineStoreLocation.ApplicationBase);
最初の行 AddInStore.Update は、使用可能なアドインおよびパイプライン コンポーネントを検索するために使用します。PipelineStoreLocation.ApplicationBase は、アプリケーションのフォルダを検索するように指示しています。フォルダ構造の詳細については、go.microsoft.com/fwlink/?LinkId=117521 を参照してください。次に、アクティブ化するアドインの型を取得します。ここでは、ホスト ビュー アセンブリに定義された IShapeAddIn 型です。最後に、検査してアクティブ化できる使用可能なアドイン トークンのコレクションを返す AddInStore.FindAddIns を呼び出します。コレクションからトークンを選択したら、1 行のコードを使用してそれをアクティブ化できます。
// activate the add-in in full trust mode
ShapeAppHostViews.IShapeAddIn addIn =
    token.Activate<ShapeAppHostViews.IShapeAddIn>(
    AddInSecurityLevel.FullTrust);
また、アドインをプロセス外または異なる信頼レベルでアクティブ化することも選択できます。分離レベルの一覧については、go.microsoft.com/fwlink/?LinkId=117522 で確認してください。各分離レベルのパフォーマンスの比較については、パフォーマンスに関するブログ記事 (go.microsoft.com/fwlink/?LinkId=117523) を参照してください。

オブジェクト モデルを公開する
空のパイプラインとアドイン マネージャが準備できたら、ホストからアドインへの機能の公開を開始できます。オブジェクト モデルに含まれる型を公開するには、まず、公開する型のそれぞれに対して、HostViews アセンブリ内にホスト ビューを作成する必要があります。このビューでは、プロパティ、メソッド、およびイベントを公開できます。ホスト ビューには、抽象基本クラスまたはインターフェイスを使用できます。
ShapeApp では、3 つの理由からインターフェイスを使用しました。1 つは、Visual Basic® から使用されたときにイベントが正しく動作するには、インターフェイスが必要であるためです。2 つ目の理由は、C# が多重継承をサポートしないため、1 つのクラスが 2 つの基本クラスから継承できないことです。したがって、すべてのホスト クラスで実装できるようにするには (それらが既に基本クラスを持つ場合であっても)、ビューがインターフェイスである必要があります。3 つ目の理由は、インターフェイスでは EIMI (明示的なインターフェイス メソッドの実装) を使用できることです。この重要性については、後で説明します。
ShapeApplication オブジェクトのホスト ビューがどのようなものかを図 4 に示します。メンバで使用される型は、EventHandler<T> のような組み込みフレームワーク型か、または IDrawing のような他のビューです。CodePlex のサイト (go.microsoft.com/fwlink/?LinkId=117524) から FxCop ルールをダウンロードして、これらのビューを完全なパイプラインに簡単に組み込めることを確認できます。
図 4 ShapeApplication オブジェクトのホスト ビュー
namespace ShapeAppHostViews
{
    public interface IShapeApplication
    {
        // the drawing present in the selected tab
        IDrawing ActiveDrawing { get; }
        // a collection of available shapes (square, circle, etc)
        IShapeCollection AvailableShapes { get; }
        // a collection of drawings currently open in the application
        IDrawingCollection Drawings { get; }
        // main window visibility
        bool Visible { get; set; }

        // create a new drawing in the application
        IDrawing NewDrawing();
        // completely exit the application
        void Quit();

        // event fired when a drawing is created
        event EventHandler<CreatedDrawingEventArgs> CreatedDrawing;
    }
}
ビュー アセンブリを作成したところで、対応するビューから継承するようにホストのクラスを修正します。ShapeApplication クラスへの変更箇所を次に太字で示します。
public class ShapeApplication : System.Windows.Forms.IWin32Window
{
...
}

public class ShapeApplication : ShapeAppHostViews.IShapeApplication,
    System.Windows.Forms.IWin32Window
{
...
}
次に、ホスト ビューから各メンバを実装する必要があります。引数および戻り値として組み込み型を使用するメンバに対しては、(そのメンバがホストのクラスでパブリックであることを確認する以外) 特別な実装は必要ありません。たとえば、ShapeApplication クラスの Visible プロパティは、Visible インターフェイス メンバと同じシグニチャを持ちます。
public bool Visible
{
    get {...}
    set {...}
}
これは、ホストの ShapeApplication クラスでの Visible の既存の実装が、ビューの実装として使用されることを意味します。
ただし、ホスト ビューに定義された他の型を使用するメンバは、明示的に実装される必要があります。たとえば、ActiveDrawing プロパティは、ShapeApplication クラス内と IShapeApplication ホスト ビュー内で異なるシグニチャを持ちます。ホストのクラスに含まれる元のバージョンのプロパティは、Drawing オブジェクトを返します。
public Drawing ActiveDrawing
{
    get {...}
}
ホスト ビュー内のバージョンである IShapeApplication は、対応するビュー IDrawing だけを返せば済みます。
IDrawing ActiveDrawing
{
    get;
}
この 2 番目のバージョンをホストのクラスに実装するために、EIMI を使用する必要があります。これは、ActiveDrawing という同じ名前の新しいプロパティをホストのクラスに追加することで行います。これで、ホストの ShapeApplication クラスには、ActiveDrawing というプロパティが 2 つ含まれることになります (図 5 を参照)。
図 5 ActiveDrawing プロパティ
// original property
public Drawing ActiveDrawing
{
    get {...}
}

// implementation of the host view's version of ActiveDrawing
ShapeAppHostViews.IDrawing
ShapeAppHostViews.IShapeApplication.ActiveDrawing
{
    get
    {
       // a call to the original property
       return this.ActiveDrawing;
    }
}
新しい実装は、Drawing オブジェクトを IDrawing 型へと暗黙的にキャストします。Windows フォーム アプリケーションに対してオブジェクト モデルを公開する場合、公開されたメンバの一部が Windows フォーム コントロールにアクセスして変更を加える可能性が高くなります。そのようなメンバが、コントロールを作成したスレッド以外のスレッドから呼び出された場合、GUI の動作は予測不能になります。
アドインに公開されるメンバはどのスレッドからでも呼び出せるため、公開されたメンバが GUI コントロールにアクセスしたり変更を加えたりする際には、Control.InvokeRequired および Control.Invoke を使用して、処理が安全に行われるようにする必要があります。たとえば、ShapeApplication.NewDrawing 機能は ApplicationForm オブジェクトにアクセスするので、安全な実装のために Invoke を使用する必要があります。
次に古い実装を示します。
public Drawing NewDrawing()
{
    Drawing newDrawing = new Drawing(this);
    ...
    this.ApplicationForm.drawingsTabControl.TabPages.Add(
        newDrawing.DrawingSurface);
    ...
    return newDrawing;
}
新しい実装を図 6 に示します。変更箇所は太字で示しています。
図 6 Invoke を使用した NewDrawing メソッド
                                                            // delegate added to allow invoke
private delegate Drawing NewDrawingDelegate();

public Drawing NewDrawing()
{
    // check if we need an invoke
    if (this.ApplicationForm.InvokeRequired)
    {
        // invoke this method using the ApplicationForm object
        NewDrawingDelegate del = new NewDrawingDelegate(NewDrawing);
        return (Drawing) ApplicationForm.Invoke(del);
    }
    else
    {
        Drawing newDrawing = new Drawing(this);
        ...
        this.ApplicationForm.drawingsTabControl.TabPages.Add(
            newDrawing.DrawingSurface);
        ...
        return newDrawing;
    }
}

アダプタ、コントラクト、およびアドイン ビュー
ホストによって公開された機能にアドインからアクセスできるようにするため、アダプタ、コントラクト、およびアドイン ビューを作成する必要があります。パイプライン コンポーネントは、ホスト ビューから自動的に生成できます。Pipeline Builder は、この自動生成だけを実行するためのツールです。これは go.microsoft.com/fwlink/?LinkId=117525 から入手できます。Pipeline Builder は、このアプリケーションの最初のバージョンに対して適切に動作します。
または、パイプラインを手動でコーディングすることもできます。これは、複数バージョンに対応したアダプタを記述するときに必要です。Visual Studio では、必要な各アセンブリ (ビュー、アダプタ、コントラクトなど) に対するプロジェクトを ShapeApp ソリューションに追加することで、パイプラインを設定できます。Visual Studio でパイプラインを作成する手順のステップバイステップ ガイドについては、go.microsoft.com/fwlink/?LinkId=117526 を参照してください。
このサンプルでは、パイプラインを手動でコーディングしています。これは ShapeApp オブジェクト モデルの最初のバージョンであるため、ホストとアドインのビューは同じです。したがって、両方のビューに対して 1 つのアセンブリを使用できます。手動でコーディングする場合には、最初に空のパイプラインを設定すると便利です。そのようなパイプラインのビューは次のようになります。
public interface IShapeAddIn
{
    void Initialize(IShapeApplication application);
}

public interface IShapeApplication
{
    // TODO: Implement this.
}
Initialize メソッドは、読み込み時にメイン アプリケーション オブジェクトをアドインに渡すために使用されます。これにより、ホストが明示的に何らかのサービスを要求しなくても、アドインでいつでもホストを制御できるようになります (これが、自動化アドインと呼ばれる理由です)。
空のパイプラインを作成した後は、ホストから他の機能を 1 つずつ公開し、それをコンパイルしてテストできます。この反復的なアプローチにより、コンパイル エラーが果てしなく検出されるような事態を避けることができます。もちろん、いったんインターフェイスが完成し、アプリケーションがリリースされた後は、既存のインターフェイスにそれ以上メソッドを追加することはできません。どのようなアダプタが必要となるかは、オブジェクトがどこに存在し、どこでアクセスされるかによって決まります。一般に、オブジェクトは次の 3 つのカテゴリのいずれかに含まれます。
ホスト側オブジェクト これらのオブジェクトは、ホスト側に存在し、アドインからアクセスされます (ShapeApplication クラスなど)。このようなオブジェクトをアドインに対して公開するには、図 7 に示すようなパイプライン コンポーネントが必要です。オブジェクトはホスト側に存在するため、上部のホスト ビューから開始します。ビューからコントラクトへのホスト アダプタを使用してビューをコントラクトに変換し、コントラクトからビューへのアドイン アダプタを使用してアドイン ビューに変換します。この 2 つのアダプタにより、アドインはホスト側オブジェクトにアクセスできます。
図 7 必要なパイプライン コンポーネント
ホスト側オブジェクト | アドイン側オブジェクト
ホスト (オブジェクトはここに存在) | アドイン (オブジェクトはここに存在)
ホスト ビュー | アドイン ビュー
ビューからコントラクトへのホスト アダプタ | ビューからコントラクトへのアドイン アダプタ
コントラクト | コントラクト
コントラクトからビューへのアドイン アダプタ | コントラクトからビューへのホスト アダプタ
アドイン ビュー | ホスト ビュー
アドイン (ここからオブジェクトにアクセス) | ホスト (ここからオブジェクトにアクセス)
アドイン側オブジェクト これらのオブジェクトは、アドイン側に存在し、ホストからアクセスされます (ShapeAddIn クラスなど)。ここではパイプライン コンポーネントは互いに似ていますが、ホストからアドイン側オブジェクトにアクセスできるように、アダプタの方向が逆になっています。ビューからコントラクトへのアダプタはホスト側ではなくアドイン側にあり、コントラクトからビューへのアダプタはホスト側にあります。パイプラインの残りの部分 (ビューおよびコントラクト) は同じであることに注意してください。
両側オブジェクト これらのオブジェクトは、どちらの側にも存在でき、どちらの側からでもアクセスできます。両方向に適用する必要があるため、このようなオブジェクトには各側に 2 つ、合計 4 つのアダプタが必要となります。ホストとアドインのビューに同じアセンブリを使用している場合、アダプタも再利用できることに注意してください。その場合、必要なアダプタは 2 つで済みます。
コントラクトは、イベントをネイティブではサポートしていません。ただし、次に示すパターンを使用して、コントラクト内でイベントをシミュレートできます (IShapeApplication.CreatedDrawing イベントの例)。
ホスト ビュー :
public interface IShapeApplication
{
       ...
       // event fired when a drawing is created
       event EventHandler<CreatedDrawingEventArgs> CreatedDrawing;
}
対応するコントラクト :
public interface IShapeApplicationContract : IContract
{
    ...
    void CreatedDrawingAdd(ICreatedDrawingEventHandlerContract handler);
    void CreatedDrawingRemove(ICreatedDrawingEventHandlerContract
        handler);
}

public interface ICreatedDrawingEventHandlerContract : IContract
{
    void Handler(ICreatedDrawingEventArgsContract args);
}
ご覧のとおり、コントラクトでは System.EventHandler<CreatedDrawingEventArgs> のようなデリゲートを使用できないため、イベント ハンドラに対して新しいコントラクト ICreatedDrawingEventHandlerContract を作成しています。
アドイン側アダプタは、Add および Remove メソッドを使用してホスト側にハンドラを登録し、アドインがサブスクライブできるローカル イベントを保持します。ホスト側アダプタは、イベントが発生すると Handler 関数を呼び出します。
このような複雑な追加部分は、アドイン開発者に対しては大部分が透過的ですが、1 つ注意が必要です。アドインが自分自身を登録するときと登録解除するときには、オブジェクトの同じアダプタ インスタンスで行う必要があります。そうしないと、登録解除が有効になりません。
次に、ホスト オブジェクトがどのようにして、それを参照するアダプタ オブジェクトを 2 セット (またはそれ以上) 持つことができるのかを説明します。ホスト側のオブジェクトが (プロパティのアクセスまたは関数呼び出しを通して) アドインに返されると、ホスト オブジェクトへのアクセスを可能にする 2 つのアダプタ オブジェクト (ホスト側アダプタとアドイン側アダプタ) が作成されます。同じオブジェクトが再度アドインに返されると、2 つの新しいアダプタが作成されます。
たとえば、ShapeApplication.ActiveDrawing に 2 回アクセスすると、2 つの異なるオブジェクト参照がアドインに返され、ShapeApplication.ActiveDrawing.ReferenceEquals(ShapeApplication.ActiveDrawing) は false を返します。同じホスト オブジェクトに対して 2 つ (またはそれ以上) のアダプタが存在することは、イベントを登録/登録解除したり、ホスト オブジェクトをコレクションに格納したりするときなどに、問題となる可能性があります。
これらの問題を解決するために、アダプタ上で .Equals および .GetHashCode 関数をオーバーライドして、実際のホスト オブジェクト上の対応する関数を呼び出すようにできます。これにより、ホスト オブジェクトをアドイン内のコレクションに配置することができ、.Contains などのメソッドが適切に機能します。もちろん、その場合でも、アドイン開発者はイベントの登録と登録解除が同じオブジェクト上で行われるよう注意する必要があります。また、.Equals 関数がそのために役立つことも知っておいてください。
既にお気付きかもしれませんが、もう 1 つのオプションとして、アダプタをキャッシュすることも考えられます。ただし、オブジェクトからアダプタへの弱いマッピングを格納する簡単な方法がないため、これは実際には困難です (ここで、"弱い" とは、ガベージ コレクタによって無視される弱いオブジェクト参照のことを指します)。標準の辞書を使用することで、アダプタやオブジェクトに対するガベージ コレクションの実行を避けることができます。

アドインを記述する
パイプラインが準備できたら、アドイン ビューに対してアドインを記述できます。アドインの記述は、非常に簡単です。アドイン開発者が行うのは、アドイン ビューから継承するクラスを作成し、アドイン実装を AddIn 属性でマークすることだけです。残りのコードは、ホストとアドインの間にパイプラインが存在しない場合とほとんど同様に記述できます。ただし、注意すべき点がいくつかあります。1 つは、前に述べたオブジェクトの種類の問題であり、もう 1 つは、どのような分離境界が使用されるかに応じたパフォーマンスの問題です。パフォーマンスのベンチマークについては、go.microsoft.com/fwlink/?LinkId=117527 を参照してください。
現在のところ、アドインがホスト アプリケーションのフォーム上に Windows フォーム コントロールを直接表示することはできません。ただし、次の 3 つの方法のいずれかを使用できます。アドイン独自のフォームを表示するか (インターネット信頼レベルなど、アドインが何らかの部分的な信頼状況で動作している場合には、アドイン フォームの読み込み時にユーザーに対して警告が表示されます)、ホスト アプリケーションのフォーム上で直接 Windows Presentation Foundation (WPF) コントロールを表示するか (go.microsoft.com/fwlink/?LinkId=117528 を参照)、または WPF コンテナにラップされた Windows フォーム コントロールを使用するか (go.microsoft.com/fwlink/?LinkId=117529 を参照) のいずれかです。
Windows フォームでは、スレッドに関していくつかの要件を満たす必要があります。たとえば、コマンドライン アプリケーションを拡張するアドインは、フォームを構築してそのイベントを処理するために新しいスレッドを作成する必要があります。これは、コマンドライン アプリケーションが既定でマルチスレッド アパートメント (MTA) モデルを使用する一方、Windows フォームでは UI スレッドに対してシングルスレッド アパートメント (STA) モデルを必要とするためです。この解決方法は単純です。Windows フォーム アプリケーションを拡張するアドインに、フォームを表示するための次のような 2 行のコードを含めるだけです (アドインがホストの UI スレッドからアクティブ化されて使用されると仮定しています)。
AddInForm form = new AddInForm();
form.Show();

コラボレーション アドイン
公開されたインターフェイスを使用すると、非常に強力なアドインを作成できます。その一例が、このサンプルに含まれるコラボレーション アドインです。このアドインを使用すると、2 つの異なるマシンを使用している 2 人の ShapeApp ユーザーが、リアルタイムで同時に描画を編集できます。コラボレーション アドインは、Windows Communication Foundation (WCF) を通して互いに接続し、この対話がインターネット経由でグローバルに動作します。接続画面のスクリーンショットを図 8 に示します。
図 8 コラボレーション アドインの UI
2 つのアドインが互いに接続されると、どちらかの側で作成されるかまたは開かれた新しいドキュメントがすべて共有されます。つまり、一方のマシン上での変更がリアルタイムでもう一方のマシンに送信されます。これは、イベントを使って実現されます。コラボレーション アドインが読み込まれると、アドインはアプリケーションのすべてのイベントに対してサブスクライブします。新しい描画が作成されると、CreatedDrawing イベントが発生します。アドインはこのイベントを受信し、新しい描画に対するすべてのイベントにサブスクライブします。同様に、図形の作成時には、図形に関連するすべてのイベントにサブスクライブします。これにより、アドインはすべてのユーザー アクションを追跡し、それをもう一方のアドインに伝播できます。
コラボレーション アドインを通してイベントが渡される経路を図 9 に示します。マシン 1 で、ユーザーがアクションを実行します (図形の位置の変更など)。これにより、ホストはイベントを発生させ、コラボレーション アドインがそれを受信します。コラボレーション アドインはメッセージを作成し、WCF 経由でマシン 2 のアドインに送信します。このアドインは、ホスト上で同じアクションを実行します。ホスト上でアクションを実行するときに、アドインがイベント ハンドラを一時的にアンフックすることに注意してください。これにより、イベントがアドインに戻されて無限のサイクルに陥ることを防ぎます。
図 9 コラボレーション アドイン経由でイベントが渡される経路 (クリックすると拡大画像が表示されます)
WCF を使用して、さらに興味深いシナリオを実現できます。コラボレーション アドインはサービスをホストするので、任意の数のクライアントがアドインに接続できます。これにより、3 人以上のユーザーが 1 つの描画に対してシームレスに共同作業できます。3 台のマシン間の接続を図 10 に示します。各マシン上のコラボレーション アドインが、他のすべてのアドインと接続されています。
図 10 ShapeApp の 3 つのインスタンスの接続 (クリックすると拡大画像が表示されます)

以上、.NET アドイン フレームワークを使用してアドインをホストするために ShapeApp を調整する方法を見てきました。アドイン フレームワークの能力や、ShapeApp をリアルタイムの共同エディタへとシームレスに変換するアドインの作成方法について、ある程度の知識が得られたのではないでしょうか。フィードバックや質問などがありましたら、アドイン チームのブログ (go.microsoft.com/fwlink/?LinkId=117530) までお気軽にお寄せください。

Mueez Siddiqui は、マイクロソフトの CLR セキュリティおよび拡張性チームのソフトウェア開発エンジニアです。


データ ポイント
階層型アーキテクチャの Entity Framework
John Papa

このコラムは、ADO.NET Entity Framework のプレリリース版に基づいて書かれています。ここに記載されているすべての情報は、変更される場合があります。

コードのダウンロード : DataPoints2008_07.exe (3,549 KB)
オンラインでのコードの参照
目次
階層を定義する
モデルを構築する
動作のしくみ
変更を保存する
削除および追加を行う
まとめ

n 層アーキテクチャの設計者が新しいテクノロジ、パターン、または方式を評価する場合には、パズルの新しいピースをアーキテクチャにはめ込む方法を検討する必要があります。Entity Framework を使用すれば、統合の問題はなくなります。Entity Framework は n 層アーキテクチャにも単層アーキテクチャにも統合することが可能です。
今月のコラムでは、Windows® Communication Foundation (WCF) テクノロジ、Windows Presentation Foundation (WPF) テクノロジ、および Model View Presenter (MVP) パターンを使用して、n 層アーキテクチャに適した Entity Framework ソリューションを構築する方法について説明します。ここでは、論理ストア データベース、データ アクセス、ドメイン モデル、ビジネス マネージャ、サービス層、プレゼンテーション層、受動的な UI 層の各層を持つサンプル アーキテクチャを紹介し、Entity Framework を使用してこれらの層を統合する方法について説明します。この記事で使用するすべてのコード例は、MSDN® Magazine Web サイトからダウンロードできます。

階層を定義する
これから説明するアプリケーションでは、ユーザーは NorthwindEF サンプル データベースおよびビュー内の顧客を検索して表示、追加、編集、または削除することができます。コードや例の説明に入る前に、サンプルの全体的なアーキテクチャについて説明します。説明の重点はアーキテクチャ自体ではなく、Entity Framework をアーキテクチャ デザインと統合する方法にあるので、変更可能で他の方式と簡単に統合できる比較的一般的なアーキテクチャを選びました。
図 1 に、一般的な階層型アーキテクチャの概要を示します。最上位の 2 層は、UI 層とプレゼンテーション層を使用したユーザー インターフェイスの表示と遷移を処理します。UI 層の実装にはさまざまなテクノロジを使用できますが、この記事およびこの記事で紹介する例では WPF を使用します。UI 層はパッシブ ビューの MVP パターンに従います。これにより、ビュー (最上位の UI 層) はプレゼンテーション層で管理され、遷移先が指定されます。プレゼンタは、ビューにデータを供給し、下位層に保存するデータをビューから取得します。さらに、一般にビューで発生したイベントに応答します。
図 1 アーキテクチャの概要 (クリックすると拡大画像が表示されます)
この記事に示す例では、プレゼンタは WCF 経由で下位層との通信を行います。また、サービスのコントラクトをガイドとして使用し、WCF 経由でサービスを呼び出します。サービス層は、サービス コントラクト インターフェイスでサービスを公開します。サービス コントラクトによってプレゼンタはサービスの呼び出し方法を確認できます。
サービス層は、プレゼンタから情報を受信し、適切なビジネス ロジックを実行してデータの収集や変更を行うための適切なビジネス層のメソッドを呼び出します。このプロジェクトのビジネス ロジックおよび LINQ to Entities コードは、ビジネス層に属します。LINQ to Entities コードは、Entity Framework から生成されたエンティティ モデルを参照します。LINQ クエリを実行すると、LINQ クエリから概念エンティティ モデル (エンティティ データ モデル : EDM) への変換、エンティティ要素からストレージ層へのマップ、およびデータベースに対して実行する SQL クエリの生成が Entity Framework によって行われます。

モデルを構築する
ここまでは、アーキテクチャ内での各層のしくみに関するおおまかな概要を示しました。次に、Entity Framework に関連する各層の主な要素を説明します。アプリケーション用のデータベースは既に存在するので、開始点となるエンティティ モデルを NorthwindEF データベースから生成しました。
先にエンティティ モデルを構築してからエンティティをデータベースにマップすることも可能です。EDM ウィザードは基本エンティティ モデルの生成に役立ちます。生成したモデルを必要に応じて変更し、継承やエンティティ分割などのドメイン モデリングの概念を取り入れることができます。図 2 の EDM ウィザードでは、すべてのテーブルおよびストアド プロシージャが EDM へのインポート対象として選択されています。
図 2 データベースからのモデルの生成 (クリックすると拡大画像が表示されます)
EDM に関して混乱しやすい問題の 1 つに、EntitySets および EntityTypes の既定の名前付け規則があります。筆者は、ドメイン モデルのすべてのエンティティに単数形の名前を使用することを好みます。ここでは、Customer のインスタンスを作成するか、List<Order> を使用して Order インスタンスの一覧を返します。各エンティティは、エンティティを定義するプロパティを持つブループリントの単数形インスタンスです。
一方、EntitySets には複数形の名前付け規則を使用することにします。EntitySets は、多くの場合、LINQ クエリで ObjectContext に一連の Customers または Orders を参照するように指示するときに使用します。
例として、次の LINQ to Entities クエリを参照してください。
var q = from c in context.Customers
        select c;
List<Customer> customerList = q.ToList();
このクエリは、LINQ to Entities に対して、実行時に Customers EntitySet にアクセスしてすべての Customer エンティティ インスタンスを返すように指示します。2 行目でクエリを実行し、customerList というローカル変数に List<Customer> を返します。この例では、EntitySets のクエリを実行して Customer (単数形) エンティティのインスタンスを取得するということがわかりやすいように、EntitySet が複数形になっています。
この名前付け規則は絶対的なものでしょうか。もちろんそうではありません。ただ、この規則に従った方がコードは読みやすくなります。そうでなければ、EDM ウィザードから返される値の既定値を取得する場合、Customers という名前の EntitySets と Customers という名前の EntityType を取得します。この場合の LINQ to Entities クエリは次のようになります。
var q = from c in context.Customers
          select c;
  List<Customers> customerList = q.ToList();
EDM ウィザードでモデルを生成すると、EntitySet および EntityType の名前を簡単に変更できます。それには、図からエンティティを選択し、[プロパティ] ウィンドウにプロパティを表示し、目的の設定を変更します (図 3 を参照)。このアプリケーションでは、Name プロパティを設定して、すべての EntityTypes を単数形に変更しています。EntitySet の Name プロパティは既に単数形になっていたので変更していません。
図 3 EntityType の名前の変更 (クリックすると拡大画像が表示されます)

動作のしくみ
次に、アプリケーションについて説明し、このアプリケーションがビュー (NWUI プロジェクトにあります) およびプレゼンタ (NWPresentation プロジェクトにあります) から始まって上位層から下位層に移行する動作のしくみを説明します。2 つのプロジェクトは、このコラムに付属のコード サンプルに含まれています。アプリケーションは顧客検索ビューを読み込みます。このビューで、ユーザーは会社名を条件にして顧客を検索できます (図 4 を参照)。このビューの実装には WPF を使用し、ユーザーがビューを操作すると、イベントが生成されます。プレゼンタはそのイベントをリスンし、適切なアクションを実行します。
図 4 顧客の検索 (クリックすると拡大画像が表示されます)
図 4 に示すように、D で始まるすべての顧客を検索する場合、ユーザーが [Search] ボタンをクリックすると、ビューでイベントが生成されます。プレゼンタは、WCF 経由でサービス層を呼び出すことにより、このイベントをリスンして応答し、CustomerSearchView に表示する顧客エンティティの一覧を取得します。ユーザーが [Search] ボタンをクリックしたときのビューのコードは次のようになります。
private void btnSearch_Click(object sender,     RoutedEventArgs e)  {
      if (FindCustomerSearchResults != null)          FindCustomerSearchResults();
  }
このコードでは、返されたエンティティの一覧を操作していません。一覧の操作はプレゼンタに任せています。ビューは、エンティティの一覧をリスト ビュー コントロールの要素にバインドする方法を確認するために、WPF のデータ バインドを使用してエンティティのプロパティを参照します。ビューがエンティティに対して行う唯一の操作は、データ バインドを通じて実行されます。
CustomerSearchView が FindCustomerSearchResults イベントを生成すると、CustomerSearchPresenter はこのイベントをリスンし、応答として、制御を受け取って検索を実行します。次のコードは、CustomerSearchPresenter クラスで、下位層で公開される WCF サービスのプロキシとなる NWServiceClient クラスのインスタンスを作成する方法を示しています。
public void view_FindCustomerSearchResults()
{
    if (this.view.CompanyNameCriteria.Length > 0)
        using (var svc = new NWServiceClient())
        {
            IList<Customer> customerList = svc.FindCustomerList(                view.CompanyNameCriteria);
            view.CustomerSearchResultsList = customerList;
        }
}
NWPresentation プロジェクトから ServiceReference を使用して NWServiceClient を参照し、サービスの呼び出し方法と返却するデータの型をプレゼンタに認識させます。プレゼンテーション層では EDM を直接参照していません。このような直接参照は避ける必要があります。代わりに、WCF を通して公開された DataContracts から目的のエンティティ型を取得します。これにより、物理ネットワークの境界を越えて、WCF 経由でプレゼンタに Entity Framework のエンティティを渡すことができます。
取得した Customer エンティティの一覧は、ビューのパブリック プロパティに設定されます。次に、ビューのこのプロパティは List<Customer> を受け取り、それをビューの DataContext にバインドします。WPF、Silverlight®、Windows フォーム、ASP.NET のいずれでも、コードにはテクノロジ固有の要素が多く、プレゼンタがデータを処理してビューに渡すと、ビューはそのビューに固有のバインドを処理します。
この方法により、同じプレゼンタを使用して、ICustomerSearchView インターフェイスを実装する任意のビューを操作できます。このアプリケーションでは、バインドの処理は、DataContext を使用した WPF のバインド方式で行われます。
コントラクトは、サービス層で呼び出すことができるメソッドと取得されたエンティティを公開します。このアプリケーションでは、Customer および Order エンティティ型を返すメソッドのみを用意しています。そのため、コントラクトに含まれるのは、これらのエンティティ型のみです。
WCF は、必要に応じてエンティティに WCF の DataContract 属性を適用することによって、そのエンティティのシリアル化を処理します。DataContracts でエンティティを公開することにより、EDM を直接参照しなくても、エンティティを UI 層で使用できます。
.NET Framework 3.5 SP1 Beta 1 の時点では、Entity Framework はグラフの自動シリアル化をサポートしています。たとえば、親エンティティに関連付けられた子エンティティが存在する場合、その親エンティティと子エンティティはシリアル化されます。サンプル アプリケーションでは、OrderManager の FindOrderList メソッドで各 Order の Order Details を一括で読み込む LINQ to Entities クエリを使用しているので、中間層から返された各 Order エンティティに、ナビゲーション プロパティを使用してアクセス可能な 1 つの List<OrderDetail> が格納されます。
シリアル化されたエンティティはプレゼンタとサービス層との間で WCF を通して受け渡しすることができますが、ObjectContext はシリアル化されず、プレゼンタにも渡されません。そのため、エンティティを UI 層で使用することは可能ですが、ObjectContext は EDM および Entity Framework のすべてのリソースにアクセスできる下位層に残されます。
ObjectContext が残されるということは、このオブジェクトを使用して UI 層でエンティティを直接取得または変更したり、UI 層の変更履歴を管理したりはできないということです。何にしてもこれらの役割は下位層に委ねるのが最適です。ただし、エンティティを下位層に渡すとき、アプリケーションは ObjectContext と同期して、エンティティに変更を保存できるようにする必要があります。
図 4 のように、ユーザーが [Search] ボタンをクリックすると、プレゼンタはサービス層を呼び出した後、NWBusinessManagers プロジェクトにあるビジネス層を呼び出して、List<Customer> を取得します。この層の主な役割は 2 つあります。その 1 つは、EDM からのデータの取得または EDM へのデータの設定です。もう 1 つの役割は、存在する可能性があるビジネス ロジックの処理です。
CustomerManager は、ObjectContext を使用して EDM の操作を処理します。そのために、コンテキストと呼ばれるローカル フィールドを定義し、そのコンテキストのインスタンスをコンストラクタに作成します。ObjectContext は各メソッドで作成および破棄できますが、必要に応じてデータベース接続リソースをオープンまたはクローズするように最適化されています。また、クラス全体で ObjectContext にアクセスできるようにしておけば、クラス内の一連のプライベート メソッドを順次渡していく必要もなく、変更履歴を維持できます。
public CustomerManager()
{
    context = new NWEntities();
}
ただし、この種類のアプリケーションでは ObjectContext を保持するのではなく、必要に応じて作成および破棄してください。ID 解決が原因で、同じオブジェクト コンテキストを保持したままにしておくと、データは不整合で古くなり、ID 解決を行う際のパフォーマンスの低下 (追跡するデータが増えるほど深刻になります) が引き起こされ、これがマルチスレッド環境での更新の問題にもつながります。
次のコードは、ビジネス層の CustomerManager クラスの FindCustomerList メソッドを示しています。このメソッドは、コンテキストにアクセスする LINQ to Entities クエリを宣言し、指定した条件で始まる Customer エンティティの一覧を要求します。このクエリを実行すると、概念層からストレージ層へのマッピングが評価され、適切な SELECT コマンドが生成されます。
public List<Customer> FindCustomerList(string companyName)
  {
      var q = from c in context.Customers
              where c.CompanyName.StartsWith(companyName)
              select c;
      return q.ToList();
  }
SQL Server® Profiler を使用して実行すると、クエリを表示できます。

変更を保存する
ここまではアプリケーションでの単純なデータ取得方法を説明してきました。次に、データの変更を保存する方法を説明します。ユーザーが顧客を編集するとき、CustomerView ビューは適切な Customer エンティティ インスタンスにバインドされているように見えます (図 5 を参照)。CustomerView がプレゼンタに対してイベントを生成すると、プレゼンタは下位層に Customer エンティティ インスタンスを要求します。
図 5 顧客の編集 (クリックすると拡大画像が表示されます)
ユーザーが顧客を変更して保存するとき、図 6 のコードを使用してプレゼンタから下位層にエンティティが渡されます。このコードでは、ユーザーが顧客を追加または変更しているかどうかを評価し、適切なサービス層のメソッドを呼び出して、エンティティを渡しています。
図 6 プレゼンタの SaveCustomer
public virtual void view_SaveCustomer()
{
    Customer customer = view.CurrentCustomer;
    var svc = new NWServiceClient();
    switch (view.Mode)
    {
        case ViewMode.EditMode:
            svc.UpdateCustomer(customer);
            break;
        case ViewMode.AddMode:
            svc.AddCustomer(customer);
            break;
        default:
            break;
    }
    view.CurrentCustomer = FindCustomer();

}
次に、サービス層はビジネス層に制御を渡し、ビジネス層で顧客エンティティがデータベースに保存されます。顧客エンティティは ObjectContext の一部ではなくなったので、まず、次のコードのように ObjectContext の Attach メソッドを使用して 1 つに再結合する必要があります。エンティティがコンテキストにアタッチされたら、エンティティのプロパティは変更済みとしてマークされる必要があります。そのためには、コンテキストの ObjectStateManager を使用して、各プロパティの SetModified メソッドを呼び出します。これで、エンティティの変更がコンテキストに認識され、SaveChanges メソッドを発行できるようになります。SaveChanges メソッドを発行すると、SQL UPDATE コマンドが生成され、このコマンドがデータベースに対して実行されます。
public void UpdateCustomer(Customer customer)
{
    context.Attach(customer);
    customer.SetAllModified(context);     // custom extension method
    context.SaveChanges();
}
UpdateCustomer メソッドのコードでは、SetAllModified<T> という名前を付けた拡張メソッドを使用しています。この拡張メソッドによって、エンティティのすべてのプロパティを簡単に変更済みに設定できます。SetAllModified<T> は、指定されたエンティティ T の ObjectStateEntry のインスタンスを取得した後、エンティティのすべてのプロパティ名の一覧を取得して、各プロパティに対して SetModifiedProperty を呼び出す処理を繰り返します。
public static void SetAllModified<T>(this T entity, ObjectContext  context)
where T : IEntityWithKey
{
    var stateEntry = context.ObjectStateManager.      GetObjectStateEntry(entity.EntityKey);
    var propertyNameList = stateEntry.CurrentValues.DataRecordInfo.      FieldMetadata.Select
      (pn => pn.FieldType.Name);
    foreach (var propName in propertyNameList)
        stateEntry.SetModifiedProperty(propName);
}
エンティティを最終的に保存する別の方法として、コンテキストの Refresh メソッドを呼び出すこともできます。これにより、コンテキストに対して、エンティティのデータを取得し、プロパティの値をデータベースの値で更新するように指示します。ClientWins の RefreshMode 列挙子は、元の値をデータベースから取得した最新の値に変更し、最新のデータが優先されるようにします。
StoreWins の RefreshMode は、エンティティ キャッシュ内の元の値と現在の値を、データベースから取得した値で上書きします。ClientWins が最新データを優先する場合に適した方法であるのに対し、StoreWins は変更を取り消し、UI の表示をデータベース内の最新の値で更新する場合に適しています。
context.Refresh(RefreshMode.ClientWins, customer);  // Last in wins
Entity Framework は、更新および削除コマンドの生成時に、強制的にオプティミスティックな同時実行を行います。この処理を行うために、ConcurrencyMode 属性の値が "Fixed" に設定されているプロパティの WHERE 句に元の値を含めます。
既定では、モデルの生成時に、どのフィールドも同時実行フィールドに指定されません。そのため、ユーザーが変更を保存したとき、別のユーザーの変更が誤って上書きされる可能性があります。CustomerView が開いた状態で別のユーザーが値を変更したときに、オプティミスティックな同時実行を使用する場合は、概念モデルの EntityType の ConcurrencyMode 属性を設定してください。
EDM ファイルを編集して ConcurrencyMode を Fixed に設定すると、Entity Framework に対して、この列を Update または Delete コマンドの WHERE 句に追加するように指示することになります。一致する行が見つからない場合は OptimisticConcurrencyException が生成されます。図 7 は、ユーザーがデータベース内の顧客の地域を変更した直後に別のユーザーが同じ地域を変更しようとして、この例外が生成された場合を示しています。
図 7 OptimisticConcurrencyException (クリックすると拡大画像が表示されます)
この例外をトラップして適切な処理を行うことができます。たとえば、次に示すように例外をトラップしてログに記録し、ユーザーの変更を上書きすることができます。
catch (OptimisticConcurrencyException e){
    context.Refresh(RefreshMode.ClientWins, customer); // Last in wins
    logger.Write(e);
    context.SaveChanges();
}

削除および追加を行う
ユーザーが顧客を削除すると、CustomerManager の DeleteCustomer メソッドが顧客エンティティを取得し、削除を適用します。
context.Attach(customer);
context.DeleteObject(customer);
context.SaveChanges();
まず、Attach メソッドを使用して Customer エンティティ インスタンスを ObjectContext と再結合します。次に ObjectContext から顧客を削除する必要があります。この方法によって、Customer エンティティ インスタンスが削除されたことが ObjectContext 内の変更履歴の記録メカニズムに認識されます。最後に、SaveChanges メソッドを呼び出すと、その時点で ObjectContext はエンティティが削除されたことを認識しており、DELETE SQL コマンドを生成して実行します。
顧客を追加すると、CustomerManager の AddCustomer メソッドで顧客エンティティが取得され、次のように挿入操作が適用されます。
context.AddToCustomers(customer);
context.SaveChanges();
このエンティティ インスタンスは新規のインスタンスなので、Customer エンティティのコンテキストに追加し、Customer エンティティの新規インスタンスとしてフラグを設定する必要があります。そのためには、AddToCustomer メソッドを使用して Customer エンティティ インスタンスを ObjectContext に関連付けます。最後に、SaveChanges メソッドを呼び出すと、ObjectContext はエンティティが追加されたこと、したがって INSERT SQL コマンドを生成して実行する必要があることを認識しています。

まとめ
この記事では、Entity Framework をアーキテクチャに統合する方法、MVP パターンなどの最新パターンを使用する方法、および一般的なアーキテクチャの問題に対処する方法について説明しました。階層型アーキテクチャにおける Entity Framework の主要な構成要素には、変更履歴の記録メカニズム、LINQ to Entities との統合、ObjectContext の接続解除および再接続機能、開発者が同時実行の問題に対処するための手段があります。

ご質問やご意見は、John (mmdata@microsoft.com) まで英語でお送りください。

John Papa (johnpapa.net) は、ASPSOFT (aspsoft.com) の上級コンサルタントです。野球を熱狂的に愛し、夏の夜を家族と共にヤンキースの応援に費やします。C# の MVP であり、INETA の講演者でもある John は、何冊かの書籍を発表しており、現在も最新作の『Data Access with Silverlight 2』を執筆中です。また、主にカンファレンス (DevConnections、VSLive など) での講演で活躍しています。


Advanced Basics
LINQ Enumerable クラス (第 1 部)
Ken Getz

コードのダウンロード : AdvancedBasics2008_07.exe (184 KB)
オンラインでのコードの参照
目次
Enumerable の内部
シーケンスを作成する
シーケンスを選択する
単一の要素を取得する
シーケンスをフィルタ処理する
シーケンスを順序付けする
シーケンスを検証する
最近のプログラミング プロジェクトの中で、筆者は 1 から 100 までの整数をランダム順に並べたリストを作成する必要がありました。これは間違いなく基本的なコンピュータ サイエンス入門の問題です。最初は 1985 年に授業で作成したのと同じソリューションを使用し、ループや比較を多用して、乱数と複数の配列を生成しました。しかし、最近の宣言型プログラミングに比べて、このソリューションはいかにも煩雑です。
少し調査した後、不具合の部分を削ってわずか 2 行のコードに集約できました。
Dim rnd As New System.Random()
Dim numbers = Enumerable.Range(1, 100). _
  OrderBy(Function() rnd.Next)
この 2 行を実行すると、numbers 変数に 1 から 100 までの整数がランダム順に格納されます。
単純でありながら洗練されたこのソリューションの動作は、System.Linq.Enumerable クラスに依存します。このクラスには拡張メソッドと呼ばれる数十個の共有メソッドがあり、それらのメソッドを使用して、IEnumerable(Of T) インターフェイスを実装したクラスのデータを操作できます。また、この例のように、共有メソッドでデータの新しいコレクションを生成することもできます。
今回と次回のコラムでは、Enumerable クラスの拡張メソッドの例を示します。メンバのオーバーロードの複数バージョンを示し、さまざまなデータ構造体を使用して機能を説明します。これらの拡張メソッドはさまざまな場合に応用でき、非常に便利なので、時間を割いて各メソッドを詳しく検討する価値があります。そうすることで、コレクションを操作する必要があるときにどのようなツールを使用できるかがわかります。
単純な Windows® ベースのアプリケーションをサンプルとして作成しました。このプロジェクトでは、Customers、Categories、および Products のデータを表す単純なクラスと、これらのクラスのインスタンスに SQL Server® Northwind サンプル データベースのデータを設定する SimpleDataContext というクラスを追加しています。また、このクラスは Customers、Categories、および Products というパブリック プロパティを公開し、各プロパティは Customer、Category、および Product インスタンスの汎用リストを保持します (LINQ to SQL デザイナを使用して "本物" の DataContext クラスを作成することもできましたが、それでは Enumerable クラスの拡張メソッドの使用例を示すことができません。つまり、LINQ to SQL のコンテキストで前述のメソッドを呼び出すと、実際には、ここに示したメソッドと同じシグネチャを持ち、LINQ to SQL プロバイダの Queryable クラスの拡張メソッドであるメソッドが呼び出されます。そのような例では、動作が同じでも Enumerable クラスは使用されません)。サンプル アプリケーションをダウンロードし、このアプリケーションを使用しながら記事を読み進めることを強くお勧めします。コードを読むだけでなく直接使用してみることに意義があります。
メソッドの長いリストを扱いやすくするため、メソッドを論理的なグループに分割しました。これから、単一の要素の作成、選択、取得と、シーケンス内でのフィルタ処理、変換、配置、さらにはデータのシーケンスの順序付け、検証、計算、およびセット操作の実行を処理するメソッドを詳しく見ていきます。以下の各セクションで、Enumerable クラスの 1 つまたは複数のメソッドを使用するさまざまな例を実際に試してください。最後の例まで試すことで、Enumerable クラスのすべてのメソッドを使用し、複雑なメソッドのオーバーロードを示す多くの例を詳しく検証できます。
ここに示す例では、通常、Enumerable クラスの拡張メソッドを呼び出したときの戻り値型をコードで指定するのではなく、Visual Basic® でデータ型を推定できるようになっています。また、サンプル コードでは SQL Express 上にインストールした Northwind サンプル データベースを使用しています。サンプルを実行する場合は、これと同じ環境を用意するか、サンプル アプリケーションを変更する必要があります。

Enumerable の内部
Enumerable クラスは、作成するすべての LINQ クエリで重要な役割を果たします。たとえば、プロジェクトにサンプル プロジェクトと同様の Product クラスと Product インスタンスの汎用リストが含まれているとします。サンプルから SimpleDataContext クラスのインスタンスを作成し、Products リストにデータを入力して、次のようなクエリを記述します。
Dim db As New SimpleDataContext()
Dim query = _
  From prod In db.Products _
  Where prod.CategoryID = 1 _
  Order By prod.ProductName _
  Select prod.ProductName
これをよく見たとき、次のことに気付いた読者もいるでしょう。つまり、SQL に似たこの構文を特定の型のインスタンスでの一連のメソッド呼び出しに変換する処理を Visual Basic コンパイラで内部的に行う必要があります。実際、この例では、SimpleDataContext クラスの Products プロパティは List(Of Product) 型です。また、既におわかりかもしれませんが、List(Of T) 型は IEnumerable(Of T) インターフェイスを実装します。LINQ のキーワードはコンパイラによって対応する Enumerable 型の拡張メソッドの呼び出しに変換されます。具体的には、前の LINQ クエリを次のように書き直すことによって、まったく同一の一連の命令を作成できます。
Dim query = _
  db.Products _
  .Where(Function(prod) CBool(prod.CategoryID = 1)) _
  .OrderBy(Function(prod) prod.ProductName) _
  .Select(Function(prod) prod.ProductName)
コンパイラによって作成された中間言語 (IL) を見ると、Visual Basic のキーワードを使用した LINQ クエリで、Visual Basic のキーワードが Enumerable クラスの対応するメソッドの呼び出しに変換されていることがわかります。この変換の結果として System.Collections.Generic.List(Of T) クラスの動作が拡張されます。実際のところ、その判別は簡単です。この特定のクラスのメンバに関するドキュメントで拡張メソッドの一覧を参照すると、ほとんどの拡張メソッドが Enumerable クラスで定義されていることがわかります。
これは何を意味するのでしょうか。Enumerable クラスの拡張メソッドは、他の多くのクラス (Array や List など) を処理できます。Enumerable クラスのメソッドは、LINQ クエリを作成するだけでなく、配列などのデータ構造体の動作を操作するためにも使用できます。
Enumerable クラスの拡張メソッドのドキュメントを読むと、各メンバが共有メンバであることが記載されています。結果的に、メソッドの呼び出しには 2 つの選択肢があります。1 つは宣言型のメソッドとして呼び出す方法であり、次のように、拡張されたクラスのインスタンスを渡します (results 変数が、Count メソッドで処理できる型であると想定)。
Dim itemCount = _
  Enumerable.Count(results, _
  Function(prod) prod.CategoryID = 3)
もう 1 つは、Count をコレクション自体のインスタンス メソッドと同様に扱って呼び出す方法です。この方法で拡張メソッドを呼び出す場合、第 1 パラメータを指定する必要はありません。
Dim itemCount = _
  results.Count(Function(prod) _
  prod.CategoryID = 3)
Visual Basic と C# は、キーワードから拡張メソッド呼び出しへの変換方法が異なります。C# よりも Visual Basic の方が、LINQ クエリ内で使用できるキーワードが多数あります。たとえば、Visual Basic では、次のように Take キーワードを使用して LINQ クエリを作成できます。
Dim db As New SimpleDataContext()
Dim query = _
  From prod In db.Products _
  Where prod.CategoryID = 1 _
  Order By prod.ProductName _
  Take 10 _
  Select prod.ProductName
または、次のように Enumerable クラスの Take 拡張メソッドを使用することもできます。
Dim query = _
  (From prod In db.Products _
  Where prod.CategoryID = 1 _
  Order By prod.ProductName _
  Select prod.ProductName).Take(10)
C# には Take キーワードがないため、Take メソッドを明示的に呼び出す以外に方法はありません。図 1 は、Visual Basic の LINQ クエリ キーワードと対応する Enumerable クラスのメソッドのマッピングを示しています。
図 1 LINQ キーワードと Enumerable メソッドとの対応
Visual Basic の LINQ クエリ キーワード | Enumerable のメソッド
Aggregate … Into … All | All
Aggregate … Into … Any | Any
Aggregate … Into … Average | Average
Aggregate … Into … Count | Count
Aggregate … Into … LongCount | LongCount
Aggregate … Into … Max | Max
Aggregate … Into … Min | Min
Aggregate … Into … Sum | Sum
Distinct | Distinct
Group By | GroupBy
Group Join | GroupJoin
Order By | OrderBy
Order By … Descending | OrderByDescending
Order By (複数フィールドの場合) | ThenBy
Order By … Descending (複数フィールドの場合) | ThenByDescending
Select | Select
Skip | Skip
Take | Take
Take While | TakeWhile
Where | Where

シーケンスを作成する
Enumerable クラスには、新しいシーケンスを作成するために存在する、拡張メソッドではないいくつかの共有メソッドがあります。これらの単純なメソッドを検証することから始めます。
Enumerable.Range メソッドは、整数の新しい順序付きリストを作成します。リストの開始値と項目数を指定すると、IEnumerable(Of Integer) 型のシーケンスが返されます。このメソッドを使用して、開始値と終了値の間の数字をランダム順に並べたリストを取得するという、この記事の基本問題を解決できます。
次のコードでは、Range メソッドを使用してリストを設定し、単純なラムダ式で Enumerable.OrderBy メソッドを呼び出して、シーケンスをランダム順に並べます。
' From RangeDemo in the sample:
Dim rnd As New System.Random
Dim items = Enumerable.Range(1, 10)
Dim randomList = _
  items.OrderBy(Function() rnd.Next())
サンプル プロシージャを複数回実行したとき、返される結果は毎回異なりますが、コードを実行するたびに次のようなリストが得られます。
6, 8, 7, 1, 2, 9, 10, 4, 3, 5
Enumerable.Reverse メソッドは、入力シーケンスを逆順に返します。次のコードは、Reverse メソッドを Enumerable クラスの共有メソッドとして呼び出した場合と、Enumerable.Range メソッドの呼び出しから返されたリストのインスタンス メソッドとして呼び出した場合を示しています。
'From ReverseDemo in the sample:
Dim items = Enumerable.Range(1, 10)
Dim reversed = items.Reverse()
reversed = Enumerable.Reverse(items)
どちらの方法で Reverse メソッドを呼び出しても、返されるリストは次のとおりです。
10, 9, 8, 7, 6, 5, 4, 3, 2, 1
Enumerable.Repeat メソッドは、指定の値を指定の回数だけ繰り返したリストを作成します。
'From RepeatDemo in the sample:
Dim repeated = Enumerable.Repeat("Hello", 5)
前述のコードを呼び出すと、repeated 変数に次の項目が格納されます。
Hello, Hello, Hello, Hello, Hello
Enumerable.Empty メソッドは、空の IEnumerable(Of T) を作成し、指定したデータ型を格納するように設定するときに使用します。このメソッドを使用してコレクションの空のインスタンスを作成し、必要に応じてそのインスタンスに項目を追加できます。コレクションを空のコレクションとして初期化すると、参照が Nothing であるかどうかを考慮する必要がありません。
'From EmptyDemo in the sample:
Dim emptyList = _
 Enumerable.Empty(Of Customer)()

シーケンスを選択する
Enumerable クラスには、一連の要素を選択し、その要素を入力シーケンスから出力シーケンスに射影するための 2 種類の拡張メソッドがあります。Enumerable.Select メソッドは、各要素を新しい形式に単純に射影します。次の例では、Northwind Customers テーブルから米国の顧客のシーケンスを取得し、その結果を顧客名の番号付きリストのシーケンスに射影します。
' From SelectDemo in the sample:
Dim db As New SimpleDataContext
Dim results = _
  From cust In db.Customers _
  Where cust.Country = "USA"
Dim selectResults = results.Select( _
  Function(cust, index) _
  String.Format("{0}. {1}", _
  index + 1, cust.ContactName))
Select メソッドに複数のオーバーロード バージョンがあることに注意してください。このコードを実行すると、selectResults に次のような値のリストが格納されます。
1. Howard Snyder
2. Yoshi Latimer
3. John Steel
4. Jaime Yorres
5. Fran Wilson
6. Rene Phillips
7. Paula Wilson
8. Jose Pavarotti
9. Art Braunschweiger
10. Liz Nixon
11. Liu Wong
12. Helvetius Nagy
13. Karl Jablonski
Enumerable.SelectMany メソッドは、項目のコレクションを受け取り (その各項目が項目のコレクションである場合がある)、2 次元の入力を 1 次元の出力シーケンスに変換します。このメソッドは、たとえば、カテゴリのリスト (各カテゴリに製品が含まれている) を受け取り、対応するカテゴリ情報を含む製品リストを返す場合に便利です。次の例では、含まれる製品数が 7 個未満であるカテゴリのリストを取得し、その結果を射影して、各製品に関する情報とカテゴリ情報を含む 1 つのリストを返します。
' From SelectManyDemo in the sample:
Dim db As New SimpleDataContext
Dim categories = _
  From cat In db.Categories Where cat.Products.Count < 7
Dim manyResults As IEnumerable(Of String) = _
  categories.SelectMany(Function(cat, index) _
  cat.Products.Select( _
  Function(prod As Product) String.Format("{0}. Category {1}: {2}", _
    index, prod.CategoryID, prod.ProductName)))
この場合、SelectMany メソッドに渡したラムダ式が特定のカテゴリへの参照を受け取り、各カテゴリを順番に処理します。SelectMany メソッドのこの特定のオーバーロードにより、カテゴリのインデックスとカテゴリをラムダ式に渡します。カテゴリとインデックスを受け取ったラムダ式がカテゴリの Products プロパティの Select メソッドを呼び出し、カテゴリに含まれる各製品がメソッドで処理されます。このサンプルでは次の文字列リストを含むシーケンスが返されます。SelectMany の呼び出しによって、元のカテゴリと製品の階層が単一の値リストにフラット化されます。
0. Category 6: Mishi Kobe Niku
0. Category 6: Alice Mutton
0. Category 6: Thüringer Rostbratwurst
0. Category 6: Perth Pasties
0. Category 6: Tourtière
0. Category 6: Pâté chinois
1. Category 7: Uncle Bob's Organic Dried Pears
1. Category 7: Tofu
1. Category 7: Rössle Sauerkraut
1. Category 7: Manjimup Dried Apples
1. Category 7: Longlife Tofu

単一の要素を取得する
シーケンスから単一の要素を取得して処理することが必要な場合が多くあります。Enumerable クラスには、シーケンスの内容をフィルタ処理して単一の要素を取り出すメソッドがいくつかあります。Enumerable.First メソッドはシーケンスの最初の要素を返し、Enumerable.Last メソッドはシーケンスの最後の要素を返します。Enumerable.Single メソッドは、単一の要素を指定した関数またはラムダ式を受け取り、1 つの要素を返します。Enumerable.ElementAt メソッドは、シーケンス内の特定のインデックス位置にある単一要素を返します。
要求した要素がシーケンスに存在しない場合、または Single メソッドでの制限の結果として要素が 0 個しか存在しないか複数個存在する場合、これらの各メソッドは例外をスローします。また、これらの各メソッドでは、ラムダ式または関数を指定して入力データを制限することもできます。いずれの場合も、返されるシーケンスに含まれる要素は 1 つでなければなりません。
Enumerable クラスの同様のメソッドに、可能な場合は単一要素を返し、単一要素を返すことができない場合はシーケンスの型の既定メンバを返すメソッドもあります。これに該当するメソッドは、Enumerable.FirstOrDefault、Enumerable.LastOrDefault、Enumerable.SingleOrDefault、および Enumerable.ElementAtOrDefault です。図 2 は、これらのメソッドの動作を示しています。サンプルを実行すると、次のデータが出力ストリームに書き込まれます。
ALFKI: Maria Anders
ALFKI: Maria Anders
Last in USA: Karl Jablonski
Sequence contains no elements
Specified argument was out of the range of valid values.
Parameter name: index
CustomerID = XXXXX is Nothing: True
Customer 2 in USA is Nothing: False
Customer 200 in USA is Nothing: True
図 2 Enumerable メソッドでの要素の取得
' From SingleElementDemo in the sample:

Dim oneCustomer As Customer
Dim sw As New StringWriter

Dim db As New SimpleDataContext

Dim query0 = From cust In db.Customers

Dim query1 = _
  From cust In db.Customers _
  Where cust.CustomerID = "ALFKI"

Dim query2 = _
  From cust In db.Customers _
  Where cust.Country = "USA"

Dim query3 = _
  From cust In db.Customers _
  Where cust.CustomerID = "XXXXX"

' This works fine, finds the customer whose CustomerID is ALFKI:
oneCustomer = query1.Single
sw.WriteLine("ALFKI: {0}", oneCustomer.ContactName)

' You can supply an expression to filter, when calling
' the First, Last, Single, and so on, methods:
oneCustomer = query0.Single( _
  Function(cust) cust.CustomerID = "ALFKI")
sw.WriteLine("ALFKI: {0}", oneCustomer.ContactName)

' This works fine, finds the last customer in the USA:
oneCustomer = query2.Last
sw.WriteLine("Last in USA: {0}", oneCustomer.ContactName)

' These two raise exceptions:
Try
  ' There's no customer use customer ID is "XXXXX":
  oneCustomer = query3.Single
Catch ex As Exception
  sw.WriteLine(ex.Message)
End Try

Try
  ' There aren't 200 customers in the USA:
  oneCustomer = query2.ElementAt(200)
Catch ex As Exception
  sw.WriteLine(ex.Message)
End Try

' These don't raise exceptions, but return default values
' if they fail:
oneCustomer = query3.SingleOrDefault
sw.WriteLine("CustomerID = XXXXX is Nothing: {0}", _
             oneCustomer Is Nothing)

' Because there are at least two customers in the USA,
' method returns a valid, single customer:
oneCustomer = query2.ElementAtOrDefault(2)
sw.WriteLine("Customer 2 in USA is Nothing: {0}", _
             oneCustomer Is Nothing)

oneCustomer = query2.ElementAtOrDefault(200)
sw.WriteLine("Customer 200 in USA is Nothing: {0}", _
             oneCustomer Is Nothing)

シーケンスをフィルタ処理する
Enumerable.Where メソッド、Enumerable.Distinct メソッド、または Enumerable.OfType メソッドを使用して既存のシーケンスの内容をフィルタ処理し、出力シーケンスで元のデータのサブセットを返すことができます。OfType メソッドは、指定された型に基づいて入力シーケンスをフィルタ処理します。フォーム上の特定の型のコントロールのみを操作する場合を考えてみましょう。この場合、OfType メソッドを使用して、フォームで公開されるコントロールのコレクションを制限し、該当するコントロールのサブセットのみを反復処理することができます。
次の例では、汎用リストにデータを設定し、整数の出力リストを取得して、文字列のリストを取得します。
' From OfTypeDemo in the sample:
Dim items As New List(Of Object)
items.Add("January")
items.Add(0)
items.Add("Monday")
items.Add(3)
items.Add(5)
items.Add("September")

Dim numbers = items.OfType(Of Integer)()
Dim strings = items.OfType(Of String)()
このコードを実行すると、2 つの出力リストに次のデータが格納されます。
0, 3, 5
January, Monday, September
Enumerable.Where メソッドでは、入力シーケンスをフィルタ処理する条件を指定できます。2 つ目のオーバーロード バージョンとして、コレクションの各項目のインデックスにアクセスし、インデックスに基づいてフィルタ処理を行うことができます。次の例では、Where メソッドと Select メソッドの両方を使用します。最初にフィルタ処理を行い、次に結果を射影して、C:\Windows フォルダ内の 100 バイトより小さいファイルのシーケンスを取得します。
' From WhereDemo in the sample:
Dim files As IEnumerable(Of FileInfo) = _
  New DirectoryInfo("C:\Windows").GetFiles()
Dim fileResults = _
  files _
  .Where(Function(file) file.Length < 100) _
  .Select(Function(file) _
    String.Format("{0} ({1})", file.Name, file.Length))
配列は IEnumerable インターフェイスを実装するため、Where メソッドを使用して他のコレクションと同様に配列の内容もフィルタ処理できます。この場合、コードで GetFiles メソッドによって返される FileInfo オブジェクトの配列をフィルタ処理します。また、例では、Where メソッドの呼び出しの後に Select メソッドの呼び出しをカスケードしています。筆者のコンピュータでこのサンプルを実行したところ、次の結果が返されました。
Addrfixr.ini (62)
bthservsdp.dat (12)
iltwain.ini (36)
S86D5A060.tmp (48)
setuperr.log (0)
Where メソッドの 2 つ目のオーバーロードを使用して、処理対象の各入力項目のインデックスにアクセスできます。たとえば、次の Where メソッドを呼び出すと、サイズが 100 バイト未満で、最初の 20 個の入力ファイルのいずれかであるファイルが返されます。
' From WhereDemo in the sample:
fileResults = _
  files _
  .Where(Function(file, index) _
    (file.Length < 100) And (index < 20)) _
  .Select(Function(file) _
    String.Format("{0} ({1})", file.Name, file.Length))
この場合、筆者のコンピュータでの実行結果は次のようになりました (不明なファイルは、処理された最初の 20 個のファイルから除外されています)。
Addrfixr.ini (62)
bthservsdp.dat (12)
iltwain.ini (36)
Enumerable.Distinct メソッドを使用すると、フィルタ処理を実行し、入力リストの一意の項目のみを出力シーケンスに格納できます。文字列や数値などの単純な値の場合、この処理は簡単です。
では、複雑なオブジェクト リストの場合、ランタイム エンジンは、オブジェクトが重複しているかどうかを判断するためにどのように比較を行うのでしょうか。この場合、単純にインスタンス変数を比較しても、オブジェクトのメモリ アドレスが比較されるだけなので意味がありません。複雑なオブジェクトの場合は比較処理を指定する必要があります。つまり、比較を実行するための IEqualityComparer(Of T) を実装するクラスのインスタンスを指定します。この問題は、Enumerable クラスの複数のメソッドに当てはまるため、このコラムで後ほどもう一度説明します。
たとえば、顧客情報を含むシーケンスが存在し、顧客の所在国を示す一意のリストが必要だとします。単純な所在国リストがあれば、既定の比較処理を使用して文字列を比較できます。ところが、ここにあるのは顧客リストです。Select メソッドを使用してリストを射影し、所在国のリストだけを取得することも可能ですが、その場合、リストを使用してその他の計算を行うための残りのすべてのデータが失われます。
サンプル プロジェクトには、IEqualityComparer(Of Customer) インターフェイスを実装する図 3 のような CustomerCountryComparer クラスがあります。次のサンプル コードは Distinct メソッドを使用する異なる 2 つの方法を示しています。
図 3 IEqualityComparer
Public Class CustomerCountryComparer
  Implements IEqualityComparer(Of Customer)

  Public Function Equals1( _
    ByVal x As Customer, ByVal y As Customer) As Boolean _
    Implements IEqualityComparer(Of Customer).Equals

    Return x.Country.Equals(y.Country)

  End Function

  Public Function GetHashCode1( _
    ByVal obj As Customer) As Integer _
    Implements IEqualityComparer(Of Customer).GetHashCode

    Return obj.Country.GetHashCode

  End Function
End Class
最初の方法では、顧客リストをまず国名のリストに射影し、次に既定の文字列比較処理を使用して、国名リストを一意なデータのリストに集約しています。2 つ目の方法では、明示的に CustomerCountryComparer クラスを使用して比較を実行し、一意なリストを作成して、重複のない顧客リストを文字列のリストに射影しています。
' From DistinctDemo in the sample:
Dim db As New SimpleDataContext

Dim countries As IEnumerable(Of String) = _
  db.Customers _
  .Select(Function(cust) cust.Country) _
  .Distinct()

countries = customers _
  .Distinct(New CustomerCountryComparer) _
  .Select(Function(cust) cust.Country)
必要に応じて空のシーケンスを適切に処理します。Enumerable.DefaultIfEmpty メソッドは入力シーケンス全体を返すか、シーケンスが空の場合は既定値のインスタンスを返します。このメソッドを使用すると、シーケンスの型を持つ項目が常に少なくとも 1 つ存在するようになります。
図 4 のプロシージャは、DefaultIfEmpty メソッドの 1 つの使用例を示しています。このコードでは、noCustomers 変数に空のリストを設定し (国名が "XXX" の顧客は存在しません)、DefaultIfEmpty メソッドを呼び出す noCustomers1 リストを作成します。このサンプル コードを実行すると、次の出力が生成されます。
noCustomers contains 0 element(s). noCustomers1 contains 1 element(s).
XXXXX
図 4 DefaultIfEmpty の使用
' From DefaultIfEmptyDemo in the sample:
Dim db As New SimpleDataContext
Dim sw As New StringWriter

Dim noCustomers = _
  From cust In db.Customers Where cust.Country = "XXX"
Dim noCustomers1 = _
  noCustomers.DefaultIfEmpty()

Dim results = _
  String.Format( _
  "noCustomers contains {0} element(s). " & _
  "noCustomers1 contains {1} element(s).", _
  noCustomers.Count, noCustomers1.Count)
sw.WriteLine(results)

' You can specify the exact value to use if the
' collection is empty:
noCustomers1 = noCustomers.DefaultIfEmpty( _
  New Customer With {.CustomerID = "XXXXX"})
For Each cust In noCustomers1
  sw.WriteLine(cust.CustomerID)
Next
noCustomers1 には 1 つの要素 (情報が設定されていない 1 件の Customer) が格納されています。2 番目のメソッド呼び出しのように、シングルトン項目の既定値を指定して DefaultIfEmpty メソッドを呼び出すことができます。

シーケンスを順序付けする
Enumerable.OrderBy、Enumerable.ThenBy、Enumerable.OrderByDescending、および Enumerable.ThenByDescending を使用して最初の順序付けを指定し、次に、2 番目以降の 1 つまたは複数の順序付け (昇順または降順) を指定できます。このサンプルでは、顧客のシーケンスから開始して、これらのメソッドのいくつかを使用して順序付けを適用します。
' From OrderByDemo in the sample:
Dim db As New SimpleDataContext
Dim customers = _
  From cust In db.Customers _
  Where cust.ContactTitle = "Owner"

' Sort first by country, then by city descending, then by name:
Dim results = _
  customers _
  .OrderBy(Function(cust) cust.Country) _
  .ThenByDescending(Function(cust As Customer) cust.City) _
  .ThenBy(Function(cust As Customer) cust.ContactName) _
  .Select(Function(cust) String.Format("({0}, {1}) {2}", _
  cust.Country, cust.City, cust.ContactName))
Visual Basic では、ThenBy メソッドおよび ThenByDescending メソッドの呼び出し時にラムダ式のパラメータのデータ型を指定しなくてもエラーになりませんが、データ型を指定しない場合、IntelliSense® の機能を利用できません。最初の順序付けメソッドのみがラムダ式のパラメータのデータ型を推定できます。おかしなことですが、Select メソッドでは、長大に連鎖したメソッドのリストの最後であってもデータ型が正しく推定されます。このサンプルでは、肩書きが "Owner" の顧客を最初に国順、次に都市の降順、最後に各都市の連絡先名順に並べ替えます。
(Denmark, Kobenhavn) Jytte Petersen
(France, Paris) Marie Bertrand
(France, Nantes) Janine Labrune
(France, Marseille) Laurence Lebihan
(Germany, Köln) Henriette Pfalzheim
(Mexico, México D.F.) Ana Trujillo
(Mexico, México D.F.) Antonio Moreno
(Mexico, México D.F.) Miguel Angel Paolino
(Norway, Stavern) Jonas Bergulfsen
(Poland, Warszawa) Zbyszek Piestrzeniewicz
...

シーケンスを検証する
アプリケーションによっては、シーケンス内で特定の条件が満たされているかどうかを検証する必要があります。条件を満たしている要素が存在するか。すべての要素が条件を満たしているか。特定の項目がシーケンス内に存在するか。2 つのシーケンスは等しいか。Enumerable クラスには、このすべての情報を取得するメソッドがあります。
シーケンスに要素が 1 つでも存在するかどうかを確認するには、パラメータを指定しないで Enumerable.Any メソッドを呼び出します。条件を指定し、その条件に該当する要素がシーケンス内に存在するかどうかを確認するには、このメソッドに関数を渡します。入力シーケンスのいずれかの要素が存在するか、指定された条件に一致する要素が存在する場合、True が返されます (図 5 を参照)。
図 5 シーケンスの検索
' From AnyDemo in sample:
Dim db As New SimpleDataContext
Dim results = _
  From product In db.Products _
  Where product.CategoryID = 1

' Determine if a list has any elements:
Dim anyElements = results.Any()

' Call as extension method or shared method
' of IEnumerable:
Dim matchingElements = _
  results.Any(Function(prod) prod.ProductName.StartsWith("M"))
matchingElements = _
  Enumerable.Any(results, _
  Function(prod As Product) prod.ProductName.StartsWith("M"))
シーケンスのすべてのメンバが条件を満たしているかどうかを確認するには、条件を指定する関数を渡して Enumerable.All メソッドを呼び出します。次の例では、結果コレクション内のすべての要素の UnitsInStock の値が 5 より大きいかどうかを確認しています。
' From AllDemo in sample:
' Is it true that all products have more than 5 units in stock?
Dim db As New SimpleDataContext
Dim results = _
  From product In db.Products _
  Where product.CategoryID = 1
Dim allElements = _
  results.All(Function(prod) CBool(prod.UnitsInStock > 5))
シーケンスに特定の要素が含まれているかどうかを確認するには、Enumerable.Contains を呼び出します。検索対象が文字列や整数などの単純な値である場合、既定の比較処理を使用し、検索する値を指定するだけで済みます。より複雑なオブジェクトがシーケンス内に存在するかどうかを確認する場合は、IEqualityComparer(Of T) を実装するクラスのインスタンスを指定する必要があります。
図 6 のサンプル コードは、どちらも Enumerable.Contains の呼び出し例を示しています。最初の例は、単純にリスト内に数字の 5 が含まれているかどうかを確認し、2 番目の例は、"Chai" に対応する要素が製品リスト内に存在するかどうかを確認します (この例では、前述のコードと同じ ProductComparer クラスを使用しています)。
図 6 シーケンス内の項目の検索
' From ContainsDemo in the sample:
' Simple check to see if list contains a value:
Dim numbers = Enumerable.Range(1, 10)
Dim contains5 = numbers.Contains(5)

' More complex objects require more complex comparison:
Dim db As New SimpleDataContext
Dim results = _
  From product In db.Products _
  Where product.CategoryID = 1

Dim item As New Product _
  With {.ProductID = 1, .ProductName = "Chai"}
Dim containsChai = results.Contains(item, New ProductComparer())
2 つのシーケンスが等しいかどうかを確認するには、Enumerable.SequenceEqual メソッドを呼び出します。Enumerable.Contains メソッドの場合と同様、単純な値から成る 2 つのシーケンスを比較する場合は既定の比較処理を使用し、より複雑なオブジェクトから成る 2 つのシーケンスを比較する場合はカスタムの比較処理を使用します。
2 つのシーケンスに同じ型が含まれていない場合、または 2 つのシーケンスの長さが異なる場合、比較は直ちに失敗します。2 つのシーケンスに同じ型が含まれていて、2 つのシーケンスの長さが等しい場合、Enumerable.SequenceEqual メソッドは指定された比較処理を使用して各項目を比較します。図 7 は、Enumerable.SequenceEqual メソッドの両方の呼び出し方法を示しています。どちらの場合も戻り値は False です。
図 7 シーケンスの比較
' From SequenceEqualDemo in sample:
Dim rnd As New Random
Dim start = rnd.Next

Dim count = 10
Dim s1 = Enumerable.Range(start, count)

' Choose a different random starting point:
start = rnd.Next
Dim s2 = Enumerable.Range(start, count)
Dim sequencesEqual = s1.SequenceEqual(s2)
sw.WriteLine("sequencesEqual = {0}", sequencesEqual)

' What if there's no default comparer? Must use your own:
Dim products1 = _
  From prod In db.Products _
  Where prod.CategoryID = 1
Dim products2 = _
  From prod In products1 _
  Where prod.UnitPrice > 5

sequencesEqual = _
  products1.SequenceEqual(products2, New ProductComparer())
sw.WriteLine("sequencesEqual = {0}", sequencesEqual)
次回のコラムでは、便利なセットベースの操作を行うおもしろいメソッドなど、Enumerable クラスのその他のメソッドに重点を置いて説明します。そのときまでに、サンプル アプリケーションをダウンロードして、Enumerable クラスのメソッドを試してみてください。Visual Studio® 2008 のデータ構造体を使用した処理は、それらのメソッドによって確実に強化されます。

ご質問やご意見は、Ken (basics@microsoft.com) まで英語でお送りください。

Ken Getz は、MCW Technologies のシニア コンサルタントであり、AppDev (www.appdev.com) のコースウェアの作成者でもあります。Ken は、『ASP.NET Developers Jumpstart』(Addison-Wesley、2002 年)、『Access Developer's Handbook』(Sybex、2002 年)、および『VBA Developer's Handbook, 2nd Edition』(Sybex、2001 年) の共著者です。連絡先は keng@mcwtech.com (英語) です。


Office Space
自動化された SharePoint サイトのブランド化
Ted Pattison

目次
LitwareBranding ソリューションについて
ブランド管理のユーティリティ クラスを作成する
マスタ ページを入れ替える
Web.config ファイルに HttpModule を登録する
機能のアクティブ化時にブランド化を初期化する
SharePoint® テクノロジを使用して作業するようになってから、よく受ける依頼があります。それは、企業では SharePoint の標準サイトを使用するのではなく、独自のサイトを作成するということです。SharePoint テクノロジを使用してインターネット上の企業 Web サイトを作成しているにせよ、何百ものチームの共同作業サイトを抱えるイントラネット環境を作成しているにせよ、ページ レイアウトやフォントと色の使用など、SharePoint サイトの視覚的な面を管理するように依頼されることがあります。
Windows® SharePoint Services (WSS) 3.0 では、テーマによるブランド化を行うことができます。ユーザーは、サイトに WSS テーマを適用してその外観を変更できます。WSS におけるテーマと ASP.NET におけるテーマはまったく異なるものであることに注意してください。WSS では、Web サーバーのファイル システムから現在のサイトのコンテキストにカスケード スタイル シート (CSS) ファイルをコピーして、各ページからこの CSS ファイルに動的にリンクを設定することで、テーマのインフラストラクチャを実装します。
WSS テーマには制限事項があることから、開発者にとってはあまりおもしろくないものとなっています。最初の制限事項は、WSS テーマはユーザー設定のマスタ ページを統合してサイト内で使用されるページ レイアウトをカスタマイズできないということです。さらに、WSS テーマを適用できるのはサイト コレクション全体に対してではなく各サイトに対してのみです。サイト コレクション内のすべてのサイトに対してテーマを一度に適用することはサポートされていません。したがって、サイト コレクション全体で一貫した外観および機能を維持するには、各サイトに個別にアクセスして同じテーマを適用する必要があります。
SharePoint Designer には、WSS または Microsoft® Office SharePoint Server (MOSS) 2007 でサイトのブランド化を行うための別の手段が備わっています。SharePoint Designer を使用すると、サイトのマスタ ページをカスタマイズして、カスタム CSS ファイルを編集および統合できます。ただし、WSS の標準サイトに基づいてサイト コレクションで作業する場合、SharePoint Designer にはすべてのサイトを同期させて同じバージョンのカスタマイズされたマスタ ページを使用する方法がありません。このため、マスタ ページの編集内容を複数のマスタ ページ ファイルにコピーして貼り付けなければならないというシナリオに陥る可能性があります。
MOSS では、発行ポータルを使用することで Web Content Management (WCM) インフラストラクチャを提供しているため、SharePoint Designer を MOSS と共に使用すると事態は良くなります。MOSS 発行ポータルは、サイト コレクションの 1 つで、すべてのサイト全体でページを自動的に同期させて同じマスタ ページおよび同じ CSS ファイルを使用できる発行サイトが含まれています。
ただし、複数のサイト コレクション全体または企業シナリオでの使用を目的とするブランド化ソリューションを作成する場合には、SharePoint Designer の使用には特筆に値する制限事項がいくつかあることに注意することが必要です。SharePoint Designer での変更は常に、特定のサイトのコンテキスト内で 1 回限りのカスタマイズとして行われます。これは、SharePoint Designer で行われたカスタマイズをサイト コレクション間またはファーム間で移行するのは困難または不可能だということです。また、SharePoint Designer で行われたサイトのカスタマイズを Visual Studio® Team System などのソース コード管理システムに統合するのは実用的ではありません。
今月は、SharePoint サイト向けのカスタム ブランド化ソリューションの開発について詳しく説明し、ユーザー設定のマスタ ページとカスタム CSS ファイルをサイト コレクションのレベルで統合するために WSS の機能を活用します。Visual Studio プロジェクトを使用して開発され、ソリューション パッケージを使用して展開されている LitwareBranding というブランド化ソリューションのサンプル内のコードについて説明します。すべてのコードは、今月号のコード サンプルに含まれています。
Visual Studio を使用してソリューション パッケージを生成することで、WSS 3.0 または MOSS 2007 を実行するすべてのファームにブランド化ソリューションを展開することが可能になります。また、各マスタ ページおよび CSS ファイルについて複数のサイト コレクションまたはファーム全体で再利用が可能な 1 つのソース ファイルを作成することが容易になります。さらに、Visual Studio のアプローチではすべてのソース ファイルをソース コード管理システムにチェックインできるため、SharePoint Designer を使用するよりもさらに魅力的です。また、作業を開発ファームからテスト用のステージング ファームへ、そして実稼動へとさらに容易に移動できます。

LitwareBranding ソリューションについて
今月のコラムには、LitwareBranding という Visual Studio のサンプル プロジェクトが付属しています。図 1 に、Visual Studio での LitwareBranding プロジェクトの構造を示します。ご覧のとおり、このソリューションは 3 つの機能を使用するように設計されています。今月のこのコラム全体で、これらの機能の目的および実装について説明します。このソリューションの中心的な機能は LitwareBranding という名前で、WSS または MOSS サイト コレクションのスコープ内でユーザーによってアクティブ化されるように設計されています。LitwareBranding 機能をアクティブ化することで、今月のコラムで取り上げるすべてのブランド化技法が LitwareBranding ソリューションによって自動的に適用されます。
図 1 Visual Studio 内の LitwareBranding ソリューション
まずは、図 2 に示す LitwareBranding という中心的な機能の feature.xml を見ていくことからチュートリアルを始めましょう。LitwareBranding 機能は、Site という Scope 属性値で定義されています。これは、スコープがサイト コレクション レベルに設定されていることを指します。つまり、Litware の企業ブランド化を現在のサイト コレクション内のすべてのサイトに適用するには、サイト コレクションの所有者が機能を 1 つだけアクティブ化できるようにするという考えです。
図 2 feature.xml
<Feature
  Id="065E2243-B968-4F14-BAAE-610BB975EFB7"
  Title="A sample feature: LitwareBranding"
  Description="Demoware created for Ted Pattison's OfficeSpace column"
  Hidden="FALSE"
  Scope="Site"
  ImageUrl="LitwareBranding\AfricanPith32.gif"
  ReceiverAssembly="LitwareBranding, [4-part assembly name]"
  ReceiverClass="LitwareBranding.FeatureReceiver"
  xmlns="http://schemas.microsoft.com/sharepoint/">

  <ElementManifests>
    <ElementManifest Location="elements.xml"/>
    <ElementManifest Location="stapling.xml" />
    <ElementFile Location="LitwareBranding.master"/>
  </ElementManifests>

</Feature>
LitwareBranding 機能には、LitwareBranding.master というマスタ ページと、elements.xml という要素マニフェストが含まれています。elements.xml マニフェストには、このマスタ ページ テンプレートのインスタンスを作成する宣言型 XML プロビジョニング ロジックが含まれています。このロジックは、File 要素を内部に含む Module 要素によって定義されます。
<Module Name="MasterPages"
        Path="" List="116"
        Url="_catalogs/masterpage" >

  <File Url="LitwareBranding.master"
        Type="GhostableInLibrary" />

</Module>
機能をアクティブ化する間に、この Mudule 要素によって WSS は最上位レベルのサイトのマスタ ページ ギャラリーにある LitwareBranding.master というマスタ ページ テンプレートのインスタンスを強制的に準備します。このマスタ ページ インスタンスの準備が行われると、WSS オブジェクト モデルに対してこのマスタ ページにリンクするサイト コレクション全体のページを同期するように指示するコードを提供できます。これについては、次のセクションで示します。

ブランド管理のユーティリティ クラスを作成する
LitwareBranding プロジェクトには、BrandManager というクラスが含まれています。このユーティリティ クラスは、WSS オブジェクト モデルに対してさまざまなブランド化要素を適用または削除するように指示するすべてのコードをカプセル化します。次の作業を行う静的メソッドが BrandManager クラスによって公開されています。
  • サイト ページを同期して、LitwareBranding.master にリンクする。
  • ページを同期して、代替 CSS ファイルを使用する。
  • ページを同期して、サイト ロゴにカスタム グラフィックを使用する。
  • サポート機能を追加して、アプリケーション ページ用マスタ ページを入れ替える。
特定のメンバについて掘り下げる前に、図 3 をご覧ください。ここに、BrandManager クラスのすべてのメンバを示します。このクラスには、マスタ ページや CSS ファイルなどのさまざまなリソースを指す URL を解析して結合するための静的プロパティがいくつか含まれています。たとえば、SiteCollectionUrl プロパティは現在のサイト コレクションへの Web アプリケーション相対パスを返します。DefaultMasterPageUrl は、現在のサイト コレクションで最上位レベルのサイトのマスタ ページ ギャラリーにある default.master という既定マスタ ページへの Web アプリケーション相対パスを返します。このパスは、SiteCollectionUrl プロパティを使用して次のサイト相対パスに解析されて結合されます。
_catalogs/masterpage/default.master
図 3 BrandManager クラス
public class BrandManager {
  // read-only properties
  public static string SiteCollectionUrl
  public static string DefaultMasterPageUrl
  public static string CustomMasterPageUrl
  public static string CustomCssUrl
  public static string CustomSiteLogoUrl
  // utility branding methods
  public static void ConfigureMasterUrl(bool ApplyMasterUrl) {}
  public static void ConfigureCustomMasterUrl(bool ApplyCustomMasterUrl) {}
  public static void ConfigureAlternateCss(bool ApplyCustomCss) {}
  public static void ConfigureSiteLogo(bool ApplySiteLogo) {}
  public static void ConfigureApplicationPageMaster(bool ApplyApplicationPageMaster) {}
}
CustomMasterPageUrl は、現在のサイト コレクションでマスタ ページ ギャラリーにある LitwareBranding.master というマスタ ページ インスタンスへの Web アプリケーション相対パスを返します。このパスは、SiteCollectionUrl プロパティを使用して次のサイト相対パスに解析されて結合されます。
_catalogs/masterpage/litwarebranding.master
それでは、ConfigureMasterUrl メソッドを確認します。ConfigureMasterUrl は、現在のサイト コレクション内のすべてのサイトを列挙して、各サイトの MasterUrl プロパティがユーザー設定のマスタ ページまたは既定のマスタ ページを指すように更新します。
public static void ConfigureMasterUrl(bool ApplyMasterUrl) {
  // determine MasterUrl property setting
  string MasterUrlPath =
    (ApplyMasterUrl ?
    CustomMasterPageUrl :
    DefaultMasterPageUrl);

  // update MasterUrl property for all sites
  foreach (SPWeb site in SPContext.Current.Site.AllWebs) {
    site.MasterUrl = MasterUrlPath;
    site.Update();
  }
}
MasterUrl プロパティは、~masterurl\default.master の MasterPageFile プロパティ設定を使用して作成されているすべてのサイト ページに影響します。これには、チーム サイト、空のサイトなどの多くの SharePoint サイト テンプレートのホーム ページとなる標準の default.aspx ページ テンプレートがあります。また、標準の WSS リスト タイプによって使用される AllItems.aspx、NewItem.aspx などのフォーム ページもあります。
MasterUrl プロパティを更新しても、MOSS 発行サイトのページ レイアウトは影響を受けません。これには、MOSS 発行サイトの Pages ライブラリ内のすべてのコンテンツ ページが含まれます。
代わりに、これらのコンテンツ ページは、MasterPageFile 属性の動的トークンを使用するように設計されています。この動的トークンは ~masterurl\custom.master の形式に従います。このトークンは、MasterUrl プロパティではなく CustomMasterUrl プロパティを使用するように置き換えられているため、~masterurl\custom.master トークンとは異なります。したがって、BrandManager クラスは、MOSS 発行サイトの Pages ライブラリで作成されたコンテンツ ページのマスタ ページを置き換えるために使用される ConfigureCustomMasterUrl という 2 番目のメソッドを提供します。
public static void ConfigureCustomMasterUrl(
  bool ApplyCustomMasterUrl) {
  // determine MasterUrl property setting
  string CustomMasterUrlPath =
    (ApplyCustomMasterUrl ?
    CustomMasterPageUrl :
    DefaultMasterPageUrl);

  // update MasterUrl property for all sites
  foreach (SPWeb site in SPContext.Current.Site.AllWebs) {
    site.CustomMasterUrl = CustomMasterUrlPath;
    site.Update();
  }
}
LitwareBranding ソリューションはマスタ ページを入れ替えるだけでなく、styles.css というカスタム CSS ファイルの統合も行います。LitwareBranding ソリューションは、LAYOUTS ディレクトリ内に入れ子になっているディレクトリに styles.css という CSS ファイルを展開することで、各フロントエンド Web サーバーのファイル システム上に 1 回のみ展開するという戦略を使用します。LAYOUTS ディレクトリ内部に展開することで、style.css などのファイルを Web サーバーのファイル システム上に 1 回展開するだけで済み、CustomCssUrl プロパティによって返される標準の URL を使用して現在のファーム内のすべてのサイトからアクセスできるという価値があります。
public static string CustomCssUrl {
  get {
    return "/_layouts/1033/STYLES/LitwareBranding/styles.css";
  }
}
BrandManager クラスには、CustomCssUrl プロパティを使用して現在のサイト コレクション内の各サイトの AlternateCSS プロパティに URL を割り当てる ConfigureAlternateCss というメソッドがあります。
public static void ConfigureAlternateCss(bool ApplyCustomCss) {
// determine MasterUrl property setting
  string AlternateCssUrl =
    (ApplyCustomCss ?
    CustomCssUrl :
    string.Empty);

  // update AlternateCssUrl for all sites
  foreach (SPWeb site in SPContext.Current.Site.AllWebs) {
    // make sure no theme is enabled
    site.ApplyTheme(string.Empty);
    // apply custom CSS file
    site.AlternateCssUrl = AlternateCssUrl;
    site.Update();
  }
}
WSS に組み込まれているブランド化機能にも、ページの左上隅部分に表示される標準の WSS サイト ロゴを置き換える簡単な方法が備わっています。LitwareBranding ソリューションは、IMAGES ディレクトリ内に入れ子になっているディレクトリに展開されている SiteLogo.gif というカスタム グラフィック ファイルを含めることでこの機能を活用します。styles.css という CSS ファイルと同様に、SiteLogo.gif というグラフィック ファイルもまた、CustomSiteLogoUrl プロパティによって返される URL を使用して現在のファーム内のすべてのサイトからアクセスできるように展開されます。
public static string CustomSiteLogoUrl {
  get {
    return "/_layouts/images/LitwareBranding/SiteLogo.gif";
  }
}
BrandManager クラスの ConfigureSiteLogo メソッドは、現在のサイト コレクションの各サイトを列挙して、SiteLogoUrl プロパティを更新するように記述されています。
public static void ConfigureSiteLogo(bool ApplySiteLogo) {
  // determine SiteLogoUrl property setting
  string SiteLogoUrl =
    (ApplySiteLogo ?
    CustomSiteLogoUrl :
    string.Empty);

  // update AlternateCssUrl for all sites
  foreach (SPWeb site in SPContext.Current.Site.AllWebs) {
    site.SiteLogoUrl = SiteLogoUrl;
    site.Update();
  }
}

マスタ ページを入れ替える
SharePoint ではサイト ページ用マスタ ページの入れ替えは比較的容易に行うことができますが、標準の WSS サイト設定ページ (settings.aspx) など、LAYOUTS ディレクトリから実行される組み込みアプリケーション ページ用マスタ ページを入れ替える方法は明らかではありません。これらのアプリケーション ページは、WSS で LAYOUTS ディレクトリ内部に展開されている application.master という標準の WSS マスタ ページにリンクしています。ただし、サイト コレクション内部のすべてのサイト ページ用マスタ ページを入れ替えるブランド化ソリューションを作成する場合は、settings.aspx などのアプリケーション ページでもカスタマイズされたマスタ ページを使用するようにマスタ ページを入れ替えることで、一貫性のある外観を作成できます。
LitwareBranding ソリューションでは、application.master という標準のマスタ ページ ファイルにリンクするすべてのアプリケーション ページ用マスタ ページを入れ替える技法を示します。マスタ ページの入れ替えに使用されるこの技法では、PreInit という ASP.NET ページ ライフサイクルのイベントの 1 つについてイベント ハンドラを登録するカスタム HttpModule が実装されます。このイベントは、ASP.NET プログラミング モデルでは、ページ要求の初期処理中にマスタ ページを入れ替えるたびに実装する必要があります。
LitwareBrandingHttpModule クラスには、マスタ ページの入れ替えのために LitwareBranding ソリューションで使用される HttpModule があります (図 4 を参照)。この HttpModule クラスは、Init メソッド内で PreRequestHandlerExecute イベントのハンドラを登録します。PreRequestHandlerExecute イベント ハンドラのメソッド実装内で、HttpModule クラスは要求が ASP.NET の Page クラスから派生する HttpHandler オブジェクトに基づいているかどうかを判別します。要求が Page から派生したオブジェクトに基づいている場合に限り、HttpModule クラスは PreInit イベントのイベント ハンドラを登録します。PreRequestHandlerExecute イベント ハンドラのコードはまた、WSS でアプリケーション ページを処理する場合は、着信した要求が _layouts ディレクトリ内のページを対象としていることを確認します。
図 4 アプリケーション ページ用マスタ ページを入れ替える
using System;
using System.Web;
using System.Web.UI;
using Microsoft.SharePoint;

namespace LitwareBranding {
  public class LitwareBrandingHttpModule : IHttpModule {

    public void Init(HttpApplication context) {
      context.PreRequestHandlerExecute
        += new EventHandler(context_PreRequestHandlerExecute);
    }

    void context_PreRequestHandlerExecute(object sender, EventArgs e) {
      Page page = HttpContext.Current.CurrentHandler as Page;
      if (page != null) {
        // register handler for PreInit event
        page.PreInit += new EventHandler(page_PreInit);
      }
    }

    void page_PreInit(object sender, EventArgs e) {
      // IF – requested page links to application.master
      // THEN – dynamically modify page to link to custom .master file
    }

    public void Dispose() { /* empty implementation */ }

  }
}
HttpModule を個々のサイト コレクションの WSS ファームに展開できないことを覚えておくことは重要です。代わりに、HttpModule は、Web アプリケーション レベルでオール オア ナッシングの方法として設定する必要があります。ただし、Web アプリケーションには数百のサイト コレクションが含まれていることがあり、LitwareBranding 機能を有効にしているのはそのうちの特定のサイト コレクションのみの可能性があります。したがって、HttpModule クラスの PreInit イベント ハンドラは、現在のサイト コレクションがアプリケーション ページ用マスタ ページを入れ替える動作によって構成されているかどうかを判別できる必要があります。
LitwareBranding ソリューションでは、サイト コレクションの最上位レベルのサイトでカスタム プロパティを作成して、アプリケーション ページ用マスタ ページの入れ替えが可能になっていることを示すことでこの問題を解決します。BrandManager クラス内で定義されている ConfigureApplicationPageMaster メソッドの実装を調べます。
public static void ConfigureApplicationPageMaster(
  bool ApplyApplicationPageMaster) {
  SPWeb TopLevelSite = SPContext.Current.Site.RootWeb;
  if (ApplyApplicationPageMaster) {
    TopLevelSite.Properties["UseCustomApplicationPageMaster"] = "True";
  }
  else {
    TopLevelSite.Properties["UseCustomApplicationPageMaster"] = "False";
  }
  TopLevelSite.Properties.Update();
}
このメソッドは、UseCustomApplicationPageMaster というカスタム プロパティを最上位レベルのサイトで作成して、このプロパティに値 True を割り当てます。これで、HttpModule 内の PreInit イベント ハンドラのメソッド実装は、アプリケーション ページ マスタの入れ替えが可能であるように設定されているかどうかを確認するために、現在のサイト コレクション内でこのプロパティを検索できます。PreInit イベント ハンドラは、現在の要求のコンテキスト内でマスタ ページを入れ替えることが適切かどうかを判別するために、これ以外の確認もいくつか行うことに注意してください (図 5 を参照)。
図 5 PreInit イベント ハンドラ
void page_PreInit(object sender, EventArgs e) {
  Page page = sender as Page;
  if ((page != null) &&
      (page.MasterPageFile != null) &&
      (page.Request.Url.AbsolutePath.Contains("_layouts")) &&
      (SPContext.Current != null)  ) {
    // inspect UseCustomApplicationPageMaster property
    SPWeb site = SPContext.Current.Site.RootWeb;
    string UseCustomApplicationPageMaster =
      site.Properties["UseCustomApplicationPageMaster"];
    if ((!string.IsNullOrEmpty(UseCustomApplicationPageMaster)) &&
       (UseCustomApplicationPageMaster.Equals("True"))) {
      // now replace application.master with customized version
      if (page.MasterPageFile.Contains("application.master")) {
        page.MasterPageFile = "/_layouts/LitwareBranding/application.master";
      }
    }
  }
}
PreInit イベント ハンドラが、アプリケーション ページ用マスタ ページの入れ替えが可能であるように現在のサイト コレクションが設定されていて、現在のページが application.master にリンクされていると判断すると、次のパスのような LAYOUTS ディレクトリのソリューション固有のディレクトリにあるユーザー設定のマスタ ページを使用するように、現在の ASP.NET の Page オブジェクトの MasterPageFile プロパティを変更します。
/_layouts/LitwareBranding/application.master

Web.config ファイルに HttpModule を登録する
WSS および MOSS を使用したビジネス ソリューションを展開する場合は、開発作業をソリューション パッケージに含めて配布することをお勧めします。たとえば、LitwareBranding の Visual Studio プロジェクトをビルドすると、LitwareBranding.wsp というソリューション パッケージが 1 つ作成されます。これによって、このソリューションを WSS 3.0 または MOSS 2007 を実行するファームすべてに展開することが容易になり信頼性が向上します。ただし、LitwareBranding ソリューションでは、LitwareBranding 機能をアクティブ化するサイト コレクションを実行する Web アプリケーションごとに web.config ファイルで HttpModule エントリを追加することが必要です。
特定の Web アプリケーション内の web.config ファイルを更新するために、LitwareBranding ソリューションでは LitwareBrandingWebApplication という 2 番目の機能を使用します。この機能のスコープは、Web アプリケーション レベルに設定されています。またこの機能は、特定の Web アプリケーションのスコープ内部でアクティブ化または非アクティブ化するときにイベント ハンドラを起動する機能レシーバ クラスで構成されています。
<Feature
  Id="FF739C76-0B08-4bc2-A3A2-F61524B492D8"
  Title="Litware Branding Support Feature (WebApplication)"
  Scope="WebApplication"
  Hidden="False"
  ReceiverClass="LitwareBranding. FeatureReceiverWebApplication"
  ReceiverAssembly="LitwareBranding, [4-part assembly name]"
  xmlns="http://schemas.microsoft.com/sharepoint/">

  <!-- no declarative elements -->
  <ElementManifests />

</Feature>
他の機能レシーバ クラスと同様に、FeatureReceiverWebApplication クラスは SPFeatureReceiver クラスから継承し、FeatureInstalled、FeatureActivated、FeatureDecactivating、および FeatureUninstalling という 4 つのハンドラ メソッドをオーバーライドします。FeatureActivated の実装によって、現在の Web アプリケーションの web.config ファイルに HttpModule エントリが追加されます。FeatureDeactivating の実装によって、HttpModule エントリを削除することでこの逆の操作を行います。
web.config ファイルにエントリを追加する必要がある場合は、WSS オブジェクト モデルの SPWebConfigModification クラスを使用することをお勧めします。FeatureReceiverWebApplication クラスには、CreateHttpModuleModification というユーティリティ メソッドがあります。このメソッドは、SPWebConfigModification のインスタンスを作成および初期化して、それをこのメソッドの戻り値として返します (図 6 を参照)。
図 6 CreateHttpModuleModification
public SPWebConfigModification CreateHttpModuleModification() {
  SPWebConfigModification modification;
  string ModName = "add[@name='LitwareBrandingModule']";
  string ModXPath = "configuration/system.web/httpModules";
  modification = new SPWebConfigModification(ModName, ModXPath);
  modification.Owner = "LitwareBranding";
  modification.Sequence = 0;
  modification.Type =
    SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
  modification.Value =
    @"<add name=""LitwareBrandingModule"" " + @ "type="
    "LitwareBranding.LitwareBrandingHttpModule, [4-part assembly name]"
    " />";
  return modification;
}
初期化された SPWebConfigModification オブジェクトを返す CreateHttpModuleModification などのユーティリティ メソッドがあれば、FeatureActivated、FeatureDeactivating などのイベント ハンドラからこのメソッドを呼び出すだけで、現在の Web アプリケーションの web.config ファイルに必要な HttpModule エントリを追加したり、web.config ファイルからエントリを削除したりできます (図 7 を参照)。
図 7 HttpModule を有効または無効にする
public override void FeatureActivated(
  SPFeatureReceiverProperties properties) {
  SPWebApplication WebApp =
    (SPWebApplication)properties.Feature.Parent;
  WebApp.WebConfigModifications.Add(
    CreateHttpModuleModification());
  WebApp.WebService.ApplyWebConfigModifications();
  WebApp.WebService.Update();
}

public override void FeatureDeactivating(
  SPFeatureReceiverProperties properties) {
  SPWebApplication WebApp =
    (SPWebApplication)properties.Feature.Parent;
  WebApp.WebConfigModifications.Remove(
    CreateHttpModuleModification());
  WebApp.WebService.ApplyWebConfigModifications();
  WebApp.WebService.Update();
}

機能のアクティブ化時にブランド化を初期化する
それでは、すべてのパーツを組み合わせてみましょう。ユーザーが特定のサイト コレクション内で LitwareBranding 機能をアクティブ化する場合には、さまざまなブランド化要素のすべてを適用するために BrandManager クラスを使用する FeatureActivated イベント ハンドラを使用する機能レシーバがあります。また、機能の非アクティブ化時にすべてのブランド化要素を削除する FeatureDeactivating イベント ハンドラもあります (図 8 を参照)。
図 8 ブランド化要素を適用または削除する
public override void FeatureActivated(SPFeatureReceiverProperties properties) {
  EnsureWebApplicationFeatureEnabled();
  BrandManager.ConfigureMasterUrl(true);
  BrandManager.ConfigureCustomMasterUrl(true);
  BrandManager.ConfigureAlternateCss(true);
  BrandManager.ConfigureSiteLogo(true);
  BrandManager.ConfigureApplicationPageMaster(true);
}

public override void FeatureDeactivating(SPFeatureReceiverProperties properties) {
  BrandManager.ConfigureMasterUrl(false);
  BrandManager.ConfigureCustomMasterUrl(false);
  BrandManager.ConfigureAlternateCss(false);
  BrandManager.ConfigureSiteLogo(false);
  BrandManager.ConfigureApplicationPageMaster(false);
}
FeatureActivated イベント ハンドラの先頭で、EnsureWebApplicationFeatureEnabled メソッドが呼び出されていることに注意してください。このメソッドは、アプリケーション ページ用マスタ ページを入れ替える HttpModule が ASP.NET に適切に登録されるように、LitwareBrandingWebApplication という Web アプリケーション レベルの機能を確実にアクティブ化するために記述されています。
public void EnsureWebApplicationFeatureEnabled() {
  // make sure feature which adds HttpModule to web.config is active
  SPSecurity.RunWithElevatedPrivileges(delegate() {
    using (SPSite siteCollection = new SPSite(SPContext.Current.Site.ID)) {
      try {
        Guid FeatureId = new Guid("FF739C76-0B08-4bc2-A3A2-F61524B492D8");
        siteCollection.WebApplication.Features.Add(FeatureId);
      }
      catch { }
    }
  });
}
サイト コレクションをスコープとするソリューションを作成する場合には、WSS および MOSS にはさらに別の問題があります。子サイトが作成されるときに、子サイトのブランド化の適切な初期化をどう処理するかを決定する必要があります。BrandManager クラス内部のコードは、既存のサイトのブランド化プロパティにのみ影響します。したがって、LitwareBranding ソリューションには、FeatureActivated イベントの次のイベント ハンドラを含む LitwareBrandingChildSiteInitializer という 3 番目の機能があります。
// fired whenever a new site is created
public override void FeatureActivated(SPFeatureReceiverProperties properties) {
  SPWeb ChildSite = (SPWeb)properties.Feature.Parent;
  SPWeb TopLevelSite = ChildSite.Site.RootWeb;
  ChildSite.MasterUrl = TopLevelSite.MasterUrl;
  ChildSite.CustomMasterUrl = TopLevelSite.CustomMasterUrl;
  ChildSite.AlternateCssUrl = TopLevelSite.AlternateCssUrl;
  ChildSite.SiteLogoUrl = TopLevelSite.SiteLogoUrl;
  ChildSite.Update();
}
このイベント ハンドラは、機能のアクティブ化時に起動して、最上位レベルのサイトのブランド化プロパティを現在の子サイトにコピーします。ここで、LitwareBranding 機能がアクティブ化されているサイト コレクション内部で新しい子サイトが作成されたときに、この機能を自動的にアクティブ化する方法を考える必要があります。
答えは機能を関連付けることです。LitwareBranding 機能には、GLOBAL サイト定義に LitwareBrandingChildSiteInitializer 機能を関連付ける FeatureSiteTemplateAssociation 要素があります。
<!-- staple GLOBAL site definition to   itwareBrandingChildSiteInitializer -->
<FeatureSiteTemplateAssociation
  Id="1204A425-D105-46c5-BB2C-473A2F27B563"
  TemplateName="GLOBAL" />
この関連付けの技法によって、機能のアクティブ化が強制的に自動で行われます。GLOBAL サイト定義に LitwareBrandingChildSiteInitializer 機能を関連付けることで、どのサイト テンプレートを使用して新しいサイトを作成したとしても、この機能を自動的にアクティブ化するよう設定することになります。
空のサイトという標準の WSS サイト テンプレートは、GLOBAL サイト定義への関連付けを行う機能には関係がないことに注意してください。この動作は空のサイト テンプレートから作成されたサイトが、MOSS コンテンツ展開戦略に沿って動作するように SharePoint に追加されたものです。空のサイト テンプレートから作成されたサイトで LitwareBrandingChildSiteInitializer 機能のアクティブ化を自動化する場合は、このサイト定義および構成にも明示的な関連付けの命令を追加する必要があります。
<!-- staple blank site template to LitwareBrandingChildSiteInitializer -->
<FeatureSiteTemplateAssociation
  Id="1204A425-D105-46c5-BB2C-473A2F27B563"
  TemplateName="STS#1" />

Ted へのご意見やご質問は、mmoffice@microsoft.com まで英語でお送りください。

Ted Pattison は、フロリダ州タンパ在住で、執筆、教育活動に従事しています。また、SharePoint MVP でもあります。Ted は Microsoft Press 発行の『Inside Windows SharePoint Services 3.0』の著者でもあります。彼の会社 Ted Pattison Group (www.TedPattison.net) では、企業の開発者を対象とする SharePoint の上級者向けトレーニングを実施しています。


Cutting Edge
HTML Message パターン
Dino Esposito

コードのダウンロード : CuttingEdge2008_07.exe (514 KB)
オンラインでのコードの参照

目次
AJAX サービス層
HTML UI の一般的なパターン
拡張された BST の実装
InnerHTML と DOM
HTML Message パターン
パフォーマンスと設計に関する考慮事項
DynamicPopulate エクステンダ
現時点でのソリューション
真の AJAX アーキテクチャの特徴は、プレゼンテーション層とサービス層が明確に分けられていることです。先月号で説明したように、AJAX フロント エンドと AJAX バック エンド間の明示的なコントラクトの存在により、プログラミングの可能性は広がりますが、アーキテクチャに関する数多くの疑問も生じます (msdn.microsoft.com/magazine/cc546561 を参照)。先月は、クライアント側のデータ バインドとテンプレート、およびブラウザ側テンプレート (BST) パターンの実装について説明しました。また、Web クライアントのユーザー インターフェイスをレンダリングするための代替モデルとして、HTML Message (HTM) パターンについても簡単に紹介しました。今月号では、BST の拡張された実装について説明し、それを HTM ソリューションと比較します。

AJAX サービス層
ここでは、一般的な AJAX プレゼンテーション層のサーバー側の部分のことを AJAX サービス層と呼び、通常、標準的な多階層アーキテクチャのプレゼンテーション層と中間層の間にあるコンタクト ポイントのことを表すサービス層とは区別します。図 1 にこのモデルを示します。
図 1 AJAX フロント エンドを含む一般的な多階層システム (クリックすると拡大画像が表示されます)
AJAX サービス層とクライアント フロント エンド間の通信は、HTTP エンドポイントを通じて行われます。これらのエンドポイントは、Windows® Communication Foundation (WCF) サービスにより公開され、クライアント ページに埋め込まれた JavaScript プロキシ クラスによって呼び出されます。特に ASP.NET AJAX がリファレンス プラットフォームの場合、AJAX サービス層でのサービスの設計と利用はそれほど問題にはなりません。問題が生じるのは、ユーザー インターフェイスを作成または更新するときです。
特に、JavaScript ベースのデータ バインドやテンプレートなど、クライアント上のデータを効果的に操作するための強力なツールが必要になります。このコンテキストでは、ASP.NET の部分的なレンダリングは、実際のアーキテクチャのシフトを伴わない短期的なソリューションにすぎません。ただし、ASP.NET の部分的なレンダリングを利用することで、プログラムによるユーザー インターフェイスの生成を行わずに済みます。部分的なレンダリングにより、ビュー ステートとサーバー ページ ライフサイクルは維持されます。また、コントロールやプロパティを使用し、宣言でユーザー インターフェイスを設計できるようになります。
このようなモデルは、比較的シンプルな Web サイトには最適かもしれませんが、AJAX フロント エンドが多階層サービス指向システムの最上位層となるようなエンタープライズ シナリオにおいて効果的かどうかは疑問です。図 1 に示すように、AJAX サービス層は JavaScript のフロント エンドと一連のビジネス サービスの間にある中間層です。この層は、Customer to Business (C2B) シナリオにおけるコア サービスを保護するセキュリティ バリアを追加し、JavaScript オブジェクトとの間のデータを管理することで、2 つの層の間のインピーダンス不整合を解消します。
AJAX アーキテクチャの AJAX サービス層が果たす重要な役割のため、アーキテクトや開発者は、生データ (コア サービスによって返され、AJAX サービス層で処理されるデータ) に基づく HTML ユーザー インターフェイスに対処するという問題を避けて通ることができなくなります。では、生の JavaScript データに基づいてブラウザ ユーザー インターフェイスを動的に作成、更新するにはどうすればよいのでしょうか。

HTML UI の一般的なパターン
長年にわたる ASP.NET サーバー コントロールを利用した作業のために、HTML ユーザー インターフェイスを構築するうえで何が本当に必要なのかがあいまいになっている可能性があります。カスタム コントロールの開発経験のある方は覚えておられるでしょうが、それは結局のところ、HTML マークアップをバッファに累積し、それを応答ストリームに出力することです。この一般的なパターン以外の方法はありません。付加的な作業は、エラーの発生を抑え、管理しやすくするためだけに行います。そのため、HTML が累積されるバッファは、HTML リテラルを記述するプレーンなメモリ ストリーム、またはより高度なコンポーネントの階層 (各コンポーネントで HTML のチャンクを抽出) です。
従来の ASP.NET では、Web ページの応答は再帰的にアクセスされるコントロール ツリーを構成することで取得されます。ツリーの各メンバは、それ自体の HTML マークアップを書き込むストリームを受信します。AJAX モデルにおける要求の応答は、サーバーで生成された HTML だけでなく、JavaScript Object Notation (JSON)、XML、シンジケーション、その他任意のものとしてシリアル化された生データである可能性があります。
BST パターンとは、要求によって生データがクライアントに返される状態を表します。一方、HTM パターンとは、要求によってすぐに表示できるマークアップが返される状態を表します。

拡張された BST の実装
先月号のソース コードでは、3 つの HTML テンプレートを入力として受け取り、それをデータ コレクションに対して反復処理し、その結果生成される HTML テキストを返す JavaScript クラスを作成しました。図 2 の JavaScript コードは、そのコードの最も重要な部分です。
図 2 テンプレートを取得する
function pageLoad()
{
    if (builder === null)
    {
        builder = new Samples.MarkupBuilder();
        builder.loadHeader($get("header"));
        builder.loadFooter($get("footer"));
        builder.loadItemTemplate($get("item"));
    }
}
function getLiveQuotes()
{
    Samples.WebServices.LiveQuoteService.Update(onDataAvailable);
}
function onDataAvailable(results)
{
    var temp = builder.bind(results);
    $get("grid").innerHTML = temp;
}
HTML テンプレートは、HTML 文字列として定義することも、ページに挿入された XML データ アイランドから読み込むこともできます。
<xml id="item">
    <tr>
        <td align="left">#Symbol</td>
        <td align="right">#Quote</td>
        <td align="right">#Change</td>
     </tr>
</xml>
HTML テンプレートの任意の構文では、データバインド項目のプレースフォルダを見つけることができます。上記のサンプル コードの #Quote は、バインドされたデータ項目オブジェクトの Quote プロパティの値を表します。
このコードの質と実用性を高める方法はいくつかあります。改善すべき点として忘れてはならないのが、項目のスタイルを個別に設定する機能です。たとえば、クライアントにダウンロードされるデータが、大量の株式の現在の株価情報および価格変更であるとします。値上がりしている株式を緑色で、値下がりしている株式を赤色でレンダリングするとしましょう。そのためには、いくつかのロジックをレンダリング プロセスに挿入する必要があります。図 3 に、株式データのコレクションをいくつかの既存の HTML テンプレートにバインドするために使用する Samples.MarkupBuilder クラスの一部を示します (完全なコードは今月号のダウンロードに含まれています)。
MarkupBuilder クラスの心臓部は _generate メソッドです。このメソッドは、テンプレートをデータとマージする役割を担います。図 3 では、_generate メソッドはデータとコールバックの 2 つの引数を受け取ります。先月紹介したコードでは、このメソッドが受け取る引数はデータ引数だけでした。
図 3 MarkupBuilder クラス
function Samples$MarkupBuilder$_generate(data, itemCallback)
{
    var pattern = /#\w+/g; // Finds all #word occurrences

    var _builder = new Sys.StringBuilder(this._header);

    for(i=0; i<data.length; i++)
    {
        var dataItem = data[i];
        var template = this._itemTemplate;

        var matches = template.match(pattern);
        for (j=0; j<matches.length; j++)
        {
            var text = matches[j];
            var memberName = text.slice(1);

            //Invoke a callback to further modify data to be bound
            var memberData = dataItem[memberName];
            var temp = memberData;
            if (itemCallback !== undefined)
            {
                temp = itemCallback(memberName, dataItem);
            }
            template = template.replace(matches[j], temp);
          }

          _builder.append(template);
    }

    _builder.append(this._footer);

    // Return the markup
    var markup = _builder.toString();
    return markup;
}
クライアント開発の場合は、特定の項目に対してテンプレートを処理している間にビルダ クラスがコールバックする JavaScript 関数も指定します。コールバック関数は、次のプロトタイプを備えている必要があります。
function applyFormatting(memberName, dataItem)
1 つ目の引数は、最初にシャープ記号 (#) が付いていない、プレースフォルダの名前です。ほとんどの場合、1 つ目の引数はバインドされたオブジェクトのパブリック プロパティの名前と一致します。2 つ目の引数は、テンプレートの現在のインスタンスにバインドされているデータ項目オブジェクト全体です。
データ オブジェクト全体を使用できる場合、実行時の条件を十分にチェックしてマークアップのバリエーションを特定できます。この方法で設計した場合、コールバック関数は、ASP.NET サーバー コントロールのサーバー側 DataBound イベントと論理的に同等になります。次のコード スニペットは、株価の上昇または下落に基づいて株価の色を変更するのに必要な JavaScript コードです。
function applyFormatting(memberName, dataItem)
{
    var temp = dataItem[memberName];
    if (memberName == "Change" && x.charAt(0) == "+")
    {
        return "<span style='color:green;'>" + temp + "</span>";
     }
     if (memberName == "Change" && x.charAt(0) == "-")
     {
         return "<span style='color:red;'>" + temp + "</span>";
     }
     return temp;
}
コード内のロジックは、#Change プレースフォルダに対してコールバックが呼び出された場合にのみ適用されます。別のメンバに対してコールバックが呼び出された場合は、単に生のメンバ値を返します。ページへの最終的な効果については、図 4 を参照してください。
図 4 ブラウザのカスタマイズされたデータ バインド (クリックすると拡大画像が表示されます)
図 4 の一部のセルに対して、どのような方法で異なる背景色を割り当てているのかと疑問に思われるかもしれません。上記ではコールバックをデータバインド関数として説明しましたが、実際のコールバックの呼び出しは、#xxx という表現を含む項目テンプレートで見つかった一致項目に対して行われます。これは、#xxx プレースフォルダをテンプレート タグに挿入することで、コールバックの呼び出しを意図的に設定し、HTML コードを事実上どこにでも挿入できるということを意味します。その例を次に示します。
<xml id="item">
   <tr>
      <td align="left">#Symbol</td>
      <td #Style1 align="right">#Quote</td>
     <td align="right">#Change</td>
   </tr>
</xml>
#Style1 という表現はプレースフォルダとして解釈されるため、コールバックが処理されます。コールバックの呼び出しには、疑似メンバの名前 (ここでは Style1) と現在のデータ項目が付随します。提供された情報に基づき、コールバックでホスト タグの背景色を淡い黄色に変更できます (そのセルが価格の上昇している株式をレンダリングするために使用されている場合)。
function applyFormatting(memberName, dataItem)
{
    if (memberName == "Style1")
    {
        if (dataItem["Change"].charAt(0) == "+")
            return "style='background-color:lightyellow;'";
        else
            return "";
    }
 ...
}
ASP.NET では、ほとんどのテンプレートベースのコントロールはデータバインド規則をヘッダーおよびフッター領域には適用しません。Samples.MarkupBuilder コンポーネントも例外ではありません。とはいえ、表示されているデータから派生した情報をフッターに表示する必要がある場合もあります。フッター (またはヘッダー) テンプレートにスクリプト可能な要素を定義することで、これを簡単に実現できます。
<xml id="footer">
      <tr>
         <td colspan="3" align="right"
            style="background-   color:#eeeeee;">
            <small><i>provided by <b id="lblProvider"></b></i></small>
         </td>
      </tr>
   </table>
</xml>
TD タグのコンテンツには、一意の ID を持つ <b> タグが含まれています。その要素をさらにスクリプト可能にするには、これで十分です。XML データ アイランドのコンテンツはリモート WCF サービスから収集された生データとマージされ、準備が整いしだい、HTML ブラウザ要素の innerHTML プロパティを通じてページ オブジェクト モデルに挿入されることに注意してください。
これが行われると、ブラウザは HTML のコンテンツを自動的に解析し、ドキュメント オブジェクト モデル (DOM) を更新します。このようにして、一意の ID 文字列を含むリテラル要素はスクリプト可能になります。次の関数のサンプルは、株価情報を提供するリモート WCF サービスの呼び出しに関連付けられているコールバックです。
function onDataAvailable(results)
{
    // Bind data and update the UI
    var temp = builder.bind(results, applyFormatting);
    $get("grid").innerHTML = temp;

    $get("lblProvider").innerHTML = results[0].ProviderName;
}
ページ要素の innerHTML プロパティが更新され、特定の ID (ここでは lblProvider) を持つ要素が追加された後で、その要素のスクリプティングを開始できます。
BST パターンでは、ブラウザに必要な HTML は JavaScript を使用して生成する必要があります。図 1 に示すように、すべてのプレゼンテーション ロジックを 1 つの層に分離できるため、これは一般的に好ましい手法です。
さらに、テンプレートと JavaScript コールバックを使用することで、本質的に動的な HTML に対処することができ、データの特性に対応してユーザーの期待に応えることができるようになります。テンプレートは、コードの柔軟性と管理のしやすさを両立させるうえで非常に役に立ちます。ここで紹介した MarkupBuilder のような汎用クラスは、必要なものを揃え、範囲を狭める役割を果たします。JavaScript だけに頼って HTML を生成したり、分散したプレゼンテーション ロジックに生成を結び付けたりすることは適切ではありません。コードはすぐに複雑化し、読みにくくなります。エラーが多数発生する状態になるのも避けられません。このような状況から救い出してくれるのが、Microsoft® クライアント AJAX ライブラリを使用して作成されたヘルパー クラスです。

InnerHTML と DOM
HTML Message パターン (AJAX アプリケーションで HTML マークアップを生成するためのもう 1 つの方法) を呼び出す前に、innerHTML について説明しましょう。DOM は標準の API です。これを通じて、ブラウザは現在表示されているページのコンテンツをプログラムで公開します。DOM では、ページを要素のツリーとして表します。論理ツリーの各ノードは、既知の動作と固有の ID を持つ有効なオブジェクトに対応します。
DOM ノードでは、3 つの基本的な操作を実行できます。それは、ノードの検出、ノードの作成、およびノードの操作です。特定のノードの識別は、対応する要素の ID を把握している限りは簡単です。API には便利な関数が用意されています。
var node = document.getElementById(id);
ASP.NET AJAX では、getElementById 関数は $get 関数にラップされます。同じ ID を持つ複数の要素が存在する場合、関数はコレクション内で最初に現れる要素を返します。DOM サブツリーを更新するには、不要な要素を削除し、新しい要素を追加する必要があります。これは設計の観点からすると簡潔でわかりやすいアプローチですが、パフォーマンス的には問題があります。
ほぼすべてのブラウザにおいて、その DOM 要素の innerHTML プロパティがサポートされています。HTML は、特定の要素の開始タグと終了タグの間で設定または取得されます。このプロパティは Internet Explorer® 4.0 の DHTML オブジェクト モデルで導入されましたが、正式な DOM API になることはありませんでした。しかし、innerHTML の処理は DOM API と比べてはるかに迅速です。特に、要素の複雑な構造体を作成する場合に顕著になります。innerHTML と DOM のパフォーマンスについては、go.microsoft.com/fwlink/?LinkId=116828 で詳しく説明されています。innerHTML にも問題はあります。把握しておく必要のある問題については、go.microsoft.com/fwlink/?LinkId=116827 を参照してください。

HTML Message パターン
HTM パターンの目的は、ブラウザで表示される HTML マークアップのブロックをサーバーで生成させることです。たとえば、リモート URL (サービスまたは HTTP ハンドラ) の呼び出しの実行と、すぐに表示できる HTML スニペットの受信から成る実装が可能です。
HTM の実装は、サーバー (具体的には AJAX サービス層) 上のコードに完全に依存します。これも、AJAX 固有の中間層の作成をお勧めする大きな理由の 1 つです。この中間層により、AJAX やプレゼンテーションのニーズおよび懸念事項からコア サービスを切り離すことができます (図 1 を参照)。
AJAX アプリケーションで HTML Message パターンをサポートするには、割り当てられたタスクを実行し、その計算結果を HTML スニペットに変換できるサービスが必要になります。図 5 にこのサービスを示します。
図 5 HTML Message をサポートするサービス (クリックすると拡大画像が表示されます)
ここからは、サービスのコンポーザビリティの原則がよくわかります。この原則は、再利用性の原則のバリエーションにすぎません。一般に、この原則はサービス指向アーキテクチャ (SOA) に適用されます。SOA では、Web Services Business Process Execution Language (WS-BPEL) のような構成言語を使用してビジネス プロセスを編成し、いくつかのプロセスを組み合わせることで親サービス プロセスを構成します。
HTML を出力する AJAX サービスは、データを取得するコア サービスと、データを HTML に変換するレンダラ サービスの組み合わせとして考えることができます。図 6 に、株価情報を返すサービスの構成アーキテクチャを示します。
図 6 HTML Message をサポートする株価情報サービス (クリックすると拡大画像が表示されます)
この実装では、親およびラッパー サービスを取得するためのクラスを構成しているにすぎません。設計の観点からすると、図 6 の複数のクラスはサービス (株価情報プロバイダ、データ ファインダ、出力レンダラ、およびこのコラムの後半で説明する入力アダプタ) として扱われます。
HTM パターンを実装する
図 7 に、HTM パターンの実装サンプルで使用している株価情報サービスのコントラクトを示します。このサービスは、オフラインおよびオンライン データ プロバイダを中心に構築されています。オフライン データ プロバイダは、株価と価格変更の古い値を返します。一方、オンライン プロバイダは、実際の財務サービスに接続してライブ データを返します。
図 7 サンプル StockQuote サービスのコントラクト
namespace Samples.Services.FinanceInfo
{
    [ServiceContract(Namespace="Samples.Services",
         Name="FinanceInfoService")]
    public interface IFinanceInfoService
    {
        [OperationContract]
        StockInfo[] GetQuotes(string symbols, bool isOffline);

        [OperationContract(Name="GetQuotesOffline")]
        StockInfo[] GetQuotes(string symbols);

        [OperationContract(Name="GetQuotesFromConfig")]
        StockInfo[] GetQuotes(bool isOffline);

        [OperationContract(Name = "GetQuotesFromConfigOffline")]
        StockInfo[] GetQuotes();

        [OperationContract(Name = "GetQuotesOfflineAsHtml")]
        string GetQuotesAsHtml(string symbols, bool isOffline);

        [OperationContract(Name = "GetQuotesFromConfigAsHtml")]
        string GetQuotesAsHtml(bool isOffline);

        [OperationContract(Name = "GetQuotesFromConfigAsHtmlEx")]
        string GetQuotesAsHtml(string contextKey);
    }
}
どちらのプロバイダも、実際にデータを取得するために内部ファインダ コンポーネントを使用します。ファインダ コンポーネントには、インターフェイスと実際のファインダ クラスが構成ファイルから読み取られるという特徴があります。オフライン プロバイダの既定のファインダ コンポーネントは、Microsoft .NET Framework Random クラスを使用して乱数だけを生成します。オンライン プロバイダのファインダ コンポーネントは、財務情報を返す任意のパブリック Web サービスを使用できます。
ファインダ クラスによって取得されたデータは、レンダラ クラスを使用して HTML スニペットとして構成されます。レンダラ コンポーネントはインターフェイスを公開します。このコンポーネントは、構成ファイル内の設定を変更するだけで置き換えることができます。既定の HTML レンダラは、ハードコーディングされたスタイルでテーブルを構築します。今月のソース コードをご覧になると、実際の HTML レンダラは、最新の更新ラベルも追加する既定のレンダラから派生したクラスであることがおわかりになるでしょう。以下のコード スニペットは、ファインダ クラスおよびレンダラ クラスのインターフェイスを示しています。
namespace Samples.Services.FinanceInfo
  {
      public interface IFinanceInfoFinder
      {
          string ProviderName { get; }
          StockInfo[] FindQuoteInfo            (string symbols);
      }
      public interface IFinanceInfoRenderer
      {
          string GenerateHtml            (StockInfo[] stocks);
      }
  }
ファインダで取得されたデータがレンダラのメソッドに直接渡されるので、インターフェイスにより円滑な相互運用性が実現されます。
この実装では、レンダラの GenerateHtml メソッドにより、事前に定義されているいくつかの設定に基づいてテーブルが構築されます。通常は、クライアントから渡されるその他のスタイル情報も使用できます。ただし、この株価情報サービスは、サーバーで正式な HTML ジェネレータとして構成されているレンダラ "サービス" を使用するように設計されています。この "サービス" に求められるのは、前の IFinanceInfoRenderer インターフェイスを実装することだけです。
次に示すサンプルでは、株価情報サービスを介して生成されたマークアップを使用しています。
function getLiveQuotes()
{
    var isOffline = $get("chkOffline").checked;
    Samples.Services.FinanceInfoService.GetQuotesFromConfigAsHtml
      (isOffline,
      onDataAvailable);
}
function onDataAvailable(results)
{
    // Update the UI
    $get("grid").innerHTML = results;
}
getLiveQuotes 関数は、ボタン クリックなどのクライアント側のイベントやタイマ コールバックにアタッチされます。図 8 は、サンプル ページを実行したところです。HTML マークアップは、JSON パッケージを使用してクライアントに戻されます。
図 8 HTML Message パターンを実行する (クリックすると拡大画像が表示されます)

パフォーマンスと設計に関する考慮事項
HTML Message パターンの場合、UI の生成はサーバー (特に、クライアントから呼び出すサーバー) で行われるようになります。このモデルには長所と短所があります。一方では、マネージ コードを使用してマークアップの生成に必要な複雑なロジックを実装できるという長所があります。サーバーでは、ブラウザでは確保できないプログラミング能力を活用して、構成ファイルを読み取り、リモート サービスに接続し、HTML テンプレートのデータベースにアクセスできます。
その一方で、デザイナなどのビジュアル ツールの力を借りずに、マークアップを生成するコードを記述しなければなりません。マークアップに必要な変更には、C# コードを使用して対処する必要があります。また、レイアウト、データ、分離コードを明確に分けることができません。
その回避策は、サーバー間シナリオでサービスがマークアップを照会する内部 ASP.NET ページの定義にあります。テンプレートとして機能するこれらのページは、Visual Studio® 2008 で作成できます。また、AJAX サービス層をホストする同じ IIS アプリケーションに展開できます。これらのページはプログラムで呼び出され、ページから返されるマークアップはクライアントに転送されます。
HTML Message パターンでは、生データを返すサービスのプレーンな呼び出しよりも多くのトラフィックが生成される傾向にあります。ただし、HTML Message パターンのトラフィックは、結果的には部分的なレンダリングによるトラフィックよりも少なくなることに注意してください。追加するスタイルおよび HTML 拡張が多いほど、返すパケットのサイズも大きくなります。
この点を踏まえて、HTML スタイル設定を HTML レイアウトとは切り離し、スタイル設定のためにクライアント側 CSS クラスへの参照だけをマークアップに埋め込むことができます。HTML マークアップをレイアウトとデータだけに限定すると、転送される余分なデータ (生データ以外のデータ) の割合を大幅に低減できます。実験中に私が気が付いたのは、表示する必要のあるフィールドが少ないときに、スタイル設定のために CSS クライアント クラスの参照だけを埋め込んだ場合、HTML Message パターンで構築されるページのトラフィックは、BST パターンで構築されるページのトラフィックよりもさらに少なくなることがあるということです。

DynamicPopulate エクステンダ
締めくくりとして、AJAX Control Toolkit に含まれているエクステンダの 1 つ、DynamicPopulate エクステンダについて説明します。このエクステンダは、HTML Message サービスで活用できます。
クライアント トリガ コントロール (ここではボタン) にバインドされたエクステンダは、サービス メソッドを呼び出し、結果を DOM 要素の innerHTML プロパティにアタッチします。言うまでもありませんが、DynamicPopulate エクステンダには AJAX サービス層の HTML Message サービスが必要です。
<act:DynamicPopulateExtender runat="server"
    ID="DynamicPopulateExtender1"
    BehaviorID="DynamicPopulateExtender1"
    ClearContentsDuringUpdate="false"
    TargetControlID="grid"
    UpdatingCssClass="updating"
    ServicePath="LiveQuotes.svc"
    ServiceMethod="GetQuotesFromConfigAsHtmlEx"
/>
DynamicPopulate エクステンダによっても、図 6 の入力アダプタ サービスで強調表示されているサービスに新たな要件が課されます。ServiceMethod 属性経由で参照されるメソッドは、次のプロトタイプを備えている必要があります。
string MethodName(string contextKey);
contextKey パラメータには、サービス メソッドが処理方法を認識しているフォーマットでシリアル化された、あらゆるデータを含めることができます。入力アダプタ サービス クラスでは、入力文字列を受け取り、その文字列をサービスの他のクラスが処理方法を認識している、より具体的なパラメータに変換します。
エクステンダの使用時に直面する可能性のある問題の 1 つは、ユーザーがボタンをクリックした際の既定のイベントがエクステンダによって阻止されないということです。そのため、ボタンが ASP.NET ボタンの場合、引き続きポストバックが発生し、サービス呼び出しが無効化されます。次に、DynamicPopulate エクステンダのより一般的な使用方法を示します。
<asp:Button runat="server" id="btnRefresh" text="Live Quotes"
    onclientclick="invoke();return false;" />
アタッチされた JavaScript の単純な関数は、次の処理を行います。
function invoke()
{
    var extender = $find("DynamicPopulateExtender1");
    var isOffline = $get("chkOffline").checked;
    extender.populate(isOffline.toString());
}
このコードに基づき、HTML 応答をエクステンダの TargetControlID プロパティを通じて指定した要素と結合することで、UI が更新されます。

現時点でのソリューション
HTML はプレゼンテーションに使用されます。一方、XML、JSON、および RSS は、サーバーで生成されたデータをクライアントに移動するフォーマットです。データはクライアントでプレゼンテーション用に処理できます。AJAX のコンテキストにおいてこの整然としたモデルの妨げとなるのが、JavaScript 言語の使用です。データをクライアントにダウンロードした後で UI を構築するには、JavaScript を使用するしかありません。カスタム データ バインドおよびテンプレートの手法としての BST パターンは、必要な UI を作成するうえで役立ちます。
あなたがサーバー重視の考え方を持ち、JavaScript の使用を避けたいと思っている場合はどうすればよいでしょうか。UI が特に複雑で、より信頼性の高い強力な開発ツールとデバッグ ツールを使用することを望んでいる場合はどうでしょう。かなり大きなデータ構造に対する高負荷なアルゴリズムをサーバーとクライアントの両方で使用することになった場合は、どうすればよいでしょう。HTML 応答では、クライアントでの懸念を払拭できないのでしょうか。
一般的に言って、リッチなクライアント側オブジェクト モデルを備えた強力なコントロール セットが登場するまでは、HTML をプレゼンテーションに使用して JSON をデータに使用する、好ましいモデルを常に採用できるとは限りません。AJAX プレゼンテーションに関して言えば、現時点では、このモデルをあらゆる状況で利用するわけにはいきません。そのため、HTML Message パターンに目を向ける必要があるのです。このパターンはあらゆる目的に役立つわけではありませんが、特定の領域で力を発揮します。

ご意見やご感想は、cutting@microsoft.com まで英語でお送りください。

Dino Esposito は、『Programming ASP.NET 3.5 Core References』の著者です。Dino はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。ブログは weblogs.asp.net/despos で読むことができます。


テストの実行
WCF テストの概要
Dr. James McCaffrey

コードのダウンロード : TestRun2008_07.exe (173 KB)
オンラインでのコードの参照

目次
テスト対象システム
テスト ハーネス
その他の考慮事項
Windows® Communication Foundation (WCF) をより簡単に使用するためのテストの技法および原則をいくつかご紹介します。WCF を初めて使用する方は言うまでもなく、多少の使用経験がある方もぜひ参考にしてください。WCF とは何であるかを考えるときに何とおりかのアプローチがありますが、Web サービスの大幅な拡張として捉える方法をお勧めします。Web サービス同様、WCF サービスではサービス指向アーキテクチャを使用して分散システムを構築できます。Web サービスと比較すると、WCF サービスの方が柔軟性に優れ (トランスポート プロトコルの選択など)、より多くの機能 (トランザクションやセキュリティなど) を備えています。したがって、"Web サービスの拡張機能である" という表現は WCF の定義としては不十分ですが、WCF の初心者にとってはわかりやすいアプローチだと思います。
図 1 に、代表的な WCF シナリオの単純な例を示します。ここで、Internet Explorer® はクライアント プログラムとして機能しており、ユーザーからテキストを受け取ってその暗号化ハッシュを計算する、ASP.NET Web アプリケーションにアクセスしています。背後では、ASP.NET Web アプリケーションが WCF サービスを呼び出して、実際にハッシュ計算を実行しています。このシナリオにおいて、WCF サービスは IIS によってホストされ、ASP.NET Web アプリケーションで使用されていますが、他のいくつかの方法でもホストすることができ、ほとんどすべての種類のアプリケーションおよび他のサービスでも使用可能です。
図 1 代表的な WCF アプリケーション シナリオ (クリックすると拡大画像が表示されます)
最も基本的なタイプの WCF サービス テストには、サービス操作の機能的な正確性の検証が含まれます。1 つのアプローチとして、アプリケーション UI を通して WCF サービスを手動でテストする方法があります。手動テストが必須とは言え、WCF サービスの基本的な機能を手動で調べるテストは、時間がかかるうえ、エラーを起こしやすく、非効率的であるだけでなく、とても退屈な作業です。
もっと適切なテスト方法は、図 2 で実行されているプログラムのようなテストを自動化するものを記述することです。このスクリーンショットは、私が記述したコンソール アプリケーション テスト ハーネスです。このテスト ハーネスでは、入力テキストをバックエンド WCF サービスに直接フィードし、サービスからの応答メッセージをフェッチし、テスト ケース結果の合格/不合格を判断します。図 3 のダイアグラムは、図 1 と図 2 のプログラム間の関係を単純化したビューおよび概要です。多くの場合、WCF サービスはバックエンド データベースから情報を取得するか、または Web サービスや WCF サービスから情報を取得しますが、図 3 にはそういったシナリオを含めませんでした。
図 2 WCF サービスをテストする (クリックすると拡大画像が表示されます)
図 3 単純化された関係 (クリックすると拡大画像が表示されます)
ここからは、テスト対象が何であるかを正しく理解できるようにバックエンド WCF サービスについて説明し、WCF サービスを使用する 図 1 の ASP.NET Web アプリケーションについて簡潔にお話した後で、テスト ハーネスについて詳しく解説します。また、他のいくつかの WCF テスト シナリオも最後に紹介します。

テスト対象システム
テスト対象システムは、バックエンド WCF サービスと、WCF サービスを使用する ASP.NET Web アプリケーションで構成されています。WCF サービスは柔軟性に富んでいます。WCF サービスを作成する時に必要とされる、設計に関する主要な決定事項の 1 つは、サービスのホスト メカニズムの選択です。通常は、次の 4 つのいずれかを選択します。IIS の使用、Windows® サービス、自己ホスト、および Windows プロセス アクティブ化サービス (WAS) の使用です。IIS や Windows の使用は、おそらく皆さんが慣れ親しんだ方法でしょう。自己ホストを選択した場合は、コンソール アプリケーションなどの Microsoft® .NET Framework マネージ プログラム内で WCF をホストします。WAS というのは、Windows Server® 2008 および Windows Vista® で利用可能な新しいプロセス アクティブ化メカニズムです。それぞれの WCF ホスト オプションには、個別の開発シナリオに応じて長所と短所があります。このコラムのサンプル WCF サービスには IIS を使用することにしました。IIS の使用を選択すると、組み込みの統合管理および監視機能、プロセス リサイクル、アイドル シャットダウン、メッセージベースのアクティブ化など、IIS の利点を活用できます。
IIS によりホストされる WCF サービスを作成することは非常に簡単です。最初に、Windows Server 2003 で Visual Studio® 2008 を起動します。なお、Windows Server 2008 または Windows Vista が動作しているコンピュータで WCF サービスを開発する場合は、拡張されたセキュリティ機能に関連する問題に対処する必要があります。今回はその問題を論じるスペースがありませんが、注意が必要です。
バーチャル ラボ : LINQ でテストする
LINQ では、SQL ストアド プロシージャをテストする場合に、テストの自動化を大幅に単純化することができます。バーチャル ラボで LINQ テスト手法を実験できます。コラムで説明したプロジェクトや構成済みの SQL Server® データベースを含め、すべてがインストールされ準備できています。ラボを起動し、記事を読み進めながらコードを実行してください。

バーチャル ラボでの実験 :
go.microsoft.com/fwlink/?LinkId=120461
次に、Visual Studio メニューで [ファイル]、[新規作成]、[Web サイト] の順にクリックします。[新しい Web サイト] ダイアログで WCF Service テンプレート (Visual Studio 2008 で既定でインストールされる) を選択し、ターゲットとして .NET Framework 3.5 を指定します。[場所] フィールドで、HTTP を選択し、localhost/WCF/CryptoHashService を指定します。この手順により、完全な Web アプリケーションおよび IIS 仮想ディレクトリが、開発用コンピュータの C:\Inetpub\wwwroot\WCF\CryptoHashService に作成されます。別のアプローチとして、コンピュータのファイル システム内の任意の場所を選択し、組み込みの Visual Studio Web 開発サーバーを使用することもできます。
私は実装言語として C# を選択しましたが、WCF サービスは、Visual Basic® .NET を使用して実装することもできます。ダイアログ ボックスの [OK] ボタンをクリックすると、Visual Studio により、完全に機能する WCF サービスと GetData および GetDataUsingDataContract という 2 つのサンプル演算子が作成されます。[ソリューション エクスプローラ] ウィンドウを見ると、4 つの主要なファイル (IService.cs、Service.cs、Service.svc、および web.config) が生成されていることがわかります。IService.cs ファイルには、WCF 操作に対するインターフェイス定義が保持され、Service.cs ファイルには、実際の操作の実装が含まれています。このサンプルでは、2 つのファイルの名前をそれぞれ ICryptoHashService.cs と CryptoHashService.cs に変更することにしました。次に、ICryptoHashService.cs を Visual Studio コード エディタに読み込み、サンプル インターフェイス コードを削除し、次のコードに置き換えます。
[ServiceContract]
public interface ICryptoHashService
{
    [OperationContract]
    string GetCryptoHash(string s);
}
ここでは GetCryptoHash という 1 つの演算子のみを保持していますが、他の演算子を追加することもできます。[SeviceContract] 属性および [OperationContract] 属性により、実際のコード生成の大半が背後で自動的に実行される点に注意してください。次に、実装ファイルの CryptoHashService.cs を編集します。using ステートメント参照を System.Security.Cryptography 名前空間に追加し、次のコードを記述します。
public class CryptoHashService : ICryptoHashService
{
    public string GetCryptoHash(string s)
    {
        byte[] ba = Encoding.Unicode.GetBytes(s);
        MD5CryptoServiceProvider sp = new MD5CryptoServiceProvider();
        byte[] hash = sp.ComputeHash(ba);
        string result = BitConverter.ToString(hash);
        return result;
    }
}
最初に、CryptoHashService クラスの名前と、このクラスの派生元であるクラスの名前を変更し、自分がインターフェイス定義で使用する名前と一致するようにします。私は GetCryptoHash メソッドにおいて、単純に GetBytes メソッドを使用して入力引数をバイト配列に変換し、MD5CryptoServiceProvider クラスのインスタンスをインスタンス化し、ComputeHash メソッドを使用してバイト配列を 16 バイトの MD5 暗号化ハッシュに変換しています。静的 BitConverter.ToString メソッドを使用して、結果のハッシュのバイト配列を読みやすい文字列に変換し、その文字列を返します。この例を簡潔にするために、運用環境で使用される通常のエラー チェック (入力引数のチェックや例外のキャッチなど) は省略しました。次に、Service.svc ファイルを編集し、"Service" から "CryptoHashService" への名前の変更を反映させます。
<%@ ServiceHost Language="C#" Debug="true"   Service="CryptoHashService"
           CodeBehind=           "~/App_Code/CryptoHashService.cs" %>
最後に web.config で 2 つの名前の参照を更新し、名前の編集が完了しました。
<system.serviceModel>
         <services>
         <service name="CryptoHashService"     behaviorConfiguration="Servic Behavior">
    <!-- Service Endpoints -->
    <endpoint address=""binding="wsHttpBinding"         contract="ICryptoHashService">
binding="wsHttpBinding" エントリに注目してください。WCF バインドは、WCF サービスがクライアントとどのようにやり取りするかを指定する情報 (サービスにより使用されるトランスポート プロトコル、使用されるテキスト エンコード方式など) のコレクションです。組み込みのバインドを使用するか、またはカスタム バインドを作成することができます。wsHttpBinding は、IIS によりホストされる WCF サービスを作成した場合に既定で使用される構成済みバインドです。Visual Studio が生成する web.config ファイルの別の部分は次のようになります。
<endpoint address="mex" binding="mexHttpBinding"
    contract="IMetadataExchange"/>
このエントリは、メタデータを公開するよう WCF サービスに指示するものです。これにより、クライアント プログラムは、サービスを調べてサービスとやり取りする方法を判断できます。この時点で、WCF サービスを正常にビルドできます。メイン メニューから [ビルド] メニューの [ソリューションのビルド] をクリックします。または、F5 ショートカット キーを押すことにより、WCF サービスをビルドすると共に、クライアント プログラムを作成するための命令を取得することもできます。サービスが IIS によりホストされるため、サービスを明示的に開始する必要はありません。IIS が動作している限り、WCF サービスでは、受信した WCF メッセージを受け入れる準備が整っています。
ここで、図 1 に示した ASP.NET Web アプリケーションの作成手順をまとめます。Visual Studio の新規インスタンスを起動し、[ファイル]、[新規作成]、[Web サイト] の順にクリックします。[新しい Web サイト] ダイアログで、ASP.NET Web Site テンプレートを選択し、フレームワーク ターゲットのドロップダウン リストから .NET Framework 3.5 を選択します。[場所] で HTTP を選択し、使用する言語として C# を選択します。[場所] フィールドに「localhost/WCF/UtilitiesAndTools」と入力し、Web アプリケーションに暗黙的に名前を付けます。開発用コンピュータのファイル システムの場所を指定して、IIS の代わりに開発 Web サーバーを使用することもできます。次に、最小限の UI コードを Web アプリケーションに追加します。
図 4 のコードは基本的な UI です。フォント スタイルや <hr/> タグなど書式設定の詳細は含まれていません。なお、完全な UI コードおよび他のすべてのコードは、このコラムに付属のコード サンプルから入手できます。
図 4 ASP.NET Web アプリケーションの UI コード
<body>
   <form id="form1" runat="server">
      <div>
      <asp:Label runat="server" ID="Label"
         Text="Demo Utilities Featuring WCF Services" />
      <asp:Label runat="server" ID="Label2"
         Text="Enter text here:" />
      <asp:TextBox runat="server" ID="TextBox1"
         Height="100px" Width="320px" />
      <asp:Button runat="server" ID="Button1"
         Text="Get MD5 Crypto-Hash" onclick="Button1_Click"
         Width="150px" />
      <asp:Button runat="server" ID="Button2"
         Text="Get SHA1 Crypto-Hash"
         Width="150px" />
      <asp:Label runat="server" ID="Label3"
      Text="Crypto-Hash of your text (computed by WCF Service) is:" />
      <asp:TextBox runat="server" ID="TextBox2"
         Width="320px" />
      </div>
   </form>
</body>
この時点で、ASP.NET Web アプリケーションは WCF サービスに関する情報を保持していませんが、WCF と ASP.NET のテクノロジはシームレスに連携して動作するように設計されています。Web アプリケーション プロジェクトの [ソリューション エクスプローラ] ウィンドウで、プロジェクト名を右クリックし、コンテキスト メニューの [サービス参照の追加] をクリックします。このコマンドは、より古いオプションの [参照の追加] (通常は DLL ライブラリおよび .NET 名前空間に使用される) と [Web 参照の追加] (通常は ASP.NET Web サービスに使用される) を補完する新しいオプションです。
表示された [サービス参照の追加] ダイアログで、「localhost/WCF/CryptoHashService/Service.svc」と入力し、[移動] ボタンをクリックします。[サービス参照の追加] ツールでは、利用可能なサービスを見つけるために指定の場所をスキャンし、検出したサービスを表示します (この例では、CryptoHashService WCF サービス)。ダイアログの [名前空間] フィールドで、あまり説明的ではないが単純な既定の名前の ServiceReference1 を選択し、[OK] ボタンをクリックします。アプリケーションが WCF サービスに接続するために必要なプロキシ コードがすべて Visual Studio によって生成されます。特に、CryptoHashService サービスと通信するために使用される、CryptoHashServiceClient (WCF サービスの名前に "Client" が追加されている) という名前のクラスを取得します。
デザイン ビューで、Button1 コントロールをダブルクリックすると、Visual Studio がコントロールのイベント ハンドラを登録します。続いて、次のコードを Button1_Click メソッドに追加します。
try
{
    string s = TextBox1.Text;
    ServiceReference1.CryptoHashServiceClient c =
       new ServiceReference1.CryptoHasahServiceClient();
    string h = c.GetCryptoHash(s);
    TextBox2.Text = h;
}
catch(Exception ex)
{
    TextBox2.Text = ex.Message;
}
簡単すぎるくらいですね。TextBox1 でテキストを取得し、自動生成された CryptoHashServiceClient クラスのインスタンスをインスタンス化し、オブジェクトの GetCryptoHash メソッドを呼び出して、結果の MD5 (メッセージ ダイジェスト バージョン 5) 暗号化ハッシュを TextBox2 に表示します。図 1 の Web アプリケーションのユーザー インターフェイスに、SHA-1 (セキュア ハッシュ アルゴリズム 1) 暗号化ハッシュを計算するボタン コントロールがありますが、私は、開発中のシステムをシミュレートするため、この機能は実装しませんでした。Web アプリケーションをビルドすると、図 1 に示されているように、ユーザーは Internet Explorer を起動し、アプリケーションに移動し、テキストを入力して、そのテキストの MD5 暗号化ハッシュ (バックエンド WCF サービスで計算される) を取得することができます。

テスト ハーネス
図 2 の単純なテスト ハーネスのコードをご覧ください。基本的に、このテスト ハーネス プログラムは、テスト対象の WCF サービスに入力メッセージを送信して適切なメッセージが返されるかどうかを確認する、WCF クライアントです。まず最初に、Visual Studio の新規インスタンスを起動し、TestHarness という名前の C# コンソール アプリケーション プログラムを作成します。図 2 の例では、ハーネスをテスト対象のシステムの近くに保持するという目的で、ハーネスの場所を C:\Inetpub\wwwroot のサブディレクトリに設定していますが、別のテスト ホスト コンピュータなど任意の場所にハーネスの配置先を設定できます。図 5 は、テスト ハーネスの全体的な構造を示しています。
図 5 テスト ハーネスの構造
using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;

namespace TestHarness
{
    class Program
    {
        static void Main(string[] args)
        {
        try
            {
                 // display startup messages
                 // set up test case data collection, testCases
                 // set up WCF binding object, wsb
                 // set up WCF address object, epa

                 CryptoHashServiceClient c =
                     new CryptoHashServiceClient(wsb, epa);

                foreach (TestCaseData tcd in testCases)
                {
                     // echo case ID, input, expected values
                     // call GetCryptoHash method, fetch return
                     // compare actual return with expected
                     // print pass/fail result
                }
                Console.WriteLine("\nDone");
            }
            catch (Exception ex)
            {
                Console.WriteLine("Fatal error: " + ex.Message);
            }
        }
    }

    class TestCaseData
    {
        public readonly string caseID;
        public readonly string input;
        public readonly string expected;
        public TestCaseData(string caseID, string input, string expected)
        {
            this.caseID = caseID; this.input = input; this.expected =
              expected;
        }
    }
}
System.ServiceModel 名前空間を指す using ステートメントを追加して、テスト ハーネスのコーディングを開始しました。この名前空間は、コア WCF サービス機能を保持しています。次に、テスト ケース データを設定します。図 5 では、単純な TestCaseData クラスを作成して、テスト ケースの ID、入力、および予期される値を保持しています。後で簡単に説明しますが、運用シナリオにおいては、WCF サービス バインド情報など他のフィールドの追加も必要でしょう。次に、汎用の List オブジェクトを作成して TestCaseData オブジェクトを設定することにより、テスト ケース データをハーネスに直接埋め込みます。
List<TestCaseData> testCases = new List<TestCaseData>();
testCases.Add(new TestCaseData("001", "Hello world",
    "E6-76-0D-55-5C-32-F6-6F-5E-15-93-31-DB-20-FD-8E"));
testCases.Add(new TestCaseData("002", "Goodbye world",
    "1A-05-3C-C0-4A-18-13-06-0E-AC-EA-BA-46-EC-CF-B1"));
testCases.Add(new TestCaseData("003", "",
    "D4-1D-8C-D9-8F-00-B2-04-E9-80-09-98-EC-F8-42-7E"));
XML ファイルや SQL テーブルなどの外部ストアにテスト ケース データを設定することもできます。ここでは、テスト ケースを 3 つ使用しますが、実際には数千ものテスト ケースがあるでしょう。テスト対象のシステムを包括的にテストする適切なテスト ケースの入力を特定し、予期される結果を見極めることは、ソフトウェア テストの最も難しい部分です。
WCF クライアント コードを最初から記述する方法もありますが、Visual Studio 2008 に付属している svcutil.exe コマンド ライン ツールを使用して、プロキシ コードおよび WCF 構成ファイルを生成する方がずっと簡単です。この例では、(svcutil.exe の場所を認識している) Visual Studio コマンド シェルを起動し、次のコマンドを入力します。
> svcutil.exe http://localhost/WCF/CryptoHashService/Service.svc
WCF サービスの場所を指定し、追加の引数なしで Svcutil.exe を実行します。Svcutil.exe は、WCF サービスの WCF メタデータを読み取り、2 つのファイルを自動的に生成します。最初のファイルは、CryptoHashService.cs (対象の WCF サービスの名前の後に .cs 拡張子が付加されている) です。このファイルは C# プロキシ コードを保持しているため、WCF サービスに対するメッセージ送受信に利用できます。2 番目のファイルは、output.config です。このファイルは、対象の WCF サービスで使用される WCF バインド情報 (トランスポート プロトコル、タイムアウト設定、テキスト エンコードなど) を保持しています。2 つのファイルの生成後に、テスト ハーネス プロジェクトを右クリックし、CryptoHashService.cs ファイルを自分のプロジェクトに追加します。または、直接コードをコピーして自分のテスト ハーネスに貼り付けます。
次の 2 つの方法で、output.config ファイルのバインド情報を使用できます。1 つは、ファイルの名前を app.config に変更してから、そのファイルをクライアント プロジェクトに追加する方法です。もう 1 つの方法は、output.config ファイル内のデータを調べた後で、output.config 内の値をプログラムによってバインド オブジェクトに割り当てるコードを記述します。通常の WCF クライアント プログラムを記述する場合、一般的には、プログラムを使用するアプローチよりも app.config を使用する方が適切です。app.config ファイルは標準的なテキストであり、実行時にクライアント プログラムによって読み取られるので、関連付けられた WCF サービスのバインド情報が変更されたときに対応するクライアントのバインド情報を変更する場合は、クライアントを再コンパイルせずに app.config ファイルを変更するだけです。ただし、WCF テスト ハーネス クライアントのケースにおいては、バインド情報をプログラムで指定する方が適している場合があります。プログラムを使用するアプローチでは、バインド情報をテスト ケースの入力の一部として簡単に渡すことができます。この例では、プログラムを使用するアプローチを使用しましょう。WSHttpBinding オブジェクトをインスタンス化します。
WSHttpBinding wsb = new WSHttpBinding();
これで、バインド オブジェクトの Name プロパティや、各種の時間プロパティに値を割り当てることができます。
wsb.Name = "WSHttpBinding_ICryptoHashService";
wsb.CloseTimeout = TimeSpan.Parse("00:01:00");
wsb.OpenTimeout = TimeSpan.Parse("00:01:00");
wsb.ReceiveTimeout = TimeSpan.Parse("00:10:00");
wsb.SendTimeout = TimeSpan.Parse("00:01:00");
これらの値を決定するには、svcutil.exe ツールで生成された output.config ファイルを調べてバインド エントリの属性を確認します。この例では、WCF サービスの作成時に Visual Studio で生成された既定値をすべて使用しています。残りのバインド プロパティにも同じように値が指定されます。
wsb.BypassProxyOnLocal = false;
wsb.TransactionFlow = false;
wsb.HostNameComparisonMode =
    System.ServiceModel.HostNameComparisonMode.StrongWildcard;
wsb.MaxBufferPoolSize = 524288;
wsb.MaxReceivedMessageSize = 65536;
wsb.MessageEncoding =
    System.ServiceModel.WSMessageEncoding.Text;
wsb.TextEncoding = System.Text.Encoding.UTF8;
wsb.UseDefaultWebProxy = true;
wsb.AllowCookies = false;
これで、プロキシ オブジェクトを設定できます。
string uri =
    "http://vte014.vte.local/WCF/CryptoHashService/Service.svc";
EndpointAddress epa = new EndpointAddress(uri);
CryptoHashServiceClient c =
    new CryptoHashServiceClient(wsb, epa);
CryptoHashServiceClient クラスは、svcutil.exe を使用して作成した自動生成の CryptoHashService.cs ファイル内に定義されます。このクラス コンストラクタは、(上記のプログラムで設定した) バインド オブジェクトと、WCF サービスを指す EndPointAddress オブジェクトを受け取ります。クライアント オブジェクトはインスタンス化されているため、各テスト ケースを繰り返して、テスト対象の WCF サービスを調べることができます。
図 6 では、単純に、各テスト ケースに関する合格/不合格のメッセージをコンソールに送信しています。運用環境では、さらに複雑な作業が必要とされるでしょう。合格のケースと不合格のケースの合計数を追跡する、テスト ケースの失敗が 1 つまたは複数である場合にプログラムで電子メール メッセージを送信する、結果を外部データ ストアに保存する、などの作業です。Team Foundation Server を使用している場合は、2008 年 Launch 号に掲載した私の「テストの実行」コラムの「Team System があるカスタム テストの自動化」を参考にして、このテスト ハーネスを管理できます (msdn.microsoft.com/magazine/cc164248 を参照)。
図 6 各テスト ケースの状態の表示
foreach (TestCaseData tcd in testCases)
{
    Console.WriteLine("Case ID  = " + tcd.caseID);
    Console.WriteLine("Input    = " + tcd.input);
    Console.WriteLine("Expected = " + tcd.expected);

    string actual = c.GetCryptoHash(tcd.input);
    Console.WriteLine("Actual   = " + actual);
    if (actual == tcd.expected)
        Console.WriteLine("* Pass *");
    else
        Console.WriteLine("** FAIL **");
}

その他の考慮事項
今月紹介したテクニックは、基本的な WCF サービス テストを始めるための堅固な基板となるものです。WCF テストには数多くの他の側面もありますが、それは別の機会に解説します。追加のテストの大半は、WCF の優れた柔軟性に応じて行われます。たとえば、WCF を使用したシステムでは、トランスポート レベルのセキュリティ (HTTPS の使用など) だけでなく、より低い他のレベルでもセキュリティを使用できます。WCF サービスは HTTP を使用できますが、WCF を利用するシステムでは、他のいくつかのメカニズム (TCP や名前付きパイプなど) による通信が可能です。先に示したとおり、WCF サービスは IIS でホストされますが、Windows サービスなどの自己ホスト型マネージ アプリケーションを使用するなど、他の方法でホストすることもできます。また、WCF サービスは複数のエンド ポイントをサポートし、それぞれに異なるアドレス、バインド、およびコントラクトを保持できます。さらに、WCF では、要求/応答スタイルのメッセージングや二重スタイルのメッセージングに対応します。他にも多くの WCF シナリオがありますが、すべてのシナリオが徹底的なテストに興味深い影響を与えます。
このコラムで紹介している基本的な WCF 機能テスト シナリオは、徹底的な WCF テストの一部を示しています。私のダミー WCF 暗号化ハッシュ サービスは非常に簡単であるため、ロジック全体が 1 つの GetCryptoHash メソッドに含まれています。より現実的なシナリオでは、ビジネス ロジックをカプセル化するコードとは別に、サービス機能をカプセル化するコードを使用するかもしれません。そういったアプローチでは、ビジネス ロジックとサービスとを別々にテストできるため、テスト作業が単純化されます。
Visual Studio Team System を使用して WCF サービスを作成するときに、テスト駆動型の開発方針を採用する場合は、組み込みの単体テスト サポートを利用できます。また、Visual Studio 2008 に付属する WcfTestClient.exe テスト クライアント ユーティリティを使用し、WCF サービスを手動でテストして、このコラムで紹介したタイプの自動テストを補完することもできます (MSDN® Magazine での私の同僚である Juval Lowy が執筆したコラム (msdn.microsoft.com/magazine/cc163289) を参照してください)。純粋な機能テスト以外にも、Visual Studio の統合ロード テスト ツールでローカル テストを実行することもできます。
ご意見やご質問は、James (testrun@microsoft.com) まで英語でお送りください。

Dr. James McCaffrey は、Volt Information Sciences, Inc. で、マイクロソフトのソフトウェア エンジニア向けの技術トレーニングを担当しています。これまでに、Internet Explorer、MSN サーチなどの複数のマイクロソフト製品にも携わってきました。また、『.NET Test Automation Recipes』の著者でもあります。連絡先は、jmccaffrey@volt.com または v-jammc@microsoft.com です。


セキュリティに関するブリーフィング
脅威のモデル化プロセスをよみがえらせる
Adam Shostack

目次
脅威のモデル化のアプローチ
簡便な脅威モデル
完了のタイミングを把握する
脅威のモデル化プロセスを推し進める
同期を維持する : 変更への対処
何をすべきかわからなくなったら
私の同僚の Ellen は、だれでも常に脅威のモデル化を行っているとよく言います。私たちは皆、空港のセキュリティに対する脅威をモデル化します。また、自宅のセキュリティに対する脅威もモデル化します。家族、宝飾品、取り替え不可能な思い出の写真 (デジタルな形で保存されていない写真を持っている私たちの世代に限られますが) などの自分の財産に及ぶかもしれない脅威について考えます。そして、構成や構造に基づいて脅威をモデル化します。「壁がここで、大きな窓がそこにあり、簡単に登れる木が立っている (鍵を忘れて家に入れないときに便利だが防犯的には危ないかもしれない)」という具合です。または、脅威をもたらす敵を想定して脅威をモデル化します。強盗を警戒し、子供がプールに落ちないだろうかと心配し、気象の影響について気をもみ、地震、雪、竜巻などの被害に不安を抱くこともあります。
セキュリティの専門家であれば、「十分に練られた多面的な評価プロセスを採用し、経験則を信頼して事例の再現性を抑えるように」というようなアドバイスを述べるかもしれません。ただ、実際には、あらゆる事態を想定し、可能性のある脅威すべてに防御策を講じるという人はほとんどいないと思います。また、自宅のセキュリティの管理計画の作成や自宅に対する侵入テストを行っている人もあまりいないでしょう。
ソフトウェアの開発を行う際には (開発手法がアジャイル型であるか、ウォーターフォール型であるかは問いません)、何を開発するか、何を開発しないか、および適切なものを開発していることを確認するためにどうするかについて、共通の認識が必要です。ここ数年の間に、脅威のモデル化は面倒で非効率的なプロセスであるという見方が生まれました。しかし、このようなプロセスの追加をお勧めするのには十分な理由があります。ここでは、その理由や、これらのプロセスから学んだ教訓、脅威のモデル化におもしろみを見つけだし、だれでも実行できる効率的で柔軟な作業にする方法について説明します。

脅威のモデル化のアプローチ
脅威のモデル化と呼ばれる作業は数多くあります。"正しい唯一の方法" はどれかを議論する代わりに、ご自身のニーズやスキル、能力、スケジュールを考慮したうえで、最適な方法を採用してください。そのアプローチの一環として、「あなたの脅威モデルはどのようなものですか」および「そのコンポーネントについて脅威のモデル化を行ったことはありますか」と尋ねる場合があります。前者は要件を明確にするための質問であり、後者は設計を分析するための質問です。マイクロソフトでは、ほとんどのケースで後者の手法をとります。
脅威のモデル化手法は多数あるため、1 つのコラムではカバーしきれません。目標も多種多様です。ご自身の脅威のモデル化プロセスには、迅速性と詳細さのどちらが求められますか。確実性や完全性と使いやすさを比べたとき、どちらが重要視されますか。専門家や開発者をすべての会議に参加させる必要はありますか。Microsoft® のセキュリティ開発ライフサイクル (SDL) や医療機器メーカー向けの規則など、組織や業界の規則に従う必要はありますか。高いレベルでの目標は、セキュリティ上の問題を早い段階で明らかにすることです。これにより、後から設計上の欠陥を修正する代わりに、設計段階で問題に対処できるようになります。
脅威のモデル化作業に取り組む際の方法には、主に次のようなものがあります。
資産 : 資産に基づく脅威のモデル化は、自分の家にあるどの家財を保護すべきかを考える作業によく似ています。まずソフトウェアの関連する資産を列挙してから、それらの資産を攻撃者がどのような方法で狙う可能性があるかを考えます。資産の例として、顧客のクレジット カード情報を格納したデータベースや、暗号化パスワードの含まれるファイルなどが挙げられます。
中には、資産を脅威のモデル化ダイアグラムの要素と解釈し、Web サーバー自体が資産だと考える人もいるかもしれませんが、それは正しくありません。デジタル資産とは、攻撃者による読み取り、改ざん、または破壊の対象となるものです。
攻撃者 : 攻撃者に基づく脅威のモデル化では、だれが自分の資産を狙う可能性があるかを考えます。このとき、攻撃者の能力からその攻撃方法までを理解していることが必要です。この手法は、たとえば、敵対者が外国の軍隊で、既知の戦略ドクトリンを持ち、特定の地域で活動し、長い期間をかけて攻撃システムを構築しているような場合は役立ちます。ただし、敵対者が緩やかに組織化された匿名のハッカー グループである場合は、あまり役に立ちません。
さらに一般的に言えば、この手法がソフトウェアの脅威をモデル化する際に役立つかどうかは疑問です。確かに一部の人々にとっては、"攻撃者のように考える" 作業が設計の分析における重要な要素です。とはいえ、その作業がだれもが習得できる再現可能なプロセスであるかどうかは、明らかではありません。攻撃者に基づいてモデル化を開始する場合は、標準のセットを使用するとよいでしょう。こうした一連の脅威を列挙しておくと役に立ちます。
ソフトウェア設計 : 設計に基づく脅威のモデル化は、"フェンス" と "窓" の位置に基づく手法です。ダイアグラムを作成して、そのダイアグラムに含まれる各要素に何が悪影響を及ぼすおそれがあるかを検討します (これは、今日の SDL の脅威モデル化プロセスにおいて最も重要な作業です。ソフトウェア開発に携わるあらゆる開発者がホワイトボードにダイアグラムを描く方法を把握しているからです)。
ソフトウェアにおいてフェンスと窓に相当するのは、さまざまな形の攻撃対象領域です。これには、ファイル パーサーやネットワーク リスニング サービス (ソケット、リモート プロシージャ コール (RPC) サービス、Web サービス記述言語 (WSDL) インターフェイス、AJAX API) などが含まれます。これらは信頼境界です。攻撃者の足掛かりになることを予期しておく必要があります。

簡便な脅威モデル
脅威のモデル化は、面倒な作業ではありません。ここでは、図 1 に示すプロセスのとおりに、簡単に実行できる基本的な脅威のモデル化プロセスについて説明します。
  1. アプリケーションのダイアグラムを作成し、それを使用してホワイトボードの前でアプリケーションについて説明します (図 2 を参照)。コードには円を、コードに含まれない要素 (人々、サーバー) には四角を、ストレージには円筒形を使用してください。私たちのチームでは、データ ストアには少し変わった形の平行線を使用しています。ドメインと区別するために、一部の信頼境界は波線を使用して描きます。
  2. ブレインストーミングを行って脅威を特定します。行き詰まった場合は、図 3 に示す STRIDE 脅威モデルをアプリケーションの各要素に適用します。修正は気にせずにブレインストーミングを進めてください。
  3. 脅威は集中しているのか、それとも完全に分散しているのかを考慮したうえで、再設計を検討します。どちらの場合も、脅威に一元的に対処するために、コンポーネントの設計や追加が必要になることがあります。すべての脅威が 1 か所に集中している場合は、正面のドアだけに注意していればよく、その他の場所に注意を払う必要はありません。
  4. 一次的な脅威に対する手段を講じます。細かく考えず、幅広く検討してください。脅威や対策について順序立てて考えるとよいでしょう。一次的な脅威とは、ドアが開かれることです。一次的な対策とは、鍵を閉めることです。二次的な脅威とは、何者かが鍵をこじ開けて破壊することです。二次的な対策としては、防犯性の高い鍵と頑丈なドアを適切に取り付けることなどが考えられます。三次的な防御策として、ドアにアラーム システムを設置するのもよいでしょう。ワイヤが切断された場合の被害を食い止めるために、ワイヤに定期的にメッセージを送信することもできます。アラーム システムにつながる電話線を何者かが切断したときに起きる事態を、ソフトウェアに置き換えて考えてください。ドアの鍵の心配をする前にそのような事態について憂慮しているとすれば、正しい順序で対策を講じているとは言えません。
  5. バグを整理して、脅威のモデル化で見つかった問題点を修正できるようにします。
図 1 脅威のモデル化プロセス (クリックすると拡大画像が表示されます)
図 2 アプリケーションのダイアグラムを作成すると脅威のモデル化に役立つ (クリックすると拡大画像が表示されます)
図 3 STRIDE 脅威モデル
脅威 | 影響を受けるもの | 定義 | 例
なりすまし | 認証 | 何か、または何者かになりすますこと。 | なりすましの対象は、Bill Gates、microsoft.com、ntdll.dll などです。
改ざん | 整合性 | データやコードを書き換えること。 | 書き換えの対象は、ディスクや DVD の DLL、LAN を通過中のパケットなどです。
否認 | 否認不可性 | ある行為について、行っていないと主張すること。 | 「そのような電子メールは送信していません」、「そのファイルは変更していません」、「そのような Web サイトには決してアクセスしていません」
情報漏えい | 機密性 | 見ることを許可されていない人物に情報を漏らすこと。 | Windows のソース コードを任意の人物が読めるようにする行為や、顧客リストを Web サイトに公開するような行為です。
サービス拒否 | 可用性 | ユーザーにサービスを提供できないようにしたり、その質を低下させたりすること。 | Windows や Web サイトをクラッシュさせる行為、パケットを送信して CPU 時間を大量に消費させる行為、パケットをブラック ホールにルーティングする行為などです。
特権の昇格 | 承認 | 適切な承認なしに特権を取得すること。 | 古典的な事例としては、リモート インターネット ユーザーがコマンドを実行できるようにする行為がありますが、制限付きユーザーが管理者権限を不正に取得するのも EoP と言えます。

完了のタイミングを把握する
作成したダイアグラムにシステムの該当部分がすべて含まれており、ダイアグラムのすべての要素について STRIDE モデルを説明し終えていれば、かなりうまくいったと言えるでしょう (脆弱性ごとにバグを整理できていれば、なおのこと結構です)。
これは、すべてのドアと窓に鍵が掛かっていることを確認し終えたときに「完了した」と言うのに似ています。それで十分でしょう。時間を割いて取り組むことができるのは、この程度の作業だと思われます。ただし、皆さんの中には、さらに踏み込んだ取り組みや、導入した防御策の回避方法の調査を予定している方もいらっしゃるかもしれません。
取り組みの程度は、組織や顧客のリスク許容度によって大きく変わってきます。また、予算や経験にも左右されます。脅威のモデル化プロセスを開始し、成果を出し、妥当な時間で終了する必要がある場合もあれば、安全性が最も重要視される制御システムを設計しているため、長時間かけて詳細に見ていく必要がある場合もあります。
そのため、完了のタイミングを見極める際にはトレードオフが必要になります。まず、取り組みの程度を左右する方針、法律、または規制について考慮してください。その次に、開発中のシステムのリスクを評価します。最後に、脅威のモデル化プロセスと、それに基づく対策やテストに使用できる時間やリソースを考慮する必要があります。

脅威のモデル化プロセスを推し進める
脅威のモデル化は、独りでに行われることはありません。だれかが推し進める必要があります。その人物は、脅威のモデル化の効果を大きく左右します。セキュリティを重視する人が組織に 1 人、または数人でもいる場合は、その先導により組織全体で脅威のモデル化を推し進めることができます。
他の人々をどのような方法で関与させるか、または関与させるべきかどうかという問題もあります。これは、組織の文化と人材に関わる問題です。セキュリティを重視する人々があらゆる要素に対する脅威をモデル化し、その結果を共有して、発見された問題を修正できるとすれば、最高です。ぜひ他の人々を巻き込んでください。
残念なことに、セキュリティを重視する人々が他のプロジェクトに参加しなければならず、手が空いていない場合は、そうでない人物でメンバを構成しなければならない場合もあります。マイクロソフトにより、セキュリティの専門家がいた方が脅威のモデル化の効果が高まることが明らかにされていますが、常に参加できるとは限りません。
セキュリティの専門家とは、セキュリティのことを常に考えていると同時に、それを体系的に行うことのできる人物のことを指します。また、それぞれの設計のコンテキストに基づいて否認の脅威とはどのようなものであるかを説明できる人物でもあります。専門家は何らかの資格を保有している場合もあるでしょう。コードやオペレーションに存在するセキュリティ上の欠陥を指摘する人物や、SANS や BlackHat などの会議に出席している (または出席を希望している) 人物、マイクロソフト セキュリティ情報で謝辞を送られたことのある人物かもしれません。
人々を組織化して作業のフィードバックを行うと同時に、その作業を細かく簡単な要素に分割する (ルールを設定し、各要素でセルフチェックできるようにする) と、すばらしい成果を上げることができます。ただし、脅威のモデル化作業をあまりにも細かい多数の要素に分割し、過剰な数のルール (不適切なルール) を設定すると、プロセスのおもしろみが完全に失われ、脅威のモデル化は面倒な作業と化します。そのため、専門家の数と、専門家が脅威のモデル化に割くことのできる時間を考慮したうえで、作業をどの程度分割するかを決定する必要があります。

同期を維持する : 変更への対処
計画には変更がつきものです。避けることはできません。計画が変更されると、メンバが考えている実際の数多くの計画にドキュメントを合わせるためのコストが発生します。メンバがどれほど機動性に優れていても、大規模なプロジェクトではアーキテクチャに関するドキュメントがどこかで必ず作成されます。そのドキュメントは、大きな壁ほどのサイズの Visio® ダイアグラムのセットであることも、消してはならないホワイトボード上の記述であることもあります。
脅威をモデル化するためには、そのアーキテクチャに信頼境界がいつ追加されるかを把握する必要があります。そのような作業を行う場合は、新しい脅威の影響を考慮してください。
場合によっては、脅威は直接的なものです。ポート 80 でリッスンするようにコンポーネントを変更するとします。脅威がコンポーネントに対して直接的なものである場合は、簡単に検出して対処し、作業を続けることができます。
ただし、脅威は間接的なものであることもあります。構成データを Active Directory® とライトウェイト ディレクトリ アクセス プロトコル (LDAP) から取得することにしたとします。これにより、ディレクトリ インターフェイスや抽象層 (切り替えを可能にするため) など、いくつかの新しいコンポーネントが必要になる可能性がありますが、それと同時に、構成データを変更する必要も生じます。これまでは管理者ユーザーによって GUI に入力されていたデータが、さまざまなプロトコルを使用してネットワーク経由で送られてくるようになります。おそらく、データを抽象層で慎重に検証する必要が生じるでしょう。また、構成データの確認方法や処理方法にこれまで以上に注意を払うようになるに違いありません。

何をすべきかわからなくなったら
皆でテーブルを囲んで座り、「次は何をすればいいんだろう」と頭を抱え込むような状況もあり得ます。プロセスを理解するのに苦労するかもしれません。特定の段階で完全に行き詰まることもあります。次に、これまでに SDL チームで直面したいくつかの問題と、その打開策を紹介します。
チームによっては、ダイアグラムをスムーズに作成できないことがあります。システムの機能がよくわからない場合は、詳しい人物を見つけてください。または、脅威のモデル化プロセスを中断して、引き継いだ作業を理解するために時間を割いてください。
システムにどのような機能を持たせるべきかが不明な場合 (新しいシステムの開発中に、競合する 2 つの設計があるような場合) は、脅威のモデル化が意志決定を推し進めるうえで役立ちます。両方の設計図を描き、「どちらの方が脆弱だろうか」と考えてみてください。
要素の表現方法がわからない場合は、それほど難しく考える必要はありません。不適切な要素型を使用している可能性がありますが、それはすぐに判明するでしょう。ただし、担当しているどのようなコードも、以前だれが記述したものであろうと、本質的には作成途中のものであると言えます。読み取ったり書き込んだりするデータは、すべてデータ ストアに格納されます。
また、脅威を列挙する際に頭を悩ませることもあります。何よりもまず、特定の脅威にこだわりすぎないようにすることが重要です。印を付けておいて、後から考え直せばよいのです。うまくいかない場合は、脅威のモデル化に関する Larry Osterman のすばらしいブログ記事 (go.microsoft.com/fwlink/?LinkId=118281) を参照してください。Michael Howard の著書である『Writing Secure Code』と『The Security Development Lifecycle』にも役立つ記述があります。
脅威モデルと対策の検証がうまく行かない場合は、ダイアグラムにコードが含まれているかどうか、および開発者とテスト担当者の間でコードに関する意見が一致しているかどうかを確認します。また、各要素に対する脅威の種類ごとに脅威を列挙しているかどうかを確認してください (MSDN® Magazine 2006 年 11 月号の記事「セキュリティ設計の欠陥を STRIDE 手法で明らかにする」(msdn.microsoft.com/magazine/cc163519) を参照)。最後に、脅威ごとにバグを整理および修正済みであることを確認します。
専門家がいる場合は、その助力を仰いでください。マイクロソフトの専門家たちでさえ、「あなたならこの問題にどのように対処しますか」と相談し合っています。専門家がいない場合は、専門家を採用するか、お悩みの問題についてブログ記事を書いて、解決策を手に入れてください。
1 つの問題で自分を追い詰めないようにしてください。脅威のモデル化は、避けて通ることのできない重要な作業です。とはいえ、楽しみながら取り組む必要があります。事前に "壊す" 作業には苦労も伴いますが、後から修繕するよりははるかに楽です。

ご意見やご質問は、briefs@microsoft.com まで英語でお送りください。

Adam Shostack は、マイクロソフトのセキュリティ開発ライフサイクル (SDL) チームのプログラム マネージャです。彼は、SDL の脅威のモデル化コンポーネントを担当しています。


基礎
WCF にコード アクセス セキュリティを追加する (第 2 部)
Juval Lowy

コードのダウンロード : Foundations2008_07.exe (497 KB)
オンラインでのコードの参照

目次
.NET Framework 3.5 のホスト側 CAS
部分的に信頼されたサービス
アプリケーション ドメイン ホスト
AppDomainHost を実装する
部分的に信頼されたホスト
構造化されたホスト側セキュリティ要求
構造化されたホスト要求を実装する
AppDomainHost のその他の特徴
2008 年 4 月号のこのコラムでは、Windows® Communication Foundation (WCF) に対するコード アクセス セキュリティ (CAS) のサポートが不十分であること、および WCF に適切な CAS のサポートを追加する動機について説明しました。続いて、セキュリティや WCF プログラミング モデルを損なうことなく、部分的に信頼されたクライアントが WCF サービスを呼び出せるように、CAS のサポートをクライアント側で追加する方法を説明しました (4 月号のコラムは msdn.microsoft.com/magazine/cc500644 を参照してください)。
今回は、部分的に信頼されたサービスと部分的に信頼されたホストを許可してソリューション全体を完成させるために必要なことを説明します。クライアント側については、このソリューションのしくみだけでなく、そこにつながるアプローチや思考過程についても説明します。また、WCF と Microsoft® .NET Framework の高度なプログラミング技法についても説明します。

.NET Framework 3.5 のホスト側 CAS
.NET Framework 3.5 において、WCF では BasicHttpBinding、WSHttpBinding、WebHttpBinding のバインドでサービスをホストするためには部分的に信頼されたコードのみが許可されており、セキュリティはまったく使用できないか、トランスポート セキュリティのみを使用できます。さらに WSHttpBinding では、メッセージ セキュリティ、信頼性の高いメッセージング、トランザクションなどの機能を使用できません。部分的な信頼に対応しているすべてのバインドは、テキスト エンコードを使用する必要があります。部分的な信頼で実行されているサービスでは、診断やパフォーマンス カウンタなどの追加機能を使用することはできません。
サポートされている機能の使用を一部だけに制限することはバインドで行います。HTTP でない各バインドは、サービス ホストに対し完全な信頼を自発的に要求します。許可された HTTP バインド自体は完全な信頼を要求しませんが、図 1 のとおり、使用する状況に応じてアクセス許可を要求します。
図 1 許可された HTTP バインドのホストにおける要求
シナリオ | アクセス許可
セキュリティを使用しないかまたはトランスポート セキュリティを使用する Basic、Web、WS- | 実行とインフラストラクチャのセキュリティ アクセス許可、URI への呼び出しを受け入れるための Web アクセス許可
別のアセンブリの内部サービス型を使用する上記のバインド | 無制限のリフレクション
認証された呼び出しを行う上記のバインド | プリンシパル制御のセキュリティ アクセス許可
許可された HTTP バインドはすべて、エンドポイントに構成された URI における呼び出しを受け入れるためのアクセス許可 (URI の受け入れフラグによる Web アクセス許可) の他に、実行アクセス許可とインフラストラクチャを変更するためのアクセス許可 (インフラストラクチャ フラグによるセキュリティ アクセス許可) を要求します。呼び出しが認証されている場合でも、許可された HTTP バインドはスレッド プリンシパルを制御するためのアクセス許可 (プリンシパル制御フラグによるセキュリティアクセス許可) を要求します。
これは、バインドにかかわらず、WCF では認証後に、呼び出し中に提供されるクライアントの資格情報と同じ ID の新しいプリンシパルを組み込むためです。ホスト コンストラクタに渡されたサービス型が他のアセンブリで内部型として定義されている場合も、ホストはリフレクションを使用してその型をロードできるようにするために、無制限のリフレクション アクセス許可を要求します (インフラストラクチャ アクセス許可を要求するかどうかの判断は、その明白な必要性がないので複雑です)。
構成について考慮が必要な制限は他にもあります。たとえば、構成ファイルには (サービス側の証明書用に) 証明書ストアの参照を含めることができません。証明書ストアにアクセスするには、WCF によって完全な信頼が要求されるためです。管理者は HttpConfig.exe などのツールを使用して、これらの証明書の構成を個別に行う必要があります。
バインドのみに基づいてホスト側のアクセス許可要求を行うことの欠点は、使用する必要がなくても、ホストされたサービス インスタンスにこれらのアクセス許可が暗黙的に与えられることです。たとえば、サービス インスタンスがプリンシパルを制御したり、ホスト マシン上にある別のアセンブリの内部型をリフレクションしたりできるようになります。サービス アクセス許可がホスティング コード アクセス許可のサブセットになるようにする一方で、バインドでアクセス許可を要求しながらも、何らかの形でホストがサービスに異なるアクセス許可を与えられるように設計されていた方が良かった言えます。
WCF の機能を、分散トランザクションから、信頼性の高い呼び出し、さまざまな種類のセキュリティ資格情報、TCP や名前付きパイプなどのプロセス間通信 (IPC) のバインドを使用するイントラネット (または同じマシン上の) アプリケーションに至るまで、最大限に活用できるのが理想です。双方向コールバック、非同期呼び出し、診断やトレース、インストルメンテーション、もちろん Microsoft メッセージ キュー (MSMQ) によってキューに登録された呼び出しは言うに及びません。そして、このすべてを CAS を損なうことなく (つまり完全な信頼を使用せずに) 行うことも理想的です。

部分的に信頼されたサービス
.NET Framework 3.0 では、部分的な信頼でサービスを実行する唯一の方法は、操作に必要なアクセス許可のみを明示的に与え、その結果、他のアクセス許可すべてを暗黙的に拒否することでした。これを実現する方法の 1 つが、アクセス許可属性と SecurityAction.PermitOnly フラグのマッチングを行うことでした。次のサービスについて考えてみましょう。
[SecurityPermission(
  SecurityAction.PermitOnly,
  Execution = true)]
[UIPermission(SecurityAction.PermitOnly,
  Window =
    UIPermissionWindow.SafeTopLevelWindows)]
class MyService : IMyContract {
  public void MyMethod() {
    Form form = new TestForm();
    form.ShowDialog();
  }
}
このサービスでは実行のアクセス許可が必要で (すべてのマネージ コードで必要)、セーフ ウィンドウを表示するためのアクセス許可も必要です。クラスに PermitOnly のアクセス許可属性を複数設定した場合は、実行時に 1 つのアクセス許可セットになります。
.NET Framework では、上記のアクセス許可のみを与えて他のアクセス許可すべてを積極的に拒否するために、専用のスタック ウォーク修飾子を組み込んでこのしくみを利用しています。その結果、(アプリケーション ドメインと同様に) サービスを含むアセンブリが完全な信頼をサービスに許可していても、他のすべてのアクセス許可が拒否されます。
そのサービスがファイルを開く操作などを行う場合に、セキュリティ例外が発生します。ファイル I/O の要求は、そのアクセス許可を積極的に拒否するスタック ウォーク修飾子に抵触するためです。サービスで可能な操作は、仮想的なサンドボックス内で実行して、セーフ ウィンドウを表示することのみです。.NET Framework では、アプリケーションが部分的に信頼されていることをユーザーに通知するために、図 2 のようなフォームでウィンドウ キャプションを変更し、警告タグを表示します。
図 2 部分的に信頼されたコードから表示されたウィンドウ (クリックすると拡大画像が表示されます)
属性としてアクセス許可を指定する代わりに、図 3 のようにアクセス許可セット XML ファイルに一覧を記述して、PermissionSetAttribute にそのファイル名を指定することもできます。
図 3 部分的に信頼されたサービスに対するアクセス許可設定ファイルを使用する
<!-- MyServicePermissions.xml -->
<PermissionSet class = "System.Security.PermissionSet">
  <IPermission
    class = "System.Security.Permissions.SecurityPermission"
    Flags = "Execution"
  />
  <IPermission
    class = "System.Security.Permissions.UIPermission"
    Window = "SafeTopLevelWindows"
  />
</PermissionSet>

[PermissionSet(SecurityAction.PermitOnly,File =
  "MyServicePermissions.xml")]
class MyService : IMyContract {
  public void MyMethod() {
    Form form = new TestForm();
    form.ShowDialog();
  }
}
アクセス許可設定ファイルはコンパイル時のみに使用され、展開時には使用されないことに注意してください。コンパイラは与えられたアクセス許可をクラスのメタデータに個別属性として結合します。ファイルがない場合は、ビルドが失敗します。

アプリケーション ドメイン ホスト
部分的に信頼されたサービスに対する属性に基づいたアプローチには、いくつかの問題があります。第一に、サービスが行うことは毎回変わり、さらに言えばサービスが使用する下流のクラスでも同様です。属性の組み合わせを修正したり、設定ファイルを変更したりする必要が生じます。これではサービスとクラスが結びつき、保守コストが増大します。
第二に、アクセス許可はサービス定義の一部であるため、サービスをホスティング環境の製品として、異なるアクセス許可で使用する方法がありません。アクセス許可を増やすことも減らすこともできません。
第三にさらに重要なのは、ほとんどのサービスでは、最低限の権限やアクセス許可で実行するために、操作に必要なセキュリティのアクセス許可を分析したり、与えられたアクセス許可を慎重に減らしたりする手間はかけられないということです。あるとすれば、ホストによってロードされるサービスが何に依存しているか考慮することです。それでもホストには、既定ではアクセス許可を変更する機能がありません。WCF の ServiceHost はすべてのサービスを完全な信頼でホストします。
これはサービス自体の保護にも有効ですが、サービスに最適と判断されるアクセス許可をどれでも与えられることはホストにも適しています。以上をふまえて、図 4 で定義されている AppDomainHost クラスを作成しました。
図 4 AppDomainHost
public class AppDomainHost : IDisposable {
  //Create new app domain in full trust
  public AppDomainHost(Type serviceType,params Uri[] baseAddresses);
  public AppDomainHost(Type serviceType,string appDomainName,
    params Uri[] baseAddresses);

  //Create new app domain with specified permission set
  public AppDomainHost(Type serviceType,PermissionSet permissions,
    params Uri[] baseAddresses);
  public AppDomainHost(Type serviceType,PermissionSet permissions,
    string appDomainName,params Uri[] baseAddresses);

  //Additional constructors that take standard permission set,
  //permission set filename, and an existing app domain

  public void Open();
  public void Close();
  public void Abort();

  //More members
}
AppDomainHost クラスは WCF で提供されている ServiceHost と同様に使用できます。
AppDomainHost host =
  new AppDomainHost(typeof(MyService));
host.Open();
相違点は、AppDomainHost では、指定されたサービス型を呼び出し元のアプリケーション ドメインではなく新しいアプリケーション ドメインでホストするということです。新しいアプリケーション ドメイン名は、既定で "AppDomain Host for" の後ろにサービス型と新しい GUID が付きます。新しいアプリケーション ドメインの名前の指定もできます。
AppDomainHost host =
  new AppDomainHost(typeof(MyService),"My App Domain");
host.Open();
新しいアプリケーション ドメインは、既定では完全な信頼で作成されます。ただし、部分的に信頼されたサービスを管理するためには、別のアプリケーション ドメインにサービスを配置することが重要です。
AppDomainHost では、新しいアプリケーション ドメインのアクセス許可を指定できます。たとえば図 5 は、あるサービスと、そのサービスの動作に必要十分なアクセス許可のみを与えるホストを示しています。
図 5 部分的に信頼されたサービス
class MyService : IMyContract {
  public void MyMethod() {
    Form form = new TestForm();
    form.ShowDialog();
  }
}

//Hosting code:
PermissionSet permissions =
  new PermissionSet(PermissionState.None);
permissions.AddPermission(new SecurityPermission(
  SecurityPermissionFlag.Execution));
permissions.AddPermission(new UIPermission(
  UIPermissionWindow.SafeTopLevelWindows));

AppDomainHost host =
  new AppDomainHost(typeof(MyService),permissions);

host.Open();
アクセス許可設定ファイルも使用できます。図 3 で示したファイルとは異なり、このファイルが必要なのは実行時のみで、展開後には変更できます。標準の名前付きアクセス許可セットの 1 つを指定することもできることに注目してください。部分的に信頼されたサービスに強制された仮想サンドボックスは、.NET Framework 1.0 から WCF までの何らかの技術によってサンドボックス外側の呼び出しを行う機能を使用する下流のクラスすべてに影響を及ぼします。
図 6 に示すとおり、1 つのホスト プロセスで複数の AppDomainHost インスタンスを作成し、それぞれ同じまたは異なるサービス型をホストし、任意のアクセス許可セットを指定できます (図 5 と同じサービス定義を使用します)。
図 6 同じサービス型に異なるアクセス許可を指定する
//Default is service with full trust
AppDomainHost host0 =
  new AppDomainHost(typeof(MyService),
  "Full Trust App Domain",
  new Uri("net.tcp://localhost:6000"));

host0.Open();

//With just enough permissions to do work
PermissionSet permissions1 =
  new PermissionSet(PermissionState.None);
permissions1.AddPermission(new SecurityPermission(
  SecurityPermissionFlag.Execution));
permissions1.AddPermission(new UIPermission(
  UIPermissionWindow.SafeTopLevelWindows));

AppDomainHost host1 =
  new AppDomainHost(typeof(MyService),permissions1,
  "Partial Trust App Domain",
  new Uri("net.tcp://localhost:6001"));

host1.Open();

//With not enough permissions to do work
PermissionSet permissions2 =
  new PermissionSet(PermissionState.None);
permissions2.AddPermission(new SecurityPermission(
  SecurityPermissionFlag.Execution));

AppDomainHost host2 =
  new AppDomainHost(typeof(MyService),permissions2,
  "Not enough permissions",new Uri("net.tcp://localhost:6002"));

host2.Open();

//Using one of the named permission sets
AppDomainHost host3 =
  new AppDomainHost(typeof(MyService),
  StandardPermissionSet.Internet,
  "Named permission set",
  new Uri("net.tcp://localhost:6003"));

host3.Open();

AppDomainHost を実装する
AppDomainHost の実装は 2 つの作業に分かれます。まず、新しいアプリケーション ドメインを作成してサービス ホスト インスタンスを挿入します。その後サービス ホスト (およびサービス インスタンス) を部分的な信頼で実行します。サービス ホスト インスタンスを作成して、別のアプリケーション ドメインでアクティブ化するために、図 7 のように ServiceHostActivator クラスを記述しました。
図 7 ServiceHostActivator
class ServiceHostActivator : MarshalByRefObject {
  ServiceHost m_Host;

  public void CreateHost(Type serviceType,Uri[] baseAddresses) {
    m_Host = new ServiceHost(serviceType,baseAddresses);
  }
  public void Open() {
    m_Host.Open();
  }
  public void Close() {
    m_Host.Close();
  }
  public void Abort() {
    m_Host.Abort();
  }

  //Rest of the implementation
}
ServiceHostActivator は WCF で提供される標準の ServiceHost インスタンスの単純なラッパーです。ServiceHostActivator は MarshalByRefObject から派生しているため、アプリケーション ドメインの境界を越えて AppDomainHost から呼び出すことができます。CreateHost メソッドは新しい ServiceHost インスタンスの作成をカプセル化します。ServiceHostActivator の他のメソッドは、基になっているホスト インスタンスにリモート呼び出しを転送するだけです。
AppDomainHost はオーバーロードされたコンストラクタをいくつか提供しています。これらのコンストラクタはすべて互いに呼び出し合って (図 8 参照)、新しいアプリケーション ドメインの作成も行いながら、最終的にはサービス型、新しいアプリケーション ドメイン インスタンス、新しいドメインのアクセス許可セット、およびベース アドレスを指定する protected のコンストラクタを使用します。
図 8 AppDomainHost を実装する
public class AppDomainHost : IDisposable {
  ServiceHostActivator m_ServiceHostActivator;

  public AppDomainHost(Type serviceType,
    params Uri[] baseAddresses) :
    this(serviceType,"AppDomain Host for "+
    serviceType+" "+Guid.NewGuid(),
    baseAddresses) {
  }

  public AppDomainHost(Type serviceType,
    string appDomainName,
    params Uri[] baseAddresses) : this(serviceType,
    new PermissionSet(PermissionState.Unrestricted),
    appDomainName,baseAddresses) {
  }

  public AppDomainHost(Type serviceType,
  PermissionSet permissions,
  string appDomainName,
  params Uri[] baseAddresses) :
  this(serviceType,AppDomain.CreateDomain(appDomainName),
  permissions,baseAddresses) {
  }

  //More constructors

  protected AppDomainHost(Type serviceType,
    AppDomain appDomain,
    PermissionSet permissions,Uri[] baseAddresses) {

    string assemblyName = Assembly.GetAssembly(
      typeof(ServiceHostActivator)).FullName;
    m_ServiceHostActivator = appDomain.CreateInstanceAndUnwrap(
      assemblyName,typeof(ServiceHostActivator).ToString()) as
      ServiceHostActivator;

    CodeAccessSecurityHelper.SetPermissionsSet(appDomain,permissions);

    m_ServiceHostActivator.CreateHost(serviceType,baseAddresses);
  }

  public void Open() {
    m_ServiceHostActivator.Open();
  }
  public void Close() {
    m_ServiceHostActivator.Close();
  }
  public void Abort() {
    m_ServiceHostActivator.Abort();
  }
  void IDisposable.Dispose() {
    Close();
  }
}
AppDomainHost の protected のコンストラクタでは、.NET リモーティングを使用して新しいアプリケーション ドメインに ServiceHostActivator のインスタンスを挿入し、最終的にそのリモート処理プロキシを m_ServiceHostActivator メンバへ格納します。
新しいアプリケーション ドメインは、既定では完全な信頼で作成されます。AppDomainHost では、与えられたアクセス許可のみを認めて他を拒否する新しい CAS ポリシーを新しいアプリケーション ドメインに組み込むために、自作の CodeAccessSecurityHelper クラスの SetPermissionsSet メソッドを使用します。
public static class CodeAccessSecurityHelper {
  public static void SetPermissionsSet(
    AppDomain appDomain,
    PermissionSet permissions) {

    PolicyLevel policy = PolicyLevel.CreateAppDomainLevel();
    policy.RootCodeGroup.PolicyStatement =
      new PolicyStatement(permissions);
    appDomain.SetAppDomainPolicy(policy);
  }
  //More members
}
これは、アプリケーション ドメイン レベルで新しいセキュリティ ポリシーを作成し、AppDomain クラスの SetAppDomainPolicy メソッドを呼び出すのと同様にシンプルです。
public sealed class AppDomain :
  MarshalByRefObject,... {

  public void SetAppDomainPolicy(
    PolicyLevel domainPolicy);
  //More members
}
偶然ですが同様の技法が ClickOnce でも使用されており、ClickOnce で展開されたアプリケーションの部分的な信頼での実行を強制します。
Open や Close など、AppDomainHost の他のメソッドを呼び出すと ServiceHostActivator のプロキシを使用して他のアプリケーション ドメインに呼び出しを行い、そのホスト インスタンスのオープンやクローズを行います。サービス インスタンスはホストをオープンしたアプリケーション ドメインで実行されるので、サービスもそのアプリケーション ドメインのセキュリティ ポリシー下で実行されます。

部分的に信頼されたホスト
ここまでご紹介してきた部分的に信頼されたサービスに対するアプローチは、完全な信頼で実行される AppDomainHost を使用するコードを作成することを前提としていました。これは、図 1 に記載されていないホスティングのシナリオでは、ServiceHost とバインドが完全な信頼を要求するためです。
では、ホストを起動するコードが部分的にしか信頼されていない場合はどうなるでしょうか。AppDomainHost と ServiceHostActivator を完全に信頼されたアセンブリに配置し、部分的に信頼された呼び出し元を許可して、両方に完全な信頼をアサートすることは可能です。
[PermissionSet(SecurityAction.Assert,Unrestricted = true)]
public class AppDomainHost : IDisposable
{...}

[PermissionSet(SecurityAction.Assert,Unrestricted = true)]
class ServiceHostActivator : MarshalByRefObject
{...}
このアプローチでも動作しますが、CAS を回避して重要なセキュリティ メカニズムを無効化するため、2 つのセキュリティ上の問題を招くことになります。第一に、コードをオープンするホストに、トランスポート チャネルの呼び出しを受け入れたり、分散トランザクションなどの WCF アクティビティに参加したりするためのアクセス許可があることを前提にするのは誤りです。第二に、完全な信頼をアサートしてスタック ウォークを抑制することにより、ホストを起動した部分的に信頼されたコードは、サービスをより高い権限で作成してホストに対する不正行為をするために利用される可能性があるということです。たとえば、ホストするコードにはファイル I/O のアクセス許可がなくても、ファイル I/O のアクセス許可があるサービスに対する呼び出しを受け入れるために AppDomainHost が使用される可能性があります。
この解決方法は単純です。AppDomainHost と ServiceHostActivator では、完全な信頼の無条件なアサートを行わずに、必要なときのみローカルにアサートを行い、その後は元のアクセス許可に戻す必要があります。さらに、AppDomainHost では使用する側のコードを疑い、適切なホスティングのアクセス許可 (呼び出しの受け入れなど) を要求する必要があります。ホストするコードに、サービスを実行するために最低限必要なアクセス許可があるかどうかも、AppDomainHost では確認する必要があります。これにより、別のアプリケーション ドメイン内でホストされたサービスを、制限されている部分的に信頼されたコードが高いアクセス許可で使用できないようにします。最後に残ったのは、構造化されたホスト側セキュリティ要求です。

構造化されたホスト側セキュリティ要求
部分的に信頼された呼び出し元に合わせて書き直した ServiceHostActivator を図 9 に示します。ServiceHostActivator の CreateHost メソッドは、宣言で完全な信頼をアサートすることはできません。これにより、呼び出し元コードの適切なホスト アクセス許可を要求できなくなるためです。代わりに、プログラムで CodeAccessSecurityHelper を使用して完全な信頼のアクセス許可セットを作成してアサートし、それからホストを作成します。WCF が完全な信頼を要求する場合、アサートによってその要求がコール スタックに到達できないようにします。
図 9 書き直した ServiceHostActivator
class ServiceHostActivator : MarshalByRefObject {
  public void CreateHost(Type serviceType,Uri[] baseAddresses) {
    CodeAccessSecurityHelper.PermissionSetFromStandardSet(
    StandardPermissionSet.FullTrust).Assert();

    m_Host = new ServiceHost(serviceType,baseAddresses);

    PermissionSet.RevertAssert();

    m_Host.DemandHostPermissions();
  }

  //Behavior demands happen here, must assert
  [PermissionSet(SecurityAction.Assert,Unrestricted = true)]
  public void Open() {
    m_Host.Open();
  }
  ...
}
ホストの作成後、CreateHost は明示的にアサートを元に戻します。その後 CreateHost はホスティングのアクセス許可を要求するために、作成したばかりのホスト インスタンス上で CodeAccessSecurityHelper の拡張ヘルパ メソッド DemandHostPermissions を呼び出します。DemandHostPermissions はホスト オブジェクトを検証し、適切なアクセス許可を要求します。
たとえば、ホストが TCP エンドポイントをサポートしている場合、DemandHostPermissions は実行のアクセス許可と、エンドポイント URI における TCP 呼び出し受け入れのアクセス許可を要求します。TCP で信頼性の高いメッセージングが使用されている場合、DemandHostPermissions はポリシー制御のアクセス許可を要求します。図 10 のとおり、他にも考えられる要求は数多くあります。
図 10 AppDomainHost を使用して構造化されたホスト側要求
シナリオ | アクセス許可
TCP | 実行のセキュリティ アクセス許可、URI における TCP 呼び出しを受け入れるための Socket アクセス許可
IPC | 実行、ポリシー制御、証拠制御のセキュリティ アクセス許可
MSMQ | 実行のセキュリティ アクセス許可、キューから読み取るための MSMQ アクセス許可
WS、WS-Dual、Basic、Web | 実行のセキュリティ許可、URI における呼び出しを受け入れるための Web アクセス許可
RM over TCP | ポリシー制御のセキュリティ アクセス許可
認証された呼び出し | プリンシパル制御のセキュリティ アクセス許可
トランザクション伝達 | 無制限の分散トランザクション アクセス許可
メッセージ セキュリティを使用したユーザー名の資格情報、検証された証明書の資格情報、サービス証明書 | ストアの列挙、ストアのオープン、証明書の列挙をするためのストア アクセス許可
診断トレース | COMPUTERNAME を読み取るための環境アクセス許可、パス検出およびログ ファイルへの追加と書き込みのためのファイル I/O アクセス許可
Windows サービス パフォーマンス カウンタ | サービス カウンタへの書き込み、エンドポイント カウンタへの書き込み、操作カウンタへの書き込みをするためのパフォーマンス カウンタ アクセス許可
すべての WCF パフォーマンス カウンタ | サービス カウンタへの書き込み、エンドポイント カウンタへの書き込み、操作カウンタへの書き込み、ホスト カウンタへの書き込みをするためのパフォーマンス カウンタ アクセス許可
ASP.NET プロバイダ | 最低限の ASP.NET ホスティング アクセス許可
IPC バインドを使用する場合、ポリシーと証拠を制御するためのセキュリティ アクセス許可を要求します。MSMQ バインドを使用する場合、エンドポイント キューから読み取るためのアクセス許可を要求します。HTTP バインドのいずれかを使用する場合、エンドポイント アドレスにおける呼び出しの受け入れを要求します。認証された呼び出しをバインドで使用する場合、DemandHostPermissions はプリンシパル制御のためのアクセス許可を要求します。
トランザクション対応のバインドを使用し、トランザクション フローを有効化して、トランザクション フローが許可されている (または必須となっている) エンドポイントのコントラクトで少なくとも 1 つの操作が行われる場合、無制限の分散トランザクション アクセス許可を要求します。
メッセージ セキュリティ、クライアント証明書の検証、メッセージ保護を目的としたサービス証明書の使用など、証明書ストアへのアクセスはすべて、ストアを開いて証明書を列挙するために、ストアの列挙を要求します。
サービスが診断を行う場合、DemandHostPermissions はコンピュータ名の環境変数へのアクセスと対象ファイルに対するファイル I/O のアクセス許可を要求します。ホストが WCF パフォーマンス カウンタを使用する場合、DemandHostPermissions はカウンタに報告されたレベルで書き込むためのアクセス許可を要求します。
呼び出し元の認証やロール メンバシップにおいてホストが ASP.NET プロバイダに依存している場合、DemandHostPermissions は最低限の ASP.NET ホスティング アクセス許可を要求します (このアクセス許可はすべてのプロバイダで必要です)。WS-Dual バインドによるコールバックをする場合、バインド自体がコールバック エンドポイントに接続するための Web アクセス許可を要求します。そのため、ホストの起動時に AppDomainHost で明示的に要求する必要はありません。

構造化されたホスト要求を実装する
図 11 に DemandHostPermissions の実装の一部を示します。まずエンドポイント固有の要求を行います。サービスのエンドポイントのコレクションを反復処理して、各エンドポイントにそれぞれの接続、セキュリティ、トランザクションのアクセス許可を要求します。
図 11 DemandHostPermissions を実装する
public static class CodeAccessSecurityHelper {
  internal static void DemandHostPermissions(this ServiceHost host) {
    foreach(ServiceEndpoint endpoint in host.Description.Endpoints) {
      DemandHostConnectionPermissions(endpoint);
      DemandHostSecurityPermissions(endpoint);
      using(TransactionScope scope = new TransactionScope()) {
        DemandTransactionPermissions(endpoint);
      }
    }
    DemandHostStorePermissions(host);
    DemandPerformanceCounterPermissions();
    DemandTracingPermissions();
    DemanAspNetProvidersPermissions(host);
  }
  internal static void DemandHostConnectionPermissions(
    ServiceEndpoint endpoint) {

    PermissionSet connectionSet =
      new PermissionSet(PermissionState.None);
    if(endpoint.Binding is NetTcpBinding) {
      connectionSet.AddPermission(new SocketPermission(
        NetworkAccess.Accept, TransportType.Tcp,
        endpoint.Address.Uri.Host,endpoint.Address.Uri.Port));
    }

    /* Checking the other bindings */

    connectionSet.Demand();
  }

  static void DemanAspNetProvidersPermissions(ServiceHost host) {
    bool demand = false;

    foreach(IServiceBehavior behavior in host.Description.Behaviors) {
      if(behavior is ServiceCredentials) {
        ServiceCredentials credentialsBehavior =
          behavior as ServiceCredentials;

        if(credentialsBehavior.UserNameAuthentication.
          UserNamePasswordValidationMode ==
          UserNamePasswordValidationMode.MembershipProvider)  {

          demand = true;
          break;
        }
      }
      if(behavior is ServiceAuthorizationBehavior) {
        ServiceAuthorizationBehavior serviceAuthorization =
          behavior as ServiceAuthorizationBehavior;
        if(serviceAuthorization.PrincipalPermissionMode ==
          PrincipalPermissionMode.UseAspNetRoles &&
          Roles.Enabled) {

          demand = true;
          break;
        }
      }
    }
    if(demand) {
      IPermission permission =
        new AspNetHostingPermission(
        AspNetHostingPermissionLevel.Minimal);
      permission.Demand();
    }
  }
  //Rest of the implementation
}
トランザクション要求については、トランザクション スコープを使用して一時的なアンビエント トランザクションを作成します。これにより、クライアント側でも同じメソッドが使用可能になります。このメソッドは呼び出し時にアンビエント トランザクションの存在に基づいてアクセス許可を要求します。
エンドポイント要求後に、DemandHostPermissions はサービス ホスト構成の生成物であるアクセス許可を要求します。具体的には証明書ストアへのアクセス許可、パフォーマンス カウンタ アクセス許可、トレース アクセス許可、ASP.NET プロバイダ アクセス許可です。たとえば、ASP.NET プロバイダ アクセス許可を要求するために、サービス動作のコレクションを取得し、ホストのサービス資格情報の動作がメンバシップ プロバイダを使用していないか、またはホストのサービス認証動作がロール プロバイダを使用していないか確認し、使用している場合には ASP.NET ホスティング アクセス許可を要求します。
部分的に信頼された環境で構造化要求をサポートするには、AppDomainHost も以下のように一部書き直す必要があります。
[SecurityPermission(
  SecurityAction.Assert,ControlAppDomain = true)]
[ReflectionPermission(
  SecurityAction.Assert,Unrestricted = true)]
public class AppDomainHost : IDisposable {
  protected AppDomainHost(Type serviceType,
    AppDomain appDomain,
    PermissionSet permissions,
    params Uri[] baseAddresses) {
    //Cannot grant service permissions
    // the host does not have
    permissions.Demand();

    //Rest of constructor
    }
}
AppDomainHost は新しいアプリケーション ドメインを作成するため、そのアプリケーション ドメインを制御するためのアクセス許可をアサートします。他のアプリケーション ドメインにホストを挿入するのにリフレクションを使用するため、無制限のリフレクション アクセス許可もアサートします。重要なのは、新しいアプリケーション ドメインにサービス ホストを作成する前に、呼び出し元のコードのサービスで求められたアクセス許可を AppDomainHost が要求するということです。これにより部分的に信頼されたコードでは、呼び出し元のコードでの許可を超えることなく、まったく同じだけのことを許可されたサービスのみを開始およびホストできるようになります。

AppDomainHost のその他の特徴
AppDomainHost は ICommunicationObject をサポートしていませんが、そのイベント モデルやステート マシン管理は提供しています。また、AppDomainHost はいくつかの静的変数を呼び出し元のアプリケーション ドメインから、作成した新しいアプリケーション ドメインへコピーします。具体的には、ServiceHostActivator を使用して別のアプリケーション ドメインに ASP.NET プロバイダ名をコピーします。AppDomainHost は一時的に ASP.NET ホスティング アクセス許可をアサートし、プロバイダへアクセスして、メンバシップおよびロール アプリケーション名の値をコピーした後、アサートを元に戻します。

Juval Lowy は、WCF のトレーニングとアーキテクチャ コンサルティングを行う IDesign 社に所属するソフトウェア アーキテクトです。最近の著書としては、『Programming WCF Services』があります。彼は、シリコン バレー地域の Microsoft Regional Director も務めています。連絡先は www.idesign.net です。


.NET の問題
WebClient の非同期 I/O
Stephen Toub

Q データの読み取り、書き込みをするのに System.IO.File クラスの静的ヘルパ メソッドが気に入っています。ReadAllText、ReadAllBytes、WriteAllText、WriteAllBytes などです。ただし、これらのメソッドは同期処理です。内部的にも非同期 I/O を使用して、これらのメソッドを非同期に使用できるようにしたいと考えています。System.IO.FileStream クラスで非同期 I/O がサポートされているのは知っていますが、File クラスではサポートされていますか。

A File のメソッドでは同期操作しかサポートされていません。ただし、おっしゃるような非同期メソッドを実装する機能は確かにあります。このコラムでは 2 つの実現方法を紹介します。複雑な方から見ていきましょう。ただし、こちらの方がより効率的な方法です。
まず、実際に実装する API を定義するところから始めます。あなたが挙げたシグネチャは、元は次のようになっています。
public static byte [] ReadAllBytes(string path);
public static string ReadAllText(string path);

public static void WriteAllBytes(string path, byte [] bytes);
public static void WriteAllText(string path, string contents);
これらの非同期バージョンが必要なので、次のように定義します。
public static void ReadAllBytesAsync(
    string path, Action<byte[]> success, Action<Exception> failure);
public static void ReadAllTextAsync(
    string path, Action<string> success, Action<Exception> failure);

public static void WriteAllBytesAsync(
    string path, byte[] bytes,
    Action success, Action<Exception> failure);
public static void WriteAllTextAsync(
    string path, string contents,
    Action success, Action<Exception> failure);
これらは元のシグネチャとよく似ています。Read* メソッドは同期的にデータを返す代わりに 2 つのデリゲートを受け取ります。1 つは実行が成功した場合に、もう 1 つは実行が例外になった場合に使用されるもので、前者のデリゲートには読み取ったデータが渡され、後者のデリゲートには発生した例外が渡されます。Write* メソッドも 2 つのデリゲートを受け取りますが、成功のデリゲートには出力がないのでパラメータはありません (元の Write* メソッドは void を返します)。
さらに複雑なアプローチでは、FileStream の派生元である System.IO.Stream クラスで公開されている非同期プログラミング モデル (APM) パターンによって直接これらを実装します。2008 年 3 月号の MSDN® Magazine における私のコラム (msdn.microsoft.com/magazine/cc337900) では、ある Stream から別の Stream へ、各 Stream の APM 実装を使用して非同期でコピーを行う CopyStreamToStream メソッドの実装を紹介しました。参考のために図 1 にこの実装を示し、ここで再利用します (この実装は、前回紹介したときより少し単純化し、変更を加えました。System.ComponentModel.AsyncOperationManager と AsyncOperation の使用は削除し、このコラムで実装する、より高レベルな API に移行することにしました。理由は後ほど詳しく説明します)。
図 1 非同期の Stream コピー
public static void CopyStreamToStream(
    Stream source, Stream destination, Action<Exception> completed)
{
    byte[] buffer = new byte[0x1000];
    if (completed == null) completed = delegate {};

    AsyncCallback rc = null;
    rc = readResult =>
    {
        try
        {
            int read = source.EndRead(readResult);
            if (read > 0)
            {
                destination.BeginWrite(buffer, 0, read, writeResult =>
                {
                    try
                    {
                        destination.EndWrite(writeResult);
                        source.BeginRead(
                            buffer, 0, buffer.Length, rc, null);
                    }
                    catch (Exception exc) { completed(exc); }
                }, null);
            }
            else completed(null);
        }
        catch (Exception exc) { completed(exc); }
    };

    source.BeginRead(buffer, 0, buffer.Length, rc, null);
}
この実装が、これらの非同期メソッドの実装における最も難しい部分です。CopyStreamToStream に、2 つのヘルパ メソッドを実装します。1 つはファイルから非同期にバイト読み取りをするため、もう 1 つはファイルへ非同期にバイト書き込みをするためです。これらが処理の大部分を行います (図 2 参照)。
図 2 非同期のヘルパ メソッド
private static void ReadAllBytesAsyncInternal(string path,
    Action<byte[]> success, Action<Exception> failure)
{
    var input = new FileStream(path, FileMode.Open,
        FileAccess.Read, FileShare.Read, 0x1000, true);
    var output = new MemoryStream((int)input.Length);
    CopyStreamToStream(input, output, e =>
    {
        byte [] bytes = e == null ? output.GetBuffer() : null;
        output.Close();
        input.Close();

        if (e != null) failure(e);
        else success(bytes);
    });
}

private static void WriteAllBytesAsyncInternal(
    string path, byte[] bytes,
    Action success, Action<Exception> failure)
{
    var input = new MemoryStream(bytes);
    var output = new FileStream(path, FileMode.Create,
        FileAccess.Write, FileShare.None, 0x1000, true);
    CopyStreamToStream(input, output, e =>
    {
        input.Close();
        output.Close();

        if (e != null) failure(e);
        else success();
    });
}
ReadAllBytesAsyncInternal では、基になっているストリームを非同期に処理するための入力用 FileStream と、ファイルから読み取ったバイトを格納するための MemoryStream を作成します。そして、すべてのデータを非同期に FileStream から MemoryStream へコピーするために CopyStreamToStream メソッドを使用します。操作が完了したらストリームを閉じます。失敗のデリゲートは例外がスローされると呼び出されます。それ以外は成功のデリゲートが呼び出され、ファイルから読み取られたデータが MemoryStream に渡されます。
WriteAllBytesAsyncInternal もよく似ています。ここでは、渡されたバイト配列をラップする入力用 MemoryStream が作成され、出力用 FileStream が作成されます。前述のとおり、これは非同期 I/O をサポートしています。ReadAllBytesAsyncInternal と同様に、完了したらストリームを閉じ、例外が発生した場合には失敗のデリゲートが呼び出されます。
前述の各パブリック シグネチャを実装するには、図 1 と図 2 で作成したメソッドに数行を追加する必要があります。図 3 に示します。
図 3 パブリック メソッドを実装する
public static void ReadAllBytesAsync(
    string path, Action<byte[]> success, Action<Exception>
        failure)
{
    AsyncOperation asyncOp =
        AsyncOperationManager.CreateOperation(null);
    ReadAllBytesAsyncInternal(path,
        bytes => asyncOp.Post(delegate { success(bytes); }, null),
        exception => asyncOp.Post(
            delegate { failure(exception); }, null));
}

public static void ReadAllTextAsync(
    string path, Action<string> success, Action<Exception>
        failure)
{
    AsyncOperation asyncOp =
        AsyncOperationManager.CreateOperation(null);
    ReadAllBytesAsyncInternal(path,
        bytes => {
            string text;
            using (var ms = new MemoryStream(bytes))
                text = new StreamReader(ms).ReadToEnd();
            asyncOp.Post(delegate { success(text); }, null);
        },
        exception => asyncOp.Post(
            delegate { failure(exception); }, null));
}

public static void WriteAllBytesAsync(
    string path, byte[] bytes, Action success, Action<Exception>
       failure)
{
    AsyncOperation asyncOp =
        AsyncOperationManager.CreateOperation(null);
    WriteAllBytesAsyncInternal(path, bytes,
        () => asyncOp.Post(delegate {success(); }, null),
        exception => asyncOp.Post(
            delegate { failure(exception); }, null));
}

public static void WriteAllTextAsync(
    string path, string contents, Action success, Action<Exception>
       failure)
{
    AsyncOperation asyncOp =
        AsyncOperationManager.CreateOperation(null);
    ThreadPool.QueueUserWorkItem(delegate
    {
        var bytes = Encoding.UTF8.GetBytes(contents);
        WriteAllBytesAsyncInternal(path, bytes,
            () => asyncOp.Post(delegate {success(); }, null),
            exception => asyncOp.Post(
                delegate { failure(exception); }, null));
    });
}
これらのメソッドのほとんどの部分は、図 2 で示した内部実装の単純なラッパーです。ただし、興味深い細かな問題もいくつかあります。まず前述のとおり、CopyStreamToStream からは AsyncOperationManager のサポートを削除しました。AsyncOperation 自体は System.Threading.SynchronizationContext のラッパーです。これは AsyncOperationManager.CreateOperation の呼び出しにおいてキャプチャされた、基になる SynchronizationContext を使用して、作成時点の同期コンテキストの現状に合った方法でデリゲートを Post して返すために使用されます。たとえば Windows® フォーム アプリケーションの UI スレッドでは、UI スレッドへのデリゲートのコールバックのマーシャリングをサポートしている WindowsFormsSynchronizationContext を SynchronizationContext.Current が返すことが想定されます。そのため AsyncOperationManager.CreateOperation が UI スレッドで呼び出された場合、取得した AsyncOperation の Post メソッドを使用すると、提供されたデリゲートがマーシャリングされて UI スレッドで実行されることになります。
ただし、UI スレッド上で行われる処理量を最小化する必要があります。ここで ReadAllTextAsync メソッドについて考えてみましょう。ファイルからのデータの読み込みが完了したら、読み取ったバイトを文字列に変換します (書き込み可能な StringStream クラスがあり、MemoryStream を渡すのではなく、StringStream のインスタンスを CopyStreamToStream に渡すことができたら効率的なのですが)。その変換は、UI スレッドで必要以上のコストがかかる可能性があります。そのため UI にポスト バックする前に、この変換を行います。ただし CopyStreamToStream の元の実装では、完了のデリゲートはキャプチャされた SynchronizationContext で実行されているため、変換するときには既に UI スレッド上にいることになります。代わりに AsyncOperation 処理をこれらの各外部メソッドに引き込むことで、実際の演算処理が終わるまで Post の呼び出しを遅らせることができるようにしました。
実装についてもう 1 つ興味深い点が WriteAllTextAsync にあります。非同期操作が開始されるまで WriteAllTextAsync は制御を戻しません。ただし、そのために WriteAllBytesAsyncInternal を呼び出す前に、渡されたテキスト文字列をバイト配列に変換する必要があります。そのため呼び出し元のスレッドをブロックするのではなく、作業項目を ThreadPool のキューに追加します。その作業項目では文字列からバイトへの変換を行い、それから内部コピーを開始します。
最終的には膨大なコードにはなっていませんが、単純でもありません。このようなファイル データの読み取りや書き込みの非同期の一般的なパターンがあらかじめ Microsoft® .NET Framework のどこかにあればいいのにと思われるに違いありません。データの読み取りや書き込み中の進行状況の通知など、さらに追加したいことがある場合はなおさらです。実はそのような機能が既に存在するのですが、意外な場所にあります。System.Net です。
System.Net.WebClient クラスは幅広い用途において非常に有効です。その名前と、System.Net の名前空間に配置されていることから、多くの人が HTTP サーバーからのファイル ダウンロード、FTP サイトへのファイル アップロードなど、Web 関連のアクティビティ専用だと思っていることも不思議ではありません。ただし WebClient は、これら各種の具体的な実装を作成するために、プラグ可能なファクトリ モデルをサポートしている WebRequest と WebResponse の上位にうまく抽象化されています。HTTP URL を指定して WebRequest.Create を呼び出すと HttpWebRequest のインスタンスが返されます。同様に FTP URL を指定すると FtpWebRequest のインスタンスが返されます。WebClient では WebRequest と WebResponse を内部的に使用して、数多くの便利な機能を実装できます。身近なメソッドの例を挙げます。
public void DownloadDataAsync(Uri address);
public event DownloadDataCompletedEventHandler DownloadDataCompleted;

public void DownloadStringAsync(Uri address);
public event
    DownloadStringCompletedEventHandler DownloadStringCompleted;

public void UploadDataAsync(Uri address, byte[] data);
public event UploadDataCompletedEventHandler UploadDataCompleted;

public void UploadStringAsync(Uri address, string data);
public event UploadStringCompletedEventHandler UploadStringCompleted;
これで、ダウンロードするデータの URL を指定して DownloadDataAsync を呼び出せば非同期にダウンロードが行われ、完了したら DownloadDataCompleted イベントが発生するようになります。同様に、テキスト文字列とその文字列をアップロードする場所を指定して UploadStringAsync を呼び出せば非同期でアップロードが行われ、完了したら UploadStringCompleted イベントが発生します。これならば便利で簡単です。
ところで、すばらしいのは .NET Framework に組み込まれている WebRequest プロバイダの 1 つに FileWebRequest (および、対応する FileWebResponse) があることです。これを使って、次のようなコードを記述できます。
WebRequest wr = Webrequest.Create(@"file://C:\test.txt");
このコードでは、どこかの Web サイト上のファイルではなくディスク上のファイルを対象としていることを除いて、他と同じように使用できる WebRequest を取得できます。WebClient における WebRequest の使用と組み合わせて、私が何をしようとしているかおわかりでしょう。非同期でファイルの読み取りや書き込みをするのに、WebClient を使用できるのです。
前半で作成したような読み取りおよび書き込みメソッドを実装するには、図 4 で示すとおり、1 つのメソッドにつき数行のコードだけで済みます (実際のところ、現時点では、作成した API シグネチャを同じタスクの WebClient で提供されるシグネチャに合わせて調整することだけにコードの大部分が費やされているので、これらのラッパーを用意する意味があるのか疑問です)。コールバックが正しいコンテキストで発生するようにするため、WebClient は内部的に AsyncOperationManager を使用し、また WebClient 上で非同期メソッドが使用されている場合には FileWebRequest と FileWebResponse は非同期 I/O を使用します (WebClient にはこれらのメソッドの同期バージョンもあります)。
図 4 ファイル読み取りおよび書き込みで WebClient を使用する
public static void ReadAllBytesAsync(
    string path, Action<byte[]> success, Action<Exception> failure)
{
    var wc = new WebClient();
    wc.DownloadDataCompleted += (sender, e) =>
    {
        if (e.Error != null) failure(e.Error);
        else success(e.Result);
    };
    wc.DownloadDataAsync(new Uri("file://" + path));
}

public static void ReadAllTextAsync(
    string path, Action<string> success, Action<Exception> failure)
{
    var wc = new WebClient();
    wc.DownloadStringCompleted += (sender, e) =>
    {
        if (e.Error != null) failure(e.Error);
        else success(e.Result);
    };
    wc.Download﻿StringAsync(new Uri("file://" + path));
}

public static void WriteAllBytesAsync(
    string path, byte [] bytes,
    Action success, Action<Exception> failure)
{
    var wc = new WebClient();
    wc.UploadDataCompleted += (sender, e) =>
    {
        if (e.Error != null) failure(e.Error);
        else success();
    };
    wc.UploadDataAsync(new Uri("file://" + path), bytes);
}

public static void WriteAllTextAsync(
    string path, string contents,
    Action success, Action<Exception> failure)
{
    var wc = new WebClient();
    wc.UploadStringCompleted += (sender, e) =>
    {
        if (e.Error != null) failure(e.Error);
        else success();
    };
    wc.UploadStringAsync(new Uri("file://" + path), contents);
}
さらにすばらしいことに、これを上回る便利な機能が WebClient に用意されています。具体的には、進行状況追跡の組み込みサポートです。この機能には、関連したイベントが 2 つ用意されています。
public event
    DownloadProgressChangedEventHandler DownloadProgressChanged;
public event
    UploadProgressChangedEventHandler UploadProgressChanged;
それぞれ、処理中のダウンロードやアップロードの進行状況が更新されたときに発生します。そしてこれらのイベント用ハンドラに渡されるイベントの引数は、進行状況のレポートに有用な情報を提供します。たとえば DownloadProgressChangedEventArgs という型があります。
public class DownloadProgressChangedEventArgs :
    ProgressChangedEventArgs
{
    public long BytesReceived { get; }
    public long TotalBytesToReceive { get; }

    /* from the base ProgressChangedEventArgs type
    public int ProgressPercentage { get; }
    public object UserState { get; }
    */
}
これにより、GUI アプリケーションに非同期 I/O をうまく統合することができます。処理の前に大きなファイルをディスクからメモリに読み込む必要がある Windows フォーム アプリケーションについて考えてみましょう。ファイルを非同期に読み込み、進行状況バーを更新する進行状況通知と完了通知を受け取ることができます。
private void button1_Click(object sender, EventArgs e) {
    WebClient wc = new WebClient();
    wc.DownloadDataCompleted += (s, de) => {
        _fileData = de.Result;
        MessageBox.Show("File loaded");
    };
    wc.DownloadProgressChanged +=
        (s, de) => progressBar1.Value = de.ProgressPercentage;
    wc.DownloadDataAsync(new Uri(@"file://c:\largeFile.dat"));
}
private byte [] _fileData;
残念ながら、この API はたいへん優れているのですが、きわめて重大な問題があります。FileWebRequest と FileWebResponse は確かに非同期 I/O を内部的に使用しているのですが、この実装も何らかの理由で、コールバック通知においてワーカーの ThreadPool から影響を受けます。非同期 I/O の完了を待っているプールによって、それらのスレッドがブロックされることもあります (これがコードの不具合なのか、単なるパフォーマンスの問題なのかは、議論の余地があります)。いずれにせよ、この WebClient のテクニックを使用して非同期かつ同時実行で多数のファイルの読み取りや書き込みを行うと、ThreadPool のスレッド数によってパフォーマンスが著しく低下するのがわかります。
ThreadPool 内のスレッドをブロックするのは、一般的には良いアイデアではありません。新しいスレッドの挿入を ThreadPool に強いることになり、不必要にリソースを消費するだけでなく、遅延によって ThreadPool が挿入を遅らせるためにアプリケーションの速度が低下します。結局のところ、非同期のファイル読み取りおよび書き込みに対する WebClient のアプローチは、数ファイルを同時に扱う場合は適していますが、この問題を解決するために FileWebRequest や FileWebResponse が修正されるまでは、このコラムの前半で示した自作のコードを使用した方が良いと言えます。もう 1 つの解決策は、正しく処理を行う WebRequest と WebResponse の独自の実装を用意して、それらを WebClient に登録することです。

ご意見やご質問は、Stephen (netqa@microsoft.com) まで英語でお送りください。

Stephen Toub は、マイクロソフトの並列コンピューティング プラットフォーム チームのシニア プログラム マネージャ リードです。また、MSDN Magazine の寄稿編集者でもあります。


{ End Bracket }
Windows Home Server をカスタマイズする
Chris Gray

ここ数年間、筆者は Windows® Home Server の開発に取り組んできました。Windows Home Server は、簡単なユーザー インターフェイスを使用してすべてのホーム コンピュータを支援するアプリケーションです。Windows Home Server の背景にある考え方は、家族全員が画像、ビデオ、および音楽を 1 か所の安全な場所に格納して、ファイルを簡単に検索できるようにすることです。このサーバーは、もちろんそのまま使用しても実に便利なものですが、筆者が最もワクワクしたのは、自宅での生活をもっと楽しく便利にするカスタム アプリケーションの開発でした。
筆者は次のように確信しています。つまり、いつかはほとんどの家庭がどこか邪魔にならない場所に隠されたコンピュータを持つようになり、自宅での生活がもう少し便利で直感的になると。このサーバーのネットワーキング機能、処理能力、およびストレージ容量を利用して、安価なデバイスや機器で他の機器と通信し、自宅での生活をもう少し快適にします。
筆者は数人の友人と過去数か月にわたって、Windows Home Server と自分たちのプログラミング スキルを利用してこれらの可能性を実験してきました。メディア、インターネット接続、および電力消費の 3 つが最も興味をそそられる分野でした。
私たちは、Windows Home Server にテレビと音楽を一元化し、安価で多種多様なネットワーク対応メディア デバイスでコンテンツを表示しました。さいわいにも、UPNP、SMB、または HTTP をサポートしているか、なんらかのソフトウェアをインストールするだけでサーバー上で使用できるあらゆる種類のすばらしいデバイスが見つかり、録画したテレビ番組を夜間にサーバーに自動的にコピーするメディア センター用のカスタム ソフトウェアを作成しました。
サーバー上では、サーバーの余った CPU サイクルを使用して、ステレオ コンポーネントのようなビデオおよびオーディオ デバイスへのストリーミングに適した形式にコンテンツを変換するコードを作成しました。このような方法で一元化することにより、ステレオやテレビ用に家庭内を再配線しなくても、ニーズに合ったネットワーク対応デバイスを選択できます。簡単に壁を通過できる通常のワイヤレス ルーターを使用するだけで済みます。
あらゆる種類のメディアを 1 台のコンピュータに配置し、インターネットを通じてアクセスすることは、無限の創造性の源でした。このソリューションの作成は、さほど難しくありませんでした。サーバーに付属のドメイン名を使用してサーバーの検索を簡単にし、特定のコンテンツ部分を検索する機能を備えた Web サイトを構築しただけです。
このプロジェクトで、Web サービスを使用してインターネット対応の携帯電話と通話することを思いつきました。まだ何も作成はしていませんが、サーバーのストレージと処理能力を電話にリンクするのはきっと楽しいでしょう。
最近思いついた別のアイデアは、照明と電源コンセントを Windows Home Server と統合して、自宅の総消費電力を削減することです。このアイデアは新しいものではありませんが (ホーム オートメーションは何年も前からあります)、筆者が取り組むのは今回が初めてです。全体的なアイデアとして、コンピュータで電源スイッチを制御し、TV、照明、サーモスタット、またはその他の機器を外出先から消せるようにします。
複数の企業がコンピュータ制御の優れた照明スイッチ、電源コンセント、および調光器を製造しています。ただし、ホーム オートメーションに対する大きな障害の 1 つは、総費用でした。これらのコンピュータ制御スイッチのコストは標準のスイッチよりも高く、通常は専用コンピュータが必要です。しかし、既に Windows Home Server を購入している場合は、付属の Windows ベースのアプリケーションをインストールするだけで済みます。
将来的には、Windows Home Server を接続し、いくつかの照明スイッチを追加し、すべてをサーバーから制御 (さらにはリモートで制御) するようになると想像できます。さらに、家族の消費電力を評価するために、一定期間にわたる家庭全体の総消費電力をグラフ化できます。
Web の機能を利用してリアルタイムの天気予報アプリケーションにリンクし、たとえば雨が降りそうな場合は芝生のスプリンクラーをオンにしないようにすることもできます。同様に、日没と日の出の時刻を毎日ダウンロードして、屋外照明を日没時に点灯し、夜明けに消灯することもできます。さらに、Exchange 予定表に休暇旅行中と示されていたら、暖房が低い設定温度に保たれます。
開発者の観点から見ると、Windows Home Server での実験によって興味深い課題が提供され、即座に効果を確認できるのはすばらしいことです。筆者が楽しみながら作成したアプリケーションは、自宅から離れているときでも便利でアクセスしやすい方法で機器をリンクします。その他の可能性については、msdn.microsoft.com/library/bb425866 を参照してください。

Chis Gray は、マイクロソフトの Windows Home Server の開発リーダーです。余暇には妻の Melinda とハイキングや家具作りを楽しんでいます。