: MSDN Magazine March 2007

March 2007
MARCH 2007 ボリューム 21 番号 3

• IIS 7.0: Windows Vista 用 Web サーバーの新機能
Mike Volodarsky

• .NET セキュリティ: .NET Framework 2.0 を使ってアプリケーションで証明書をサポートする
Dominick Baier

• デジタル メディア: WPF を使ってコントロールと 3D の表面にビデオを追加する
Lee Brimelow

• WiX の秘訣: MSBuild と Windows Installer XML を使ったリリースの自動化
Sayed Ibrahim Hashimi

• ASP.NET 2.0: カスタム プロファイル プロバイダを使用した Web ユーザーの管理
Jason N. Gaylord

Code Download (695 KB) HTMLHelp Format
コラム

• CLR 徹底解剖: .NET アプリケーションの拡張性、第 2 部
Jack Gudenkauf and Jesse Kaplan

• テストの実行: カスタム変換ストリームをテストする
Dr. James McCaffrey

• Cutting Edge: ASP.NET クエリ文字列を検証する
Dino Esposito

• Wicked Code: ASP.NET の非同期プログラミングを使ったスケール変換可能なアプリケーション
Jeff Prosise

• サービス ステーション: BizTalk Server 2006 の Web サービス
Aaron Skonnard

• Bugslayer: こちら GUI コントロール、トム少佐応答せよ
John Robbins

• 基礎: Windows Workflow Foundation の追跡サービス
Matt Milner

• 同時実行の問題: CLR の非同期プログラミング モデルを実装する
Jeffrey Richter

• {End Bracket}: Virtual Earth 3D コントロールを開発する
Duncan Lawler

各出版物

• 編集者のメモ: 不正利得に物申す
Joshua Trupin

• ツールボックス: デスクトップの共有、簡易コード編集
Scott Mitchell


IIS 7.0: Windows Vista 用 Web サーバーの新機能

IIS 7.0

Windows Vista 用 Web サーバーの新機能

Mike Volodarsky
この記事の内容 : :
  • モジュール化 Web サーバーの機能
  • 簡素化された配置と構成
  • 拡張性と ASP.NET の統合
  • セキュリティ、パフォーマンス、および互換性の向上
この記事は次のテクノロジを使用しています:
Windows Vista、IIS
コンテンツ
モジュール化 Web サーバー
簡素化された配置と構成
管理の改善
.NET Framework とスクリプティング
Web サーバー機能の作成
ASP.NET の統合
セキュリティの改善
診断の改善
デバッグなしのエラー診断
パフォーマンスの改善
下位互換性
まとめ

新しい IIS 7.0 Web サーバーは、ここ数年のマイクロソフト製品で最も重要な開発の 1 つであるとマイクロソフトの内外両方の人々が語っているのを耳にします。Windows Vista™ をはじめとして、最近はマイクロソフトから多くの印象的な製品が発表されていることを考慮すれば、これは十分に注目できる指摘です。

IIS 7.0 のリリースは、Windows NT® 4.0 の IIS の最初のバージョンのリリースから 10 年の節目に当たります。2001 年には、その 4 バージョン後の IIS 5.0 が、リリースの数か月後には悪名高い Code Red と Nimbda ワームの標的になりましたが、インターネットで最も普及している Web サーバーになりました。Windows Server® 2003 でリリースされた IIS 6.0 では、セキュリティ、信頼性、およびパフォーマンスの向上に集中してサーバーのコードが大幅に変更されました。それ以来、高い信頼性とセキュリティの成果を達成した IIS 6.0 は堅牢な Web サーバーとして認められ、リリースされてから重要なセキュリティ報告は一度だけで、それも遠隔地から悪用できるものではありませんでした。

この記事では、次世代 IIS 7.0 Web サーバーが開発者と管理者に大きな違いをもたらす主な理由を示し、多くの新機能をいち早く使用できるように説明します。

IIS 7.0 のビジョンは、IIS 6.0 のコードベースのスピード、信頼性、およびセキュリティを継承し、将来の Web アプリケーションを実行するための高い拡張性と管理性を備えた Web サーバーに進化させることです。その結果、IIS の歴史において最も多くのアーキテクチャの改善点を含む野心的な Microsoft Web サーバーになりました。

IIS 7.0 リリースのコアは完全にモジュール化された Web サーバーで、アプリケーション トポロジに必要なロールに対して最適化された小さなフットプリントの Web サーバーに統合される 40 以上の機能で構成されています。これらの機能は新しい拡張可能な層の上に構築されるので、開発者はネイティブ コードまたは Microsoft® .NET Framework を使用して、サーバーのあらゆる側面の拡張または置き換えができます。IIS 7.0 は、ランタイム、管理、および運用のすべての側面において拡張性を提供するので、個々のニーズに対してエンドツーエンドのソリューションを構築できます。IIS 7.0 は、コア プラットフォームの上位に位置し、サーバーの管理性と運用に関する多くの問題に取り組んでいます。IIS 7.0 は、完全に委任されたサイト管理を実現する新しい構成システムを採用し、最終的に Web アプリケーションの XCOPY 配置を実現します。新しい管理 API と診断機能によって、サーバーの配置、管理、およびトラブルシューティングが大いに簡略化され、これまでになく便利になりました。

ですがなぜ "Longhorn" というコードネームの Windows Server の次期バージョンが最終的にリリースされる前に、サーバー アプリケーションである IIS について考察する必要があるのでしょうか。Windows Server "Longhorn" に付属する予定のフル装備の IIS 7.0 と同じバージョンが Windows Vista に付属するので、この時点で IIS について考察することが重要です。つまり、新しい IIS 7.0 の機能をすぐに活用して個人用 Web サイトを構築し、Windows Vista でホストできます。さらに、Windows Server "Longhorn" が出荷されたときに配置する同じ IIS プラットフォームで実稼働 Web アプリケーションと Web サーバー インフラストラクチャをいち早く開発およびテストできます。

興味ありますか。それでは、詳しく説明します。

モジュール化 Web サーバー

IIS 7.0 では、Web サーバーを軽量のサーバー コアおよびこのコアにプラグインできる 40 以上の機能モジュールに分割します。これらのモジュールは、静的な Web コンテンツをダウンロードできるようにする StaticFileModule、統合された NTLM 認証をサポートする WindowsAuthModule などの必要な機能を個々に正確に提供できるように、サーバーに個別にインストールできます。

モジュールは、いつでもサーバーから完全にアンインストールするか (図 1 参照)、または必要としないアプリケーションに対して個別に無効にできます。これによって、サーバー管理者は攻撃対象領域を劇的に縮小し、最小のフットプリントのサーバーを迅速に配置し、必要なコードのみを実行することによってパフォーマンスを大きく向上できます。

図 1 必要な機能のみを使用する
図 1 必要な機能のみを使用する(画像を縮小するには、ここをクリックします)
図 1 必要な機能のみを使用する
図 1 必要な機能のみを使用する (画像を拡大するには、ここをクリックします)

コンポーネント化されたアーキテクチャは、セキュリティ リスクを軽減し、修正プログラムの必要性を最小化するための IIS 7.0 の重要な特性です。リバース プロキシとキャッシュ サーバー、HTTP プロトコル負荷分散、SSL とセキュリティ センチネル サーバーなど、アプリケーション トポロジの特定のサーバー ロールに対して最適化されたカスタム コンポーネントと選択した IIS の機能を組み合わせて専門化したサーバーを配置することもできます。

IIS 7.0 に含まれるすべてのサーバー機能は、新しいパブリック拡張性 API の上に構築されます。開発者は、既存のサーバー機能を独自の機能と交換したり、新しいモジュールを構築して IIS 7.0 の機能セットに追加できます。組み込み認証メカニズムをカスタム認証モジュールと交換したり、または新しい形式の応答の圧縮を提供することもできます。

新しい拡張性 API は、これまでの ISAPI 拡張性モデルを根本的に改善し、高い柔軟性と操作性によってサーバーを強化しています。コア サーバーから構成、管理、および診断にいたるまでのサーバーのあらゆる局面が拡張性を提供するので、各自のニーズに基づいてサーバーを拡張して調整できます。拡張性については、後で詳しく説明します。

簡素化された配置と構成

親しみをもってメタベースと呼ばれていたこれまでの IIS リリースの集中管理された構成ストアはなくなりました。IIS 7.0 には、分散 XML 構成ファイルの階層に基づいた新しい委任された構成システムがあります。この階層は、サーバー レベルの構成の既定値を含むグローバルの applicationHost.config ファイルとアプリケーションのディレクトリ構造に分散する web.config ファイルで構成されます。これらは、ASP.NET アプリケーション フレームワークがアプリケーションの設定を格納するために使用する web.config ファイルと同じです。これによって、クリーンで強固に構造化された XML ディレクティブを使用して、IIS と ASP.NET の構成を並行して保存できます。次に例を示します。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.web>
        <customErrors mode="Off" />
    </system.web>
    <system.webServer>
      <directoryBrowse enabled="true" />
    </system.webServer>
</configuration>

これまでは、アプリケーションが正しく動作するために、IIS アプリケーションの設定はマシン レベルのメタベース リポジトリに明示的に構成する必要がありました。一連の分散 web.config ファイルを使用すると、アプリケーションは各自のディレクトリ構造に必要なサーバー構成をカプセル化できます。これによって、配置が格段に簡略化されます。自己完結型アプリケーションはターゲット サーバーのアプリケーション ディレクトリにコピーするだけで、必要な設定に基づいて即座に実行できます。

新しい構成システムでもサーバー管理者は広範な制御機能を使用できるので、セキュリティまたは業務上の理由から他のアプリケーションを制御しながら、特定の構成オプションをアプリケーションに任せることができます。このようにして、サーバーにホストされているアプリケーションは、サーバー管理者に助けを求めたり、外部の構成パネルを使用する必要もなく、必須の構成を直接アプリケーションに設定できます。

IIS 7.0 では、構成システムは完全に拡張可能です。新しいモジュールでは独自に構成スキーマを追加できるので、アプリケーションは IIS と ASP.NET の構成と並行して独自の機能を構成できます。

<configuration>
    <system.webServer>
      <directoryBrowse enabled="true" />
    </system.webServer>
    <myBandwidthThrottler enabled="true" />
</configuration>

カスタム構成のセクションは IIS 7.0 の機能と同じ構成スキーマを使用するので、厳密に型指定された属性値、コレクション構文、階層的オーバーライド、およびロックのセマンティクスを活用できます。

IIS 7.0 は、ABO (Admin Base オブジェクト) API を使用して従来のメタベースに書き込む既存のセットアップ コードおよびより高いレベルの ADSI (Active Directory® Service Interfaces) と WMI (Windows Management Instrumentation) を使用して IIS を構成するスクリプトを引き続きサポートします。これは、他のすべての従来の構成 API の基になっている ABO API をエミュレートする互換性層を提供することによって実現しています。つまり、この種のスクリプトは以前の IIS のリリースと同様の方法で構成を読み込んで変更できます。メタベースの互換性の詳細については、この記事の後半にある「パフォーマンスの改善」と「後方互換性」を参照してください。構造化された XML 構成の新しい形式はテキスト エディタで簡単に編集できますが、管理者が簡単にサーバーを管理し、構成と配置の自動化を実現できるようにするために、IIS には多くの管理ツールと API が用意されています。

管理の改善

IIS 7.0 には、さまざまな状況でのサーバー管理を可能にするための豊富な機能があります。InetMgr.exe MMC スナップインの代わりになる新しいグラフィカル IIS Manager 管理ツールでは、タスクベースの管理インターフェイスを使用してサーバーを手動で簡単に管理できます (図 2 参照)。

図 2 IIS Manager のグラフィカル管理ツール
図 2 IIS Manager のグラフィカル管理ツール(画像を縮小するには、ここをクリックします)
図 2 IIS Manager のグラフィカル管理ツール
図 2 IIS Manager のグラフィカル管理ツール (画像を拡大するには、ここをクリックします)

IIS Manager を使用すれば、ほとんどの IIS 7.0 機能を管理し、サーバーの動作を監視できます。このツールはファイアウォール対応の HTTP/SSL 接続を介したリモート管理機能をサポートし、認証には Windows ベースの資格情報およびその他の資格情報の両方をサポートします。

さらに、このツールでは管理の委任もできるので、アプリケーションの所有者はサーバー コンピュータに管理者としてアクセスせずに各自のアプリケーションをリモートで管理できます。この機能を使用すれば、ホストされているサービスのユーザーは、自宅のデスクトップで管理ツールを実行して、リモートで接続してホストされているサーバーのアプリケーションを管理できます。サーバー管理者は、アプリケーションの所有者に委任されている管理機能を完全に制御できます。

最後に、管理ツールは完全に拡張可能で、構成システムの拡張性の上に構築されているので、カスタム管理 UI をツールに追加することもできます。IIS Manager ツールおよび管理プラグインを追加する方法の詳細については、iis.net/default.aspx?tabid=7&subtabid=73 を参照してください。

さらに柔軟なコマンドライン管理のために、IIS 7.0 には Appcmd.exe コマンドライン ツールがあります (図 3 参照)このツールには、包括的な管理機能が備わっており、UI より充実した一括操作がサポートされます。この強力なユーティリティによって、構成の読み取りと書き込みおよびサイトとアプリケーション プール状態情報へのアクセスが簡単になり、コマンド プロンプトから他のほとんどすべての管理タスクを実行できます。

図 3 IIS 7.0 の Appcmd.exe コマンドライン管理
図 3 IIS 7.0 の Appcmd.exe コマンドライン管理(画像を縮小するには、ここをクリックします)
図 3 IIS 7.0 の Appcmd.exe コマンドライン管理
図 3 IIS 7.0 の Appcmd.exe コマンドライン管理 (画像を拡大するには、ここをクリックします)

Appcmd.exe では、サイト、アプリケーション、アプリケーション プール、および仮想ディレクトリを作成して構成できます。サイトの起動と停止、アプリケーション プールの再利用、実行中のワーカー プロセスの一覧表示、実行中の要求の確認、FREB トレース ログの検索を実行できます。IIS と ASP.NET の構成データの検索、編集、エクスポート、およびインポートもできます。

このツールは、サポートされているサーバー オブジェクトの柔軟な検索ができるように作成されています。たとえば、特定の設定または停止しているアプリケーション プールがあるサイトを即座に検索できます。検索には、数値範囲の指定、簡単なワイルドカード文字列比較の使用を含めて、オブジェクトの任意のプロパティに対して条件をいくつでも使用できます。

Appcmd は Windows PowerShell™ にあるようなリンクされた操作もサポートし、単一のコマンド ラインから関連付けられているオブジェクト セットに対して複数の処理を実行できます。たとえば、単一のコマンドを使用して特定のサイトでアプリケーションをホストするすべてのアプリケーション プールを探して再利用できます。Appcmd を使用して IIS を管理する方法の詳細については、iis.net/default.aspx?tabid=2&subtabid=25&i=954&p=1 を参照してください。

.NET Framework とスクリプティング

IIS Manager または Appcmd.exe コマンドライン ツールによる手動のサーバー管理に加えて、IIS 7.0 では豊富なオプションを使用してプログラム的に管理することもできます。Microsoft.Web.Administration API を使用すれば、.NET アプリケーションからサーバーを管理できます。新しい COM API を使用して直接 IIS 構成システムを管理するか、または ASP、Windows® Script Host (WSH) などのスクリプティング環境からアクセスすることもできます。さらに、新しい WMI プロバイダおよびメタベース互換性層を介した従来の WMI と ADSI プロバイダのサポートもあります。

新しい .NET 管理 API である Microsoft.Web.Administration を使用すると、マネージ コードのアプリケーションはプログラムによって IIS サイトとアプリケーションを簡単に準備し、重要な状態および診断情報にアクセスし、別の方法でサーバーを構成できます。.NET Framework ベースのアプリケーションが簡単に IIS 構成情報と状態情報にアクセスできることにより、.NET ベースのセットアップおよび管理アプリケーションを記述し、さらには ASP.NET ページから直接管理タスクを実行する可能性も開かれました。

たとえば、図 4 に、Microsoft.Web.Administration を使用してコマンド ラインから Web サイトを新規作成する小さな C# プログラムを示します。

Figure 4 Microsoft.Web.Administration によるサイトの準備 (Close Figure 4)
using System;
using Microsoft.Web.Administration;

class CreateASite
{
    static void Main(string[] args)
    {
        ServerManager serverManager = new ServerManager();
        Site mySite = serverManager.Sites.Add(
            "MySite", "d:\\inetpub\\mysite", 8080);
        mySite.ServerAutoStart = true;
        serverManager.CommitChanges();
    }
}

Microsoft.Web.Administration を使用すれば、アプリケーションの内部から任意の .NET 準拠の言語を使用して IIS の操作タスクと構成タスクを簡単に実行できます。実行中のワーカー プロセスや要求などのサーバーに関するランタイム状態情報にも簡単にアクセスできます。

Microsoft.Web.Administration API は、IIS Manager ツール用のカスタム .NET サーバー モジュールと UI プラグインの内部でカスタム構成にアクセスするための基盤になります。Web サーバーおよび関連する構成と管理コンポーネントを強化するためのイメージの著作権ハンドラを含むエンドツーエンドのサーバー パッケージの例については、iis.net/default.aspx?tabid=2&subtabid=25&i=1076 を参照してください。

Windows Server "Longhorn" のタイムフレームでは、IIS チームはカスタム管理オブジェクトの追加または既存のオブジェクトの拡張のための拡張性モデルを作成しています。これによって、スクリプティング、Microsoft.Web.Administration API を含めた各種の管理機能を介してカスタム管理機能が自動的に公開されます。Windows Vista では管理オブジェクトの追加や拡張はできませんが、Microsoft.Web.Administration およびその他の API を使用して既存の IIS 構成セクションと同様にカスタム構成セクションにアクセスして操作できます。

Web サーバー機能の作成

IIS 7.0 ではサーバーを必要に応じて形成できるので、必要とする機能を確保するためにサーバーの任意の機能を追加または交換できます。この機能の中心は新規に導入された Web サーバー拡張性 API で、その上にすべての既存の IIS 7.0 HTTP 機能が構築されます。この API はパブリックなので、IIS 7.0 に付属するすべての機能を実装できます。これは IIS では初めてのことで、これまで限定されていた ISAPI 拡張性モデルに対する根本的な改善です。

新しい拡張性 API は、Web サーバーのオブジェクト モデルを定義し、IIS で要求処理サービスを提供するためのモジュールを有効にする C++ の一連の直感的なクラスです。これらのクラスは、Windows Vista SDK の \inc\httpserv.h ヘッダー ファイルで定義されます。

この API は ISAPI より強力で、はるかに使いやすくなっています。その理由は次のとおりです。新しい API はタイプセーフで、良好にカプセル化されたオブジェクト モデルです。新しいサーバー オブジェクト モデルではサーバーのすべての基本的なオブジェクトとタスクに専用のインターフェイスが提供されるので、開発がはるかに簡単になります。次のようになります。

  • IHttpRequest クラスを使用して要求を検証する
  • IHttpResponse クラスを使用して応答を操作する
  • IHttpServer クラスの便利なユーティリティ機能を使用する
  • IHttpUser クラスによる認証を提供する
  • 構成 API を使用してモジュールのカスタム構成セクションにアクセスする

以上のクラスは、これまでよりはるかに多くのサーバーの機能セットを公開しますが (IIS に付属するすべての機能を構築するために必要な機能より多い)、緩く型指定された ISAPI インターフェイスよりはるかに使いやすくなっています。

開発者は、メモリと状態管理のパターンの改善の恩恵も受けることができます。IIS 7.0 サーバーのほとんどの API は、サーバーが返すデータに対してサーバー管理のメモリを使用するので、ISAPI と既存のほとんどの Win32® API のようにバッファを割り当てて管理する必要がありません。この部分は、これまでの ISAPI 開発において最もエラーが発生する面倒な領域の 1 つでした。新しい API では、応答バッファリング、認証、クライアント用の応答データの準備などの多くの複雑な要求処理タスクが簡素化されています。数か月前に、私は自身のブログに新しいプログラミング モデルの重要な改善点とパターンに関する一連の投稿を行いました。C++ で IIS 用の開発を考えている方は、mvolo.com/blogs/serverside/archive/2006/10/07/10-reasons-why-server-development-is-better-with-IIS7.aspx を参照してください。

サーバーの拡張のために、IIS 7.0 には完全に統合された .NET Framework API もあります。さらに、この API は、Windows 2000 の ASP.NET 1.0 以来 ASP.NET モジュールとハンドラを構築するために使用されている API と同じです。ただし、使い慣れた ASP.NET モデルで既存の ASP.NET モジュールとハンドラを IIS 7.0 サーバーで動作するように維持しているのは、これまでの古いテクノロジとはまったく異なるテクノロジです。

IIS 7.0 の ASP.NET には、Classic モードと Integrated モードの 2 つのバージョンがあります。Classic モードでは、IIS の以前のバージョンとまったく同様に動作します。Integrated モードは新しいプラットフォームの既定のモードで、新しいエンジンを使用して IIS Web サーバーとの比類のない統合を実現します。Integrated モードでは、ASP.NET API は Web サーバーに直接統合される IIS 7.0 モジュールを開発するために使用でき、基になる C++ API で使用できるほとんどすべてのサービスを提供できます。

このモードは、使い慣れたインターフェイス、メンバシップとロール管理のような .NET Framework と ASP.NET 2.0 の便利なアプリケーション サービス、および以前は C ベースの ISAPI コンポーネントのみで使用できたサーバー拡張の実力といったそれぞれの良いところを活用しています。

従来の多くの ASP.NET モジュールは、Integrated モードの特定の利点に基づいて新しい ASP.NET モジュールを記述できるだけでなく、web.config ファイルの構成オプションをいくつか変更するだけではるかに強力になります。

ASP.NET の統合

IIS 7.0 では、ASP.NET 2.0 は動的アプリケーションを構築するための優れたフレームワークになるだけではありません。ASP.NET コンポーネントは、IIS Web サーバーを拡張するためのプラットフォームになり、IIS 要求処理パイプラインのフルメンバーになります。ASP.NET コンポーネントの動作は次のとおりです。

IIS 6.0 までの ASP.NET はスタンドアロン アプリケーション フレームワークとして Web サーバーに接続していました。ASP.NET は、.aspx およびその他の要求に登録されている拡張を処理し、そのような要求に対して、フォーム認証、応答出力キャッシュ、およびカスタム ASP.NET モジュールによるサービスを含むその他の強力な機能を提供しました。そのために、ASP.NET に登録されているコンテント タイプのみがこのサービスを受けることができました。ASP ページ、PHP ページ、イメージ、CGI アプリケーションなどのその他のタイプはサービスを受けることができませんでした。さらに、ASP.NET リソースに対しても、特定の Web サーバー機能は、ランタイムの制限によって、ASP.NET で使用できませんでした。たとえば、送出 HTTP 応答ヘッダーのセットを検証し、クライアントに送信される前に変更することはできませんでした。

IIS 7.0 の Integrated モードでは、ASP.NET モジュールは統一された要求処理パイプラインで、ネイティブの C++ IIS モジュールとサイドバイサイドで実行されます (図 5 参照)。つまり、出力キャッシュ、URL リライト、およびその他のカスタム ASP.NET モジュールが提供する機能などの既存の ASP.NET サービスは、任意のコンテント タイプに適用できます。ランタイムの統合の改善により、ASP.NET モジュールはこれまで使用できなかったサーバー機能にもアクセスできるので、ほとんどの場合、ネイティブの IIS 拡張機能を記述する必要がなくなります。

図 5 IIS 6.0 と IIS 7.0 における ASP.NET の統合
図 5 IIS 6.0 と IIS 7.0 における ASP.NET の統合(画像を縮小するには、ここをクリックします)
図 5 IIS 6.0 と IIS 7.0 における ASP.NET の統合
図 5 IIS 6.0 と IIS 7.0 における ASP.NET の統合 (画像を拡大するには、ここをクリックします)

最後に、Integrated モードでは、IIS とのより緊密な統合によって使用できるようになった追加機能を公開する新しい API をいくつか提供します。これには、応答の生成元に関わらず、すべての応答ヘッダーを確認する機能、別の URL への要求の実行を完全に書き直す機能などがあります。

多くの場合、既存のアプリケーションは Integrated モードを活用するために Integrated モード固有の機能を使用する新しい ASP.NET モジュールを必要としません。アプリケーションは構成を変更するだけで ASP.NET のフォーム認証、URL 認証などの機能とユーザー セキュリティ情報を使用して Web サイト全体を保護し、ASP.NET URL マッピングを使用して、アプリケーションの URL を書き直すことができます。Intergrated モードを使用して Web リーチがサイトのイメージにホット リンクすることを防止する例については、mvolo.com/2006/11/10/stopping-hotlinking-with-iis-and-aspnet.aspx の ASP.NET サンプル モジュールを参照してください。これは、Integrated モードで既存のサードパーティの ASP.NET モジュールを使用することによって、さらに多くの処理を行う良い例です。

既存のアプリケーションに Integrated モードを使用する際の詳しいチュートリアルについては、iis.net/default.aspx?tabid=2&subtabid=25&i=1081&p=1 の記事を参照してください。

セキュリティの改善

IIS 7.0 は、安全設計が既定で保証され、慎重にコーディングされたセキュリティ追跡レコードを持っている IIS 6.0 のコードベースの上に構築されています。IIS 7.0 では、さらにセキュリティを強化し、安全な Web アプリケーションの構築のための多くの機能を提供するために、構造上の変更を行っています。

攻撃対象領域を減らすことは、安全なシステムの設計と配置の基本原則の 1 つです。IIS 6.0 の既定でロックダウンする方法を次のレベルに受け継ぐことにより、IIS 7.0 では既定でインストールされる機能を減らし、サーバーのセキュリティをさらに高めています。サーバーのモジュール化の利点をさらに活用して、使用しない機能を削除し、サーバーの攻撃対象領域を最小限にすることにより、サーバーが攻撃の危険にさらされる可能性がかなり軽減されます。

サーバーで使用されていないコンポーネントに脆弱性が見つかった場合にも、悪用を予防するためにサーバーをローテーションから外す必要はなく、脆弱なコンポーネントに修正プログラムを即座に適用する必要もありません。これによって、アプリケーションの可用性を高め、修正プログラムの管理コストを減らすことができます。

コア セキュリティの改善に加えて、IIS 7.0 にはサーバーをさらにロックダウンし、アプリケーションをサーバーに安全に配置するための一連のセキュリティ機能があります。IIS では、認証によってアプリケーションのコンテンツを常に強力に保護してきました。ASP.NET の Integrated モードでは、フォーム認証、メンバシップ、ログイン コントロールなどの一般的な ASP.NET セキュリティ機能を使用して、アプリケーション全体に完全な認証とアクセス制御ソリューションを提供できます。通常の場合、この作業はコードをまったく記述せずに数分で完了できます。

ASP.NET URL 認証機能によって強化された新しい URL 認証機能は、アプリケーション全体の宣言的なアクセス制御規則を構成するために使用できます。アクセス ルールは、ユーザー名とロールに基づいたアプリケーション内の URL へのアクセスの許可または拒否に使用できます。URL 認証は、ASP.NET 2.0 のメンバシップとロール管理の機能とシームレスに統合され、ASP.NET フォーム認証、ログイン コントロールと共に効率的に使用して、アプリケーション ユーザーのセキュリティを迅速に有効にできます。

新しい要求フィルタ機能は、強力なロックダウン機能を提供します。その一部は一般的な URLScan ツールで使用できました。要求フィルタ機能を使用すれば、疑わしいデータを含む要求を拒絶し、重要なリソースを保護し、厳しい要求制限を適用することによってサイトをさらにロックダウンできます。

IIS 7.0 では、セキュリティ設定の配置と管理をさらに簡単にするための一連の変更も行いました。IIS_IUSR 匿名アカウントが新たに組み込まれました。このアカウントは、パスワードの期限切れがなく、マシン間でパスワードの同期の必要もありません。IIS_WPG グループの代わりに導入された新しい IIS_IUSRS グループは、実行時にワーカー プロセスの ID に自動的に挿入されるので、カスタム アカウントを使用する際にワーカー プロセスの ID をグループに手動で追加する必要はありません。

IIS_USR アカウントと IIS_USRS グループが組み込まれているので、 IIS の匿名アカウントとグループにアクセス制御リスト (ACL) を指定するアプリケーションのコンテンツは、セキュリティ設定を保存する手順をさらに行うことなく IIS サーバー間で単純にコピーできます。これによって、開発、テスト、実稼働というサイクル全体のアプリケーションの配置が大いに簡素化されます。

前述の分散構成システムによって、アプリケーションの所有者は、管理者としてサーバーにアクセスせずにアプリケーションから直接必要な Web サーバーの設定を管理できます。アプリケーション管理者は、アプリケーションをサーバーにアップロードする際にアプリケーションのコンテンツの内部で web.config ファイルに必要な構成を指定するか、または IIS Manager ツールを使用してアプリケーションをリモートで構成します。

IIS Manager ツールには、ファイアウォール対応の HTTPS 接続を使用する安全なリモート管理機能があります。この管理ツールでは、メンバシップ サービスを使用して Windows ユーザーまたはカスタム ユーザー アカウントのいずれかとしてアプリケーション管理者を認証できるので、Windows アクセスを持たないアプリケーションの所有者がリモートでアプリケーションを管理できます。

サーバー管理者は、構成システムの柔軟なロック サポートを使用してアプリケーションで構成できる設定を完全に制御できます。同様に、アプリケーション管理者がアプリケーションをリモートで管理するために使用できる IIS Manager ツール機能も制御できます。

診断の改善

Windows、IIS 7.0、および各自の Web アプリケーションでサポートされるすべての新機能の中で、Web サーバーはトラブルシューティングに多くの労力を必要とする複雑なシステムです。IIS 7.0 では、サーバーの動作を監視し、アプリケーションの問題をデバッグするための多くの新機能が導入されています。

まず、IIS 7.0 ではサーバーのリアルタイムの状態を監視できます。この RSCA (Runtime State and Control API) という機能では (「リースカ」と発音)、サイトとアプリケーション プールおよび実行中のワーカー プロセスのアクティブな状態を公開し、さらにサーバーで実行されている要求も表示できます。サイトの起動と停止などのサーバーの状態を制御でき、アプリケーション プールも再利用できます。Windows Vista では、Appcmd.exe コマンドライン ツールを使用するか、または Microsoft.Web.Administration API をプログラムで使用して、IIS Manager でこの情報にアクセスできます。

たとえば、実行中の要求およびそれがスタックされているサーバーのステージを表示できます。これによって、要求のハングの問題を迅速に解決し、CPU を消費しているスクリプトを追跡できます (図 6 参照)。

RSCA 機能は、サーバーの問題の調査およびサーバーのパフォーマンスの調整を行う際に非常に便利です。この機能を使用すると、システムで起こっている内容をすばやく把握し、トラブルシューティングの際にサーバーを制御できます。オフィスでバグを調査するときは、多くの場合、Appcmd.exe を使用してアプリケーション プールの状態を表示し、ワーカー プロセスを調査し、違反しているアプリケーション プールを起動または停止して、すばやく問題を特定します。

図 6 IIS Manager でスタックしているスクリプトを追跡する
図 6 IIS Manager でスタックしているスクリプトを追跡する(画像を縮小するには、ここをクリックします)
図 6 IIS Manager でスタックしているスクリプトを追跡する
図 6 IIS Manager でスタックしているスクリプトを追跡する (画像を拡大するには、ここをクリックします)

Web アプリケーションでエラーが発生した場合は、不正なサーバー構成、アプリケーション エラー、またはその他の環境の要因が考えられます。標準エラー メッセージとステータス コードが提供できる手がかりは非常に少ないので、サーバーのトラブルシューティング作業が難航します。IIS 7.0 はほとんどのエラーに対して詳細なエラー情報を提示し、不正がある場所とその理由、およびその解決方法を正確に示します (図 7 参照)。

図 7 詳細なエラー情報によって問題およびその解決方法を提示する
図 7 詳細なエラー情報によって問題およびその解決方法を提示する(画像を縮小するには、ここをクリックします)
図 7 詳細なエラー情報によって問題およびその解決方法を提示する
図 7 詳細なエラー情報によって問題およびその解決方法を提示する (画像を拡大するには、ここをクリックします)

ASP.NET の詳細なエラー情報と同様の詳細なエラーが、セキュリティ スキームの後に続きます。既定では、ローカル マシンから Web サイトをブラウズする場合のみ詳細情報を取得できます。これまでと同様に、エラー コードに対してカスタムのエラー ページを構成したり、カスタム URL にリダイレクトできます。詳細なエラー ページはローカライズされているので、対応する言語の言語パックがインストールされていれば、クライアントが指定した言語でエラーの説明が表示されます。

デバッグなしのエラー診断

発生したエラーが未知の場合、または複数の Web サーバー コンポーネントによる複雑な交錯によってエラーが発生している場合はどうなりますか。心配はありません。IIS 7.0 には、各要求に対して詳細な記録を生成する包括的なトレース メカニズムがあるので、問題を迅速に特定できます。

IIS 7.0 では、Windows Server 2003 SP1 (Service Pack 1) で IIS 6.0 に追加された ETW (Event Tracing for Windows) の他にさらに情報イベントが追加されています。このようなイベントには、サーバー処理の各ステージに関する有用な情報が含まれているので、これを確認すれば、要求の実行をバックトレースして障害が発生した場所を正確に特定できます。イベントは Windows トレース インフラストラクチャにルーティングできるので、ASP.NET と SQL Server™ を含む複数の Windows コンポーネントは、トレース情報を要求の単一の論理的実行トレースにリンクできます。

イベントは、トレース ログを XML ログ ファイルに保存する新しい FREB 機能にルーティングするか、またはプログラムで使用できます。XML ログ ファイルは、付属する XSLT スタイルシートで表示できます (図 8 参照)。

図 8 XML ログ ファイルの表示
図 8 XML ログ ファイルの表示(画像を縮小するには、ここをクリックします)
図 8 XML ログ ファイルの表示
図 8 XML ログ ファイルの表示 (画像を拡大するには、ここをクリックします)

FREB の優れたところは、サーバーで有効にできることです。これによって、正常に完了した要求のトレース ログを保存する際のパフォーマンスの低下を回避しながら、構成可能な障害条件に遭遇した要求のトレース ログを自動的に取得できます。たとえば、サーバー エラーの原因になっているか、または指定した時間内に完了できない要求に対してこの機能をオンにできます。

FREB を使用すると、断続的なエラーまたは再生が困難なエラーが発生した場合にも必ず貴重なトレース情報を得ることができます。これによって、これまでハードコア デバッグを必要としていた困難な問題を診断して修正できます。

基になるトレース インフラストラクチャは、サーバー拡張性モデルを介して IIS モジュールに公開されるので、IIS に付属するか、またはサードパーティによって開発されたすべてのサーバー コンポーネントは、要求の処理中に詳細なトレース情報を生成できます。IIS 7.0 のトレース機能は、System.Diagnostics API と ASP.NET ページ トレースを使用して ASP.NET のトレース機能に統合されるので、マネージ モジュールは統一されたトレース モデルを使用できます。さらに、トレース情報を処理して出力する新しい方法を提供する独自のトレース モジュールを記述することもできます。たとえば、IIS トレース情報を SQL Server またはテキスト ファイルに保存するモジュールを業界で初めて記述することもできます。

パフォーマンスの改善

Windows Vista はクライアント オペレーティング システムで、高いスループットを必要とする実稼働の配置には適していませんが (Windows Vista で IIS を実行すると同時発生要求が 10 までに制限)、Web アプリケーションのパフォーマンスの向上において、既に主要な構造上の改善を実現しています。Windows Server "Longhorn" のタイムフレームで行っている大規模なパフォーマンスの改善と組み合わせることにより、IIS 7.0 はサーバーのパフォーマンスを向上するでしょう。

最初の改善点は、もちろんコンポーネント化です。サーバーのモジュール化によって、管理者は不要な機能を削除できるので、要求の処理におけるメモリと CPU の使用を軽減できます。これによって、マシンのスループットと使用容量を大いに改善できます。機能は細かく有効にできるので (各機能はサーバーの各アプリケーションに対してオン/オフできる)、アプリケーションはサイトの特定の部分で特定の機能を有効にすることにより、さらにパフォーマンスを高めることができます。

IIS 7.0 のもう 1 つの注目すべきパフォーマンスの改善点は、新しい IIS 出力キャッシュです。この機能によって、負荷の高いサーバーの動的ページの応答を再利用できるので、クライアントに応答を返すために負荷の高いレンダリングおよびデータベース トランザクションを実行する必要が減少します。IIS 出力キャッシュは ASP.NET の既存のリッチな出力キャッシュのより高速な代替機能で、サポートするキャッシュ機能は少ないですが、動的コンテンツを高いパフォーマンスで柔軟にキャッシュできます。

ASP.NET ページ、PHP スクリプト、または CGI アプリケーションで動的コンテンツの出力をキャッシュすることにより、ディスクとデータベースの負荷を大いに軽減しながら 5 ～ 10 倍のパフォーマンスが得られることも少なくありません。

下位互換性

IIS 7.0 では、既存のほとんどのアプリケーションを変更せずに実行できます。IIS 7.0 の技術革新をサポートするために必要なアーキテクチャの変更が広い範囲に及んでいることを考慮すれば、このことは大きな成果です。構成システムには最も多くの変更が行われ、緩く型指定された集中管理構成ストアから委任された XML 構成ファイル階層に移行しました。構成情報の構造と記憶域の両方が IIS 6.0 メタベースと完全に異なり、従来の構成 API を使用してアクセスできなくなりました。

IIS 7.0 では、構成システムの基のデータとメタベース ABO API が公開するインターフェイス間を変換するエミュレーション層を使用してこの問題を解決します。これによって、ABO または上位レベルの WMI または ADSI スクリプトを使用してアクセスする場合に、メタベース用に記述されているコードを正しく動作させることができます。ただし、この機能を使用するには、互換性セットアップ コンポーネントをインストールしておく必要があります。

IIS 7.0 には IIS コンポーネントの開発のための新しい拡張性モデルがありますが、ISAPI コンポーネントもサポートされています。ISAPI 拡張と ISAPI フィルタ セットアップ コンポーネントをインストールすると、以前と同様に拡張機能とフィルタを実行できます。ただし、新しいコンポーネントを開発する場合は、はるかに強力で優れた開発能力を備えた新しい拡張性モデルを使用してください。

Integrated モードにおける実行時の互換性がない少数の ASP.NET アプリケーションは、Classic モードで実行されるアプリケーション プールに移動する必要があります。その場合、個別のアプリケーション プールに配置することによって、両方のモードで複数のアプリケーションを同じサーバーでサイドバイサイド実行できます。IIS 7.0 の ASP.NET の変更内容および一般的な互換性情報の一覧については、iis.net/default.aspx?tabid=2&subtabid=25&i=1223 の ASP.NET 互換性ホワイト ペーパーを参照してください。

まとめ

Windows Vista の IIS 7.0 は、Web サーバーのための正しいコア アーキテクチャ、拡張性、および管理プラットフォームに焦点を合わせ、次世代 Web アプリケーション プラットフォームに対する最良のアーキテクチャ基盤の提供を目指しています。Windows Vista によって、Windows Vista のサーバー バージョンと同じサーバー プラットフォームでアプリケーションを開発およびテストできます。開発されたアプリケーションは、Windows Vista のサーバー バージョンが公開されたときにそのまま配置できます。

IIS 7.0 が Windows Vista に付属するので、Web プラットフォームとツール チームは Web サーバーを実稼働環境で使用できるように準備し、実稼働シナリオの安定性とパフォーマンスを改善することに焦点を合わせることができます。ただし、Windows Vista と共に出荷されるコアの開発機能と管理機能は以前のままです。IIS 7.0 のサーバー バージョンが完成した時点で、改善内容がサービス パックの形式で Windows Vista に提供される予定です。その時点では、クライアント マシンとサーバー マシンで正確に同じバージョンの IIS が実行されるので、Window Vista を実行するデスクトップで Web アプリケーションを継続して開発およびテストできるようになります。

IIS 7.0 で作業を開始する場合は、是非、Web で公開されている多数の有用なリソースを、IIS チームの新しいホームページである iis.net で確認してください。この新しいサイトには、IIS 7.0 のすべての機能の詳細記事およびチュートリアルが含まれるポータルがあります。フォーラムを使用して質問し、IIS チームおよび IIS のコミュニティと問題について議論してください。

また、私のブログ (www.mvolo.com) にも IIS 7.0 に関する詳細情報および内部情報があるので、是非、お立ち寄りください。 そして、皆さんが興味を持っている IIS 7.0 のトピックをお知らせください。そうすれば、私のブログで取り上げたいと思います。

Mike Volodarskyは、Microsoft の Web プラットフォームとツール チームのテクニカル プログラム マネージャです。Mike は、ASP.NET および IIS のコア サーバー インフラストラクチャを管理しています。Mike は、次世代 Web サーバーである IIS 7.0 における Web アプリケーション プラットフォームの改善を担当しています。

© 2007 Microsoft Corporation and CMP Media, LLC. All rights reserved; 許可なしに一部または全体を複製することは禁止されています.


.NET セキュリティ: .NET Framework 2.0 を使ってアプリケーションで証明書をサポートする

.NET セキュリティ

.NET Framework 2.0 を使ってアプリケーションで証明書をサポートする

Dominick Baier
この記事の内容 : :
  • Windows 証明書ストア
  • .NET の証明書クラス
  • 検証、SSL、Web サービス、およびコード署名
  • データの署名と暗号化
この記事は次のテクノロジを使用しています:
.NET Framework 2.0
コードのダウンロード : : Certificates2007_03.exe (160KB)
コンテンツ
証明書の取得方法
Windows 証明書ストア
証明書を操作する
証明書にアクセスする
証明書の詳細の表示と証明書選択ツール
証明書を検証する
SSL サポート
Web サービス セキュリティ
セキュリティ ポリシーとコード署名
ClickOnce マニフェスト
データの署名と暗号化
データの暗号化解除と署名の確認
まとめ

通信のセキュリティ確保から、コード署名やセキュリティ ポリシーに至るまで、Microsoft® .NET Framework 全体の多くの個所で証明書が使用されます。

.NET Framework 2.0 では証明書のサポートが改訂され、証明書を使って標準に準拠した暗号化操作を行うためにまったく新しい名前空間が追加されました。この記事では、証明書と Windows® 証明書ストアの背景情報について説明します。また、証明書 API のしくみと、セキュリティ機能を実装するために Framework が証明書 API を使用する方法についても説明します。

"証明書" とは、実際には ASN.1 (Abstract Syntax Notation One) でエンコードされたファイルで、公開キー、公開キーに関する詳細情報、および公開キーの所有者を保持します。また、証明書には有効期間があり、前述の属性 (中でも最も重要なのは公開キー自体です) の信頼保証を行うために使用する別のキー (いわゆる、発行者) で署名されています。ASN.1 は一種のバイナリ XML と考えることができます。XML と同様に、符号化規則、厳密型およびタグが含まれていますが、これらは印刷可能な文字にほとんど対応しないバイナリ値です。

このようなファイルをシステム間で交換可能にするには、標準形式が必要です。この標準形式が RFC 3280 (tools.ietf.org/html/rfc3280 (英語)) に記載されている X.509 (現在はバージョン 3) です。X.509 は証明書に埋め込まれているキーの種類を問いませんが、現在最も普及している非対称暗号化アルゴリズムの RSA アルゴリズムを使用しています。

まず、簡単な歴史から始めましょう。RSA という名前は、このアルゴリズムの 3 人の考案者 (Ron Rivest、Adi Shamir、Len Adleman) の名字からとった頭字語です。彼らは RSA Security という会社を設立し、Public Key Cryptography Standards (PKCS) と呼ばれる標準ドキュメントをいくつか発行しました。これらのドキュメントには、暗号化の複数の側面が記載されています。

これらのドキュメントの中でも最も普及した PKCS #7 では、Cryptographic Message Syntax (CMS) と呼ばれる、データの署名と暗号化のバイナリ形式が定義されています。CMS は、Secure Sockets Layer (SSL) や Secure Multipurpose Internet Mail Extensions (S/MIME) など、現在普及している多くのセキュリティ プロトコルで使用されるようになりました。これは 1 つの標準なので、アプリケーションで署名および暗号化されたデータを複数の当事者間で交換する必要があるときに選択される形式でもあります。PKCS のドキュメントは、RSA Laboratories の Web サイト (www.rsasecurity.com/rsalabs/node.asp?id=2124 (英語)) から入手できます。

証明書の取得方法

証明書を取得するにはいくつか方法があります。ファイルを交換しようとする場合、通常、証明書は 2 つの形式のいずれかになります。1 つは、拡張子が .cer のファイルで、これは X.509v3 形式で署名された ASN.1 ファイルです。このファイルには、前述のように、公開キーとその関連情報が含まれます。このファイルをビジネス パートナーや友人に渡し、その公開キーを使用してデータを暗号化してもらいます。

もう 1 つは、拡張子が .pfx (Personal Information Exchange) のファイルです。.pfx ファイルには、証明書と対応する秘密キー (形式は PKCS #12 標準に記載されています) が含まれています。このようなファイルは機密度が高いので、通常、サーバーへのキー ペアのインポートやバックアップの目的に使用されます。キー ペアをエクスポートするときに、Windows ではパスワードを使って .pfx ファイルの暗号化を行うため、キー ペアをインポートするときは、このパスワードを再度指定する必要があります。

独自の証明書を生成することもできます。証明書の生成方法は、証明書の使用方法によって異なります。相手が特定されない一般的なインターネットのシナリオでは、通常、商用証明機関 (CA) に証明書を要求します。この方式は、こうした既知の CA が証明書や SSL をサポートする Windows などの OS (およびブラウザ) によって既に信頼されている点にメリットがあります。そのため、CA キー交換を行う必要がありません。

B2B やイントラネットのシナリオでは、内部 CA を使用できます。Windows 2000 および Windows Server® 2003 には証明書サービスが含まれています。この機能と Active Directory® を組み合わせることで、組織全体に証明書を容易に配布できます (この後、プライベート CA に証明書を要求する方法を示します)。

開発中には、この方式を使えない場合があります。たとえば、テスト目的ですぐに証明書が必要であれば、makecert.exe を使用できます。このツールは .NET Framework SDK に付属しており、証明書とキー ペアを生成します。IIS Resource Kit にも selfssl.exe という同様のツールがあります。このツールは SSL キー ペアの作成に特化されており、生成したキー ペアを使って、1 回の手順で IIS を構成することもできます。

Windows 証明書ストア

証明書と対応する秘密キーは、ハード ディスク、スマートカード、USB トークンなど、さまざまなデバイスに格納できます。Windows には、証明書が格納されている場所に関係なく統一的にアクセスできるように、証明書ストアという抽象層が用意されています。ハードウェア デバイスが備えている暗号化サービス プロバイダ (CSP) を Windows がサポートしている場合に限り、Certificate Store API を使用して、そのハードウェア デバイスに格納されたデータにアクセスできます。

証明書ストアは、ユーザー プロファイルの奥深くに埋め込まれています。そのため、特定のアカウント向けのキーに ACL を使用できます。すべてのストアは、コンテナにパーティション分割されます。たとえば、ユーザー独自の証明書 (関連付けられた秘密キーを含む証明書) を格納する [個人] というコンテナがあります。[信頼されたルート証明機関] コンテナには、信頼するすべての CA の証明書が含まれます。[その他のユーザー] コンテナには、セキュリティが確保された通信の相手となるユーザーの証明書が含まれます。他も同様です。証明書ストアにアクセスする最も簡単な方法は、certmgr.msc を実行することです。

また、コンピュータ単位のストアもあります。このストアは Windows コンピュータ アカウント (NETWORK、LOCAL SERVICE、および LOCAL SYSTEM) によって使用されます。また、複数のアカウントで証明書やキーを共有する場合にも使用されます。ASP.NET アプリケーションでは、常に、コンピュータ ストアが使用されます。デスクトップ アプリケーションの場合は、通常、ユーザー ストアに証明書をインストールします。

コンピュータ ストアとサービス アカウント ストアは、管理者のみが管理できます。証明書を管理するには、Microsoft 管理コンソール (mmc.exe) を起動し、[証明書] スナップインを追加します。スナップインの追加時に、管理するストアを選択できます。図 1 に、MMC スナップインのスクリーンショットを示します。

図 1 [証明書] MMC スナップイン
図 1 [証明書] MMC スナップイン(画像を縮小するには、ここをクリックします)
図 1 [証明書] MMC スナップイン
図 1 [証明書] MMC スナップイン (画像を拡大するには、ここをクリックします)

このスナップインでは、証明書のインポート、エクスポート、検索以外に、内部のエンタープライズ CA に証明書を要求することもできます。これには、[個人] コンテナを右クリックし、[すべてのタスク] をポイントして、[新しい証明書の要求] をクリックするだけです。その後、ローカル コンピュータによって RSA キー ペアが生成され、秘密キー部分が署名用に CA に送信されます。Windows では、署名済み証明書が証明書ストアに、対応する秘密キーがキー コンテナに追加されます。証明書はストレージ属性によってキー コンテナにリンクされます。

秘密キー コンテナは、対応するアカウントまたは LOCAL SYSTEM のいずれかに対して、ACL で厳密に制御されます。ASP.NET から、または他のユーザー アカウントからコンピュータ プロファイルに格納されたキーにアクセスする際に、このことが問題になります。そのため、コンテナ ファイルの ACL の変更に使用できるツールを自作しました (このツールは www.leastprivilege.com/HowToGetToThePrivateKeyFileFromACertificate.aspx (英語) から入手できます)。

商用の CA や Windows CA にも、証明書を要求するための Web インターフェイスがあります。このようなシナリオでは、通常、Internet Explorer® の ActiveX® コントロールによってキーが生成され、現在のユーザーのストアにインポートされます。原則として、ユーザーやサービスから証明書にアクセスできるようにするには、2 つの選択肢があります。1 つは自身のストアに証明書をインポートする方法で、もう 1 つは該当のユーザーでログオンしているときに証明書を要求する方法です。

証明書を操作する

.NET Framework ではさまざまな場面で証明書を使用します。レベルによっては、この機能のすべてが System.Security.X509Certificates 名前空間に含まれる X509Certificate クラスに依存する場合があります。詳しく調べてみると、証明書クラスの中にはクラス名が 2 で終わるクラスがあることに気付きます。これは、.NET Framework 1.x で X.509 証明書を X509Certificate と表記したためです。このようなクラスは、機能や暗号化操作のサポートが制限されます。バージョン 2.0 で追加された新しいクラスは X509Certificate2 と表記されます。このクラスは X509Certificate から派生され、多くの機能が追加されています。この 2 つのクラスを必要に応じて相互に切り替えることができますが、可能であれば最新バージョンを使用してください。

証明書にアクセスする

ファイル システムから直接証明書を取得できますが、証明書は証明書ストアから取得することをお勧めします。.cer ファイルから X509Certificate2 のインスタンスを作成するには、次のように、単純にファイル名をコンストラクタに渡します。

X509Certificate2 cert1 = new X509Certificate2("alice.cer");

.pfx ファイルから証明書を読み込むこともできます。ただし、前述のように、.pfx ファイルはパスワードで保護できるので、このパスワードを SecureString として提供する必要があります。SecureString はパスワードを内部的に暗号化し、メモリ、ページ ファイル、クラッシュ ダンプなどからパスワードが明らかになるのを最小限に抑えるようにします。このため、この文字列には 1 文字 (値型) ずつしか追加できません。図 2 のコードでは、コンソールにエコー出力しないで SecureString を返します。これは、ユーザーにコンソールからパスワードの入力を求める場合に役に立ちます。

Figure 2 コンソールからパスワードを要求する (Close Figure 2)
private SecureString GetSecureStringFromConsole()
{
    SecureString password = new SecureString();

    Console.Write("Enter Password: ");
    while (true)
    {
        ConsoleKeyInfo cki = Console.ReadKey(true);

        if (cki.Key == ConsoleKey.Enter) break;
        else if (cki.Key == ConsoleKey.Escape)
        {
            password.Dispose();
            return null;
        }
        else if (cki.Key == ConsoleKey.Backspace)
        {
            if (password.Length != 0)
                password.RemoveAt(password.Length - 1);
        }
        else password.AppendChar(cki.KeyChar);
    }

    return password;
}

Kenny Kerr の資料、「Credential Management with the .NET Framework 2.0」(msdn.microsoft.com/library/en-us/dnnetsec/html/credmgmt.asp 参照) (英語) には、通常の Windows 資格情報ダイアログ ボックスの結果を SecureString に変換するコードがあります。SecureString を取得した方法とは無関係に、次のように SecureString を X509Certificate2 コンストラクタに渡して、.pfx ファイルを読み込むことができます。

X509Certificate2 cert2 = new X509Certificate2("alice.pfx", password);

Windows 証明書ストアにアクセスするには、X509Store クラスを使用します。このクラスのコンストラクタには、ストアの場所 (現在のユーザーまたはコンピュータ) とストアの名前を指定します。文字列または StoreName 列挙値を使用してコンテナを指定し、コンテナを開きます。内部名は、MMC スナップインに表示される名前とは必ずしも一致しないことに注意してください。[個人] コンテナは内部名 My にマップされ、[その他のユーザー] コンテナは AddressBook になります。

X509Store の有効なインスタンスを作成したら、証明書の検索、取得、削除、および追加を実行できます。配置のシナリオを除けば、最も頻繁に使用するのはおそらく検索機能でしょう。サブジェクト名、シリアル番号、拇印、発行者、有効期間など、さまざまな条件で証明書を検索できます。アプリケーション内でプログラムを使ってストアから証明書を取得する場合は、サブジェクト キー識別子のような一意のプロパティを使用する必要があります。拇印も一意になりますが、証明書の SHA-1 ハッシュ値は、証明書が更新された場合などに変化することに注意してください。図 3 に、証明書を検索する一般的な方法を示します。

Figure 3 証明書を検索する (Close Figure 3)
static void Main(string[] args)
{
    // search for the subject key id
    X509Certificate2 cert = FindCertificate(
      StoreLocation.CurrentUser, StoreName.My,
      X509FindType.FindBySubjectKeyIdentifier,
      "21f2bf447298e83056a69eb02ebe9085ed97f10a");
}

static X509Certificate2 FindCertificate(
    StoreLocation location, StoreName name,
    X509FindType findType, string findValue)
{
    X509Store store = new X509Store(name, location);
    try
    {
        // create and open store for read-only access
        store.Open(OpenFlags.ReadOnly);

        // search store
        X509Certificate2Collection col = store.Certificates.Find(
          findType, findValue, true);

        // return first certificate found
        return col[0];
    }
        // always close the store
    finally { store.Close(); }
}

X509 Certificate2 のインスタンスを作成したら、証明書のさまざまなプロパティ (サブジェクト名、有効期限、発行者、フレンドリ名など) を調査できます。HasPrivateKey プロパティは、関連付けられた秘密キーがあるかどうかを示します。PrivateKey プロパティと PublicKey プロパティは、対応するキーを RSACryptoServiceProvider のインスタンスとして返します。

証明書をインポートするには、X509Store のインスタンスの Add メソッドを呼び出します。ストアのコンストラクタに存在しないストア名を指定すると、新しいコンテナが作成されます。ファイル名 alice.cer の証明書を新しいコンテナ Test にインポートするコードを以下に示します。

static void ImportCert()
{
    X509Certificate2 cert = new X509Certificate2("alice.cer");
    X509Store store = new X509Store("Test", StoreLocation.CurrentUser);
    try
    {
        store.Open(OpenFlags.ReadWrite);
        store.Add(cert);
    }
    finally { store.Close(); }
}

証明書の詳細の表示と証明書選択ツール

Windows には、証明書を操作するための標準ダイアログ ボックスが 2 つあります。1 つは証明書の詳細 (プロパティや証明書のパス) を表示するダイアログ ボックスで、もう 1 つはユーザーが一覧から証明書を選択できるダイアログ ボックスです。これらのダイアログ ボックスにアクセスするには、X509Certificate2UI クラスの 2 つの静的メソッド、SelectFromCollection と DisplayCertificate を使用します。

証明書の一覧を表示するには、X509Certificate2Collection にデータを設定し、それを SelectFromCollection に渡す必要があります。ストア内の個人用証明書の 1 つをユーザーに選択させることはごく一般的に行われます。この場合、単純に既に開いている X509Store の Certificates プロパティに渡します。また、ダイアログ ボックスのキャプション、メッセージ、および複数選択を許可するかどうかも制御できます。DisplayCertificate メソッドは、エクスプローラで .cer ファイルをダブルクリックした場合に表示されるものと同じダイアログ ボックスを表示します。図 4 に、証明書の選択に使用されるダイアログ ボックスを示し、図 5 に対応するコードを示します。

Figure 5 証明書を選択するコード (Close Figure 5)
private static X509Certificate2 PickCertificate(
  StoreLocation location, StoreName name)
{
    X509Store store = new X509Store(name, location);
    try
    {
        store.Open(OpenFlags.ReadOnly);

        // pick a certificate from the store
        X509Certificate2 cert =
            X509Certificate2UI.SelectFromCollection(
                store.Certificates, "Caption",
                "Message", X509SelectionFlag.SingleSelection)[0];

        // show certificate details dialog
        X509Certificate2UI.DisplayCertificate(cert);
        return cert;
    }
    finally { store.Close(); }
}
図 4 証明書を選択するダイアログ ボックス
図 4 証明書を選択するダイアログ ボックス(画像を縮小するには、ここをクリックします)
図 4 証明書を選択するダイアログ ボックス
図 4 証明書を選択するダイアログ ボックス (画像を拡大するには、ここをクリックします)

証明書を検証する

証明書を検証する場合に考慮する条件がいくつかあります。特に発行元 (一般に、信頼された証明機関の一覧にある CA が発行した証明書のみ信頼します) と現在の有効性 (有効期限が切れたり、発行元の CA によって失効になった場合など、証明書が無効になる可能性があります) を考慮します。X509Chain クラスを使用して、こうしたさまざまなプロパティをチェックできます。このクラスを使用すると、有効性の検証にポリシーを指定できます。たとえば、信頼されたルート証明機関を要求することができます。また、失効リストをオンラインまたはローカルのどちらでチェックするかを指定できます。データの署名に使われた証明書をチェックする必要がある場合、署名が算出された時点で証明書が有効であったかどうかをチェックすることが重要です。これを行うため、X509Chain では検証時刻を変更できます。

ポリシーを構築後、Build メソッドを呼び出して、ChainStatus プロパティで検証結果に関する情報を取得します。検証エラーが複数存在する場合は、ChainElement コレクション全体を繰り返し処理して、詳細情報を入手できます。図 6 に、証明書とその発行者をオンラインとオフラインの失効リストに照らして厳密な検証を行う方法を示します。

Figure 6 厳密な証明書検証 (Close Figure 6)
static void ValidateCert(X509Certificate2 cert)
{
    X509Chain chain = new X509Chain();

    // check entire chain for revocation
    chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain;

    // check online and offline revocation lists
    chain.ChainPolicy.RevocationMode =
        X509RevocationMode.Online | X509RevocationMode.Offline;

    // timeout for online revocation list
    chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 30);

    // no exceptions, check all properties
    chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;

    // modify time of verification
    //chain.ChainPolicy.VerificationTime = new DateTime(1999, 1, 1);

    chain.Build(cert);

    if (chain.ChainStatus.Length != 0)
        Console.WriteLine(chain.ChainStatus[0].Status);
}

SSL サポート

SSL 認証プロトコルは証明書に依存します。.NET Framework での SSL のサポートは、2 つの部分から構成されます。HTTP 経由の SSL の特殊なケース (ただし、最も広範に使用されます) は、HttpWebRequest クラスによって実装されます (このクラスは、最終的には Web サービス クライアント プロキシにも使用されます)。SSL を有効にする場合、https: プロトコルを使用する URL を指定する以外に何も特別なことを行う必要はありません。

SSL でセキュリティが確保されたエンドポイントに接続するときに、サーバーの証明書がクライアントで検証されます。検証に失敗すると、既定では即座に接続が閉じられます。ServicePointManager というクラスにコールバックを指定することで、この動作を変更できます。HTTP クライアント スタックが証明書の検証を行う場合は、常に最初にコールバックが指定されているかどうかをチェックします。指定されている場合は、指定されたコードを実行します。コールバックをフックするには、次のように、RemoteCertificateValidationCallback 型のデリゲートを用意する必要があります。

// override default certificate policy
// (for example, for testing purposes)
ServicePointManager.ServerCertificateValidationCallback =
    new RemoteCertificateValidationCallback(VerifyServerCertificate);

コールバックでは、そのコールバックに渡されるサーバー証明書、エラー コード、およびチェーン オブジェクトを取得し、その後独自のチェックを行って、True または False を返します。こうしたコールバックは、開発やテスト中に、通常行うチェックの 1 つ (たとえば、証明書の有効期限が切れているかどうかなど) を行わない場合に便利です。また、こうしたコールバックを使用して、既定で行われるよりもさらに厳しい検証ポリシーを実装することもできます。図 7 に、サンプルの検証コールバックを用意しました。

Figure 7 検証コールバック (Close Figure 7)
private bool VerifyServerCertificate(
    object sender, X509Certificate certificate,
    X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
    if (sslPolicyErrors == SslPolicyErrors.None) return true;

    foreach (X509ChainStatus s in chain.ChainStatus)
    {
        // allows expired certificates
        if (string.Equals(s.Status.ToString(), "NotTimeValid",
            StringComparison.OrdinalIgnoreCase))
                return true;
    }

    return false;
}

SSL では、証明書を使用したクライアント認証もサポートします。アクセス対象の Web サイトやサービスでクライアント証明書が必須の場合、Web サービス クライアント プロキシと HttpWebRequest の両方に、X509Certicate 型の ClientCertificates プロパティが用意されています。

  proxy.Url =
    "https://server/app/service.asmx";
  proxy.ClientCertificates.Add(
    PickCertificate(...));

さらに、.NET Framework 2.0 では SslStream という新しいクラスが導入されました。このクラスでは、HTTP ではなく、ストリームの上位に SSL 層を作成できます。その結果、ソケット ベースのカスタム プロトコルを SSL 対応にできます。SslStream では、標準の .NET 証明書サポートを複数の方法で使用します。たとえば、次のように前述の検証コールバック メカニズムを使用します。

public SslStream(Stream innerStream, bool leaveInnerStreamOpen,
    RemoteCertificateValidationCallback ValidationCallback) {...}

SslStream を使って SSL 認証を開始するには、次のように、X509Certificate を AuthenticateAsServer メソッドに渡します。

ssl.AuthenticateAsServer(PickCertificate(...));

Web サービス セキュリティ

WS-Security 標準では、クライアントとサーバーの認証、およびセキュリティが確保された通信に証明書を使用することが指定されています。Web Services Enhancements (WSE) for the .NET Framework のようなツールキットや Windows Communication Foundation のようなテクノロジでは、この標準が完全にサポートされます。繰り返しになりますが、これを要約すると、コードで、または構成を通じて証明書を提供することになります。次のコードでは、WSE3 を使用してクライアント証明書を Web サービス プロキシに追加する方法を示しています。

X509SecurityToken token = new X509SecurityToken(PickCertificate(...));
proxy.RequestSoapContext.Security.Tokens.Add(token);

Windows Communication Foundation では、通常、構成ファイルで証明書ストアへの参照を指定します (図 8 参照)。ご覧のように、すべての構成属性は、上記のコードで使用した列挙値に直接対応しています。

Figure 8 WCF で証明書参照を提供する (Close Figure 8)
<system.serviceModel>
  <behaviors>
    <serviceBehaviors>
      <behavior name="ServiceBehavior">
        <serviceCredentials>
          <serviceCertificate storeLocation="LocalMachine"
            storeName="My" x509FindType="FindBySubjectKeyIdentifier"
            findValue="1a7b..." />
        </serviceCredentials>
      </behavior>
    </serviceBehaviors>
  </behaviors>
</system.serviceModel>

セキュリティ ポリシーとコード署名

証明書は、Authenticode® コード署名でも使用されます。バイナリに署名することにより、発行者の情報を追加し、署名済みのファイルは署名後に確実に検証されたことを保証できます。.NET Framework SDK に付属する signtool.exe ツールを使用して、.exe ファイルと .dll ファイルに署名できます。その後、エクスプローラのプロパティ ダイアログ ボックスを使用して、署名の確認や証明書の表示を実行できます。Authenticode と厳密な名前での署名の両方を使用する場合は、まず、厳密な名前での署名を適用する必要があります。また、Authenticode で署名されたアセンブリは読み込み時に遅延が生じる場合があります。つまり、アプリケーションのエントリ ポイントとなる実行可能ファイルに署名されていると、アプリケーションの起動時間が長くなります。

署名済みのファイルをセキュリティ ポリシーに使用することもできます。ソフトウェア制限ポリシーを使用して、署名の有無を基にアンマネージ実行可能ファイルの実行を制限できます (microsoft.com/technet/prodtechnol/winxppro/maintain/rstrplcy.mspx (英語) 参照)。さらに .NET Framework コード アクセス セキュリティ (CAS) ポリシーでは、発行者の証明書に基づくコード グループがサポートされます。

CAS ポリシーを作成するには、mscorcfg.msc を使用して、発行者のメンバシップの状態に基づいて新しいコード グループを作成します。その後、その発行者が署名したすべてのアプリケーションに権限のセットを割り当てることができます (図 9 参照)。

図 9 発行者に権限を割り当てる
図 9 発行者に権限を割り当てる(画像を縮小するには、ここをクリックします)
図 9 発行者に権限を割り当てる
図 9 発行者に権限を割り当てる (画像を拡大するには、ここをクリックします)

ClickOnce マニフェスト

発行者の情報に証明書を使用するもう 1 つのテクノロジは ClickOnce です。ClickOnce アプリケーションを発行するときは、配置とアプリケーションのマニフェストに署名する必要があります。この場合も発行者の情報が追加され、署名を無効にしないでマニフェスト内の重要な情報 (セキュリティ ポリシーやアプリケーションの依存関係など) を変更できないことが保証されます。ClickOnce では、インストール時にクライアントが発行者情報を使用できるので、クライアントはこの情報を利用してアプリケーションの信頼性を判断できます。ClickOnce インストーラは、証明書 (およびその検証結果) に応じて、さまざまな表示を行います。図 10 に、Visual Studio® のマニフェスト署名のダイアログ ボックスを示します。

図 10 Visutal Studio のマニフェスト署名ダイアログ ボックス
図 10 Visutal Studio のマニフェスト署名ダイアログ ボックス(画像を縮小するには、ここをクリックします)
図 10 Visutal Studio のマニフェスト署名ダイアログ ボックス
図 10 Visutal Studio のマニフェスト署名ダイアログ ボックス (画像を拡大するには、ここをクリックします)

データの署名と暗号化

ここまでは、基本的な証明書関連の API に重点を置き、さまざまなテクノロジでこうした API を使用する方法について説明してきました。ここでは、証明書を使ったデータの暗号化と署名などの暗号化操作と、.NET Framework 2.0 に新たに導入された PKCS #7 実装について説明します。

データの保護は、常に、2 つの手順から成るプロセスで行います。まず、データの改ざんを防止するためにデータに署名します。次に、データの漏えいを防止するためにデータを暗号化します。ただし、PKCS #7 クラスを使って暗号化操作を実行する前に、まず、データを CMS データ構造体を表す ContentInfo オブジェクトにラップする必要があります。そこからデータを署名済みデータや暗号化済みデータに変更できます。これらのデータはそれぞれ SignedCms クラスと EnvelopedCms クラスで表されます。

デジタル署名とは、技術的には秘密キーで暗号化されるデータのハッシュです。つまり、秘密キーが関連付けられた証明書または .pfx ファイルが必要です。このような証明書に基づき、データの署名者を表す CmsSigner オブジェクトを作成できます。次に、SignedCms クラスが署名を算出し、PKCS #7、CMS 準拠のバイト配列として出力します。図 11 に、対応するコードを示します。エンコードされたバイト配列には、データ、署名、およびデータの署名に使用した証明書を含みます。

Figure 11 CMS 準拠のバイト配列を出力する (Close Figure 11)
byte[] Sign(byte[] data, X509Certificate2 signingCert)
{
    // create ContentInfo
    ContentInfo content = new ContentInfo(data);

    // SignedCms represents signed data
    SignedCms signedMessage = new SignedCms(content);

    // create a signer
    CmsSigner signer = new CmsSigner(signingCert);

    // sign the data
    signedMessage.ComputeSignature(signer);

    // create PKCS #7 byte array
    byte[] signedBytes = signedMessage.Encode();

    // return signed data
    return signedBytes;
}

バイト配列にどのようなデータが含まれるかは、大量のデータに署名する場合にはそれほど問題になりませんが、少量のデータに署名する場合はややオーバーヘッドが追加されます。たとえば、10 バイトの配列に 2KB の公開キーで署名すると、約 2,400 バイトの配列になります。署名したデータをデータベースなどに保存する場合は、この点に留意してください。他に、いわゆる分離署名を使用する方法があります。この方法ではデータと署名を分離し、個別に保存できます。たとえば、まず少量のデータを複数組み合わせ、それらにまとめて署名することができます。分離署名を作成するには、図 12 に示すように、SignedCms コンストラクタに追加で true を渡す必要があります。

Figure 12 分離署名を作成する (Close Figure 12)
byte[] SignDetached(byte[] data, X509Certificate2 signingCert)
{
    // create ContentInfo
    ContentInfo content = new ContentInfo(data);

    // pass true to the constructor to indicate
    // we want to sign detached
    SignedCms signedMessage = new SignedCms(content, true);

    // these steps are the same
    CmsSigner signer = new CmsSigner(signingCert);
    signedMessage.ComputeSignature(signer);
    byte[] signedBytes = signedMessage.Encode();

    // return only the signature (not the data)
    return signedBytes;
}

データに署名したら、そのデータを暗号化できます。データの受信者が暗号化を解除できるように、受信者の公開キーが必要です。通常、受信者の公開キーは [その他のユーザー] ストアから取得します (証明書ストアを使用しない場合は .cer ファイルから取得します)。このとき、煩雑な作業はすべて EnvelopedCms クラスが行います。CmsRecipientCollection にある、暗号化に使用する公開キーを使用して Encrypt メソッドに渡すだけです。SignedCms と同様に、Encode メソッドでも PKCS #7、CMS 準拠のバイト配列が作成されます (図 13 参照)。

Figure 13 Encode メソッド (Close Figure 13)
byte[] Encrypt(byte[] data, X509Certificate2 encryptingCert)
{
    // create ContentInfo
    ContentInfo plainContent = new ContentInfo(data);

    // EnvelopedCms represents encrypted data
    EnvelopedCms encryptedData = new EnvelopedCms(plainContent);

    // add a recipient
    CmsRecipient recipient = new CmsRecipient(encryptingCert);

    // encrypt data with public key of recipient
    encryptedData.Encrypt(recipient);

    // create PKCS #7 byte array
    byte[] encryptedBytes = encryptedMessage.Encode();

    // return encrypted data
    return encryptedBytes;
}

EnvelopedCms では、データが対称的に暗号化される、ランダムなセッション キーが内部的に生成されます。その後、このセッション キーが各受信者の公開キーで暗号化されます。そのため、データの暗号化のバージョンを受信者ごとに個別にする必要はありません。さらに、追加情報が埋め込まれており、受信者は自身の証明書ストア内で、暗号化解除のための対応する秘密キーを検索できます。

データの暗号化解除と署名の確認

受信側では、上記のプロセス全体を逆順に実行します。つまり、データの暗号化を解除してから、署名と署名に使用した証明書を検証します。コードでは、まず、SignedCms クラスと EnvelopedCms クラスの Decode メソッドを呼び出し、CMS バイト配列のシリアル化を解除して、オブジェクト表現に戻す必要があります。その後、Decrypt と CheckSignature をそれぞれ呼び出すことができます。

プロセスでは、暗号化されたパッケージ内を調べ、証明書ストア内の対応する秘密キーを検索することで、セッション キーの暗号化を解除できるかどうかを確認します。その後、暗号化を解除したセッション キーを使って、実際のデータの暗号化を解除します。また、証明書ストア内に秘密キーが格納されていない場合に暗号化の解除中に考慮する追加の証明書のリストを指定することもできます。

static byte[] Decrypt(byte[] data)
{
    // create EnvelopedCms
    EnvelopedCms encryptedMessage = new EnvelopedCms();

    // deserialize PKCS#7 byte array
    encryptedMessage.Decode(data);

    // decryt data
    encryptedMessage.Decrypt();

    // return plain text data
    return encryptedMessage.ContentInfo.Content;
}

データの確認は、2 つの手順から成るプロセスで行います。まず、署名が有効である (つまり、データが改ざんされていない) ことを確認します。次に、署名に使った証明書をチェックします。SignedCms クラスの CheckSignature メソッドを使えば、両方の手順を一度に実行できます。この場合、証明書は既定のシステム ポリシーに対して検証されます。このプロセスをさらに細かく制御する場合は、X509Chain オブジェクトと 図 6 に示すようなコードを使用して独自のチェックを実行できます。また、図 14 には署名のチェックと削除に使用するコードを、図 15 には分離署名検証に使用するコードを示します。

Figure 15 分離署名の検証 (Close Figure 15)
static bool VerifyDetached(byte[] data, byte[] signature)
{
    ContentInfo content = new ContentInfo(data);

    // pass true for detached
    SignedCms signedMessage = new SignedCms(content, true);

    // deserialize signature
    signedMessage.Decode(signature);

    try
    {
        // check if signature matches data
        // the certificate is also checked
        signedMessage.CheckSignature(false);
        return true;
    }
    catch { return false; }
}
Figure 14 署名の確認と削除 (Close Figure 14)
byte[] VerifyAndRemoveSignature(byte[] data)
{
    // create SignedCms
    SignedCms signedMessage = new SignedCms();

    // deserialize PKCS #7 byte array
    signedMessage.Decode(data);

    // check signature
    // false checks signature and certificate
    // true only checks signature
    signedMessage.CheckSignature(false);

    // access signature certificates (if needed)
    foreach (SignerInfo signer in signedMessage.SignerInfos)
    {
        Console.WriteLine("Subject: {0}",
          signer.Certificate.Subject);
    }

    // return plain data without signature
    return signedMessage.ContentInfo.Content;
}

まとめ

セキュリティ ポリシーや通信プロトコルを扱うときは、あらゆる場面で証明書に遭遇します。この記事では、証明書の取得と検索に使用する基本的な API について説明し、これらの API を使用して暗号化とデジタル署名を行う方法を示しました。また、Windows 証明書ストアや公開キーと秘密キーの関係を理解するのに必要な高度なアプリケーション サービスの例をいくつか用意しました。

この記事のソース コードは MSDN®Magazin Web サイトからダウンロードして入手できます。このソース コードには、ファイルの署名と暗号化をサポートする簡単な Windows フォームが含まれています。ここでは、さまざまなストアからの証明書の選択、暗号化と署名を使用したデータの保護と確認など、この記事で説明した多くの技法を使用しています。

Dominick Baier は、ドイツ在住の独立セキュリティ コンサルタントです。デザインとアーキテクチャのセキュリティ確保、コンテンツ開発、侵入テスト、コード監査に関して企業を支援しています。また、DevelopMentor で、セキュリティ カリキュラム「Developer Security MVP」(英語) の指導も行っており、『Developing More-Secure Microsoft ASP.NET 2.0 Applications』(Microsoft Press 発行、2006 年) (英語) の著者でもあります。彼のブログは www.leastprivilege.com (英語) で参照できます。


デジタル メディア: WPF を使ってコントロールと 3D の表面にビデオを追加する

デジタル メディア

WPF を使ってコントロールと 3D の表面にビデオを追加する

Lee Brimelow
この記事の内容 : :
  • Windows Presentation Foundation のデジタル メディアの基礎
  • MediaElement と MediaPlayer を使用する
  • WPF コントロールに埋め込まれたビデオ
  • ビデオを 3D の表面にマップする
この記事は次のテクノロジを使用しています:
.NET Framework 3.0
コンテンツ
技術概要
MediaElement コントロールを使用する
MediaPlayer クラスを使用する
WPF コントロールの内部にビデオを埋め込む
ウエット フロア (Wet Floor) 技法
ビデオを 3D の表面にマップする
ビデオ フレームからビットマップをレンダリングする
WPF のその他のオーディオ機能
まとめ

Windows Presentation Foundation (WPF) を取り巻く騒ぎが Adobe Flash コミュニティ全体に広がり始めたとき、私の最初の反応は懐疑的なものでした。

マイクロソフトは、私を含む多くの Flash 開発者最愛のプラットフォームをサポートする企業に難癖をつける競合テクノロジを導入したと考えたのです。その後、Windows® Presentation Foundation のデモを行うためにサンフランシスコの frog design のスタジオにテクニカル エバンジェリストを派遣したという電子メール メッセージをマイクロソフトから受け取ったので、Flash があらゆる点で最高のものである理由を全員に示す絶好のチャンスだと考えました。

Karsten Januszewski はプレゼンテーションの途中で、今では有名になった North Face デモ (channel9.msdn.com/Showpost.aspx?postid=116327 (英語)) を披露しました。このデモでは、高品質の Windows Media® Video (WMV) クリップにマップされた曲線の 3D メッシュ オブジェクトに 3D のカルーセルが描かれています。このデモは、サンフランシスコにあるインタラクティブ スタジオ Fluid と Microsoft Windows Presentation Foundation チームのメンバとの共同で作成されました。私は現実に直面して先入観を捨てることにしました。はっきり言うと、Flash などのプラットフォームでこの種のプレゼンテーションを実現するのは困難または不可能です。

このセッションから間もなく、私は自尊心を抑えて Microsoft® .NET Framework 3.0 ランタイムをダウンロードしました。そして、Windows Presentation Foundation インタラクティブ デザイナとしての私のキャリアが始まりました。その時以来、Flash と Windows Presentation Foundation の 2 つのテクノロジの中で実際に競合する分野はそれほど多くはないということから、両者を対比して行う論争はまったくなくなりました。マイクロソフトが、グラフィック、ビデオ、アニメーション、オーディオなどを含む、クロスプラットフォーム Web ページの配信に関する最新のソリューションをリリースすると、再び論争が巻き起こるかもしれません。このリリース予定のソリューションはコードネームを "WPF/E" といい、"WPF/E" デベロッパー センター (msdn2.microsoft.com/bb187358.aspx (英語)) からプレビューを入手できます。

ビデオを 3D の表面にマップする機能は、主に、インタラクティブ デザイナの注意を引きつけるために必要なアイ キャンディを提供していました。しかし、この機能はオーディオとビデオを Windows Presentation Foundation に統合した場合に可能になることのほんの一部にすぎません。この記事では、Windows Presentation Foundation のメディア統合を十分理解できるようにし、独自のアプリケーションにメディア ファイルを追加するために必要な知識を提供します。また、オプションで独自のアプリケーションが人目を引く外観を備えるようにするために必要な知識についても説明します。

技術概要

現在、Win32® アプリケーションにデジタル メディア ファイルを追加する場合に最も一般的に使われるのは、DirectShow® API を使用する方法と、Windows Media Player ActiveX® コントロールを埋め込む方法の 2 つです。DirectShow を使用すると開発者がメディア統合を完全に制御できますが、Windows Media Player ActiveX コントロールを単純に埋め込むよりもはるかに複雑になります。

Windows Presentation Foundation アプリケーションでメディアの表示や管理をきめ細かく制御する必要がある場合、DirectShow を使用することは依然として実行可能な選択肢の 1 つです。また、既存の Win32 アプリケーションを Windows Presentation Foundation と .NET Framework 3.0 に移植する際に、Windows Presentation Foundation アプリケーションに DirectShow を埋め込むこともできます。

Windows Media Player ActiveX コントロールを使用してメディアを再生することは Windows Presentation Foundation でも選択肢の 1 つですが、新しい MediaElement コントロールを使用する方がはるかに妥当です。一見すると両者は基本的に同じことを行えるように思えますが、後者は Windows Presentation Foundation に適切に統合されます。具体的には、アルファブレンディングでメディアを合成でき、3D の表面にレンダリングすることもできます。

オーディオとビデオが Windows Presentation Foundation に統合されることにより、選択可能な複数のオプションが開発者に提供されます。それぞれのオプションには複雑さとメディア ファイルの制御に関して固有のレベルがあります。使用するコントロールやクラスとは関係なく、背後で行われていることはほぼ同じです。一般的な考え方として、アプリケーションが Windows Presentation Foundation でメディア ファイルを再生するという場合は、ファイルを再生するために、バックグラウンドで Windows Media Player ランタイムを起動することになります。Windows Presentation Foundation が Windows Media Player を使用し、独自のメディア レンダリング エンジンを備えていないことにはいくつか利点と欠点があります。Windows Media Player に便乗することは、ActiveX コントロールを使用することに似た点があります。

Windows Media Player が Windows Presentation Foundation メディアの再生を担当するので、Windows Presentation Foundation と互換性のあるメディア形式を説明するのは簡単です。基本的には、既にインストールされている Windows Media Player を使ってエンド ユーザーが再生できるすべての形式が、Windows Presentation Foundation アプリケーションで使用できます。新たなメディアの種類に対するサポートを追加するためには、ユーザーは新たなビデオ コーデックをインストールする必要があります。

メディア ファイルを再生する場合、Windows Media Player ランタイムを起動する必要があるため、メディアを最初に再生するときに、バックグラウンドでランタイムが読み込まれる間遅延が生じることに注意してください。その結果、特に Windows Presentation Foundation でオーディオやビデオとアニメーションなどのイベントとの同期を取ろうとする場合、メディアを正確に制御することが困難になる可能性があります。この状況を回避する 1 つの方法は、メディア ファイルを事前に読み込み、使用準備が整うまで一時停止状態にしておくことです。もう 1 つの選択肢として、アニメーション エンジンのスリップを使用して、他のアニメーションを MediaElement に依存させることができます。そうすれば、少なくともすべてに均等に遅延が生じます。これは最適なワークフローではありません。そのため、Windows Presentation Foundation メディア チームは次のバージョンでの解決に向けて取り組んでいます。さいわい、オーディオを再生する場合には代替手段として System.Media.SoundPlayer クラスがあります。このクラスでは、Windows Media Player を必要としないで WAV ファイルを単純に再生できます。この記事の後半で、このクラスの使い方を見てみることにしましょう。

MediaElement コントロールを使用する

新しい MediaElement コントロールを使用すると、実に簡単にメディアを Windows Presentation Foundation アプリケーションに追加できます。このコントロールは、Windows Presentation Foundation の他のすべてのコントロールと同様に、XAML を使って、または C# や Visual Basic® を使用する分離コード ファイルを使って追加できます。MediaElement では、メディア ファイルの基本的な再生や制御が可能で、Independent モードまたは Clock モードのいずれかで実行できます。

Independent モードが既定のモードで、従来の Media Player に最も近い形式で機能します。

   <MediaElement Source="myMedia.wmv" Width="320"
     Height="240" LoadedBehavior="Manual" />

Source プロパティに、メディア ファイルの URI を設定します。メディア ファイルが読み込まれるときの動作は、LoadedBehavior プロパティによって決まります。このプロパティには、MediaState 列挙値の値を設定する必要があります。列挙値の値は、Close、Manual、Pause、Play、または Stop です。MediaElement コントロールの Play、Pause、または Stop の各メソッドを使用してメディアの再生を制御するためには、LoadedBehavior プロパティに Manual を設定する必要があります。

図 1 に、Independent モードで実行されている MediaElement コントロールを使用する簡単な Media Player の例を示します。図 2 には、このコントロールの実装に使用する XAML と C# コードを示します。

Figure 2 MediaElement コントロールを実装する (Close Figure 2)

XAML
XAML
<Window x:Class="VideoProject.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="VideoProject" Height="800" Width="600">
<Grid>
    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
        <MediaElement Name="myMedia" Source="myMedia.wmv"
            LoadedBehavior="Manual" Width="320" Height="240" />
        <StackPanel Orientation="Horizontal" Margin="0,10,0,0">
            <Button Content="Play" Margin="0,0,10,0"
                Padding="5" Click="mediaPlay" />
            <Button Content="Pause" Margin="0,0,10,0"
                Padding="5" Click="mediaPause" />
            <Button x:Name="muteButt" Content="Mute"
                Padding="5" Click="mediaMute" />
        </StackPanel>
    </StackPanel>
</Grid>
</Window>

C#
C#
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace VideoProject
{
    public partial class Window1 : System.Windows.Window
    {
        public Window1()
        {
            InitializeComponent();
            myMedia.Volume = 100;
            myMedia.Play();
        }

        void mediaPlay(Object sender, EventArgs e)
        {
            myMedia.Play();
        }

        void mediaPause(Object sender, EventArgs e)
        {
            myMedia.Pause();
        }

        void mediaMute(Object sender, EventArgs e)
        {
            if (myMedia.Volume == 100)
            {
                myMedia.Volume = 0;
                muteButt.Content = "Listen";
            }
            else
            {
                myMedia.Volume = 100;
                muteButt.Content = "Mute";
            }
        }
    }
}
図 1 簡単な MediaElement コントロール
図 1 簡単な MediaElement コントロール

Clock モードで MediaElement コントロールを実行すると、堅牢な Windows Presentation Foundation アニメーション エンジンを使用してメディアの再生を制御できます。Clock モードを有効にするには、MediaTimeline の一部として MediaElement を対象に指定するか、MediaTimeline から MediaClock を明示的に作成し、それを MediaElement に割り当てます (ただし、後者の方法はお勧めしません)。これには少し混乱するかもしれませんが、基本的には MediaElement を Storyboard にアタッチし、Windows Presentation Foundation アニメーション エンジンを使用して、コントロールの位置をアニメーションで動かすことを意味しているだけです。その結果、アプリケーション内でメディアの再生と他のアプリケーションを同期させることができます。

以下に、Storyboard を使用して、Clock モードでメディアを再生するのに必要な XAML コードの例を示します。

<MediaElement Name="myMedia" Width="320" Height="240" />
<Grid.Triggers>
<EventTrigger RoutedEvent="Grid.Loaded">
  <EventTrigger.Actions>
    <BeginStoryboard>
      <Storyboard Storyboard.TargetName="myMedia">
        <MediaTimeline Source="myMedia.wmv"
         BeginTime="00:00:00" Duration="00:05:00" />
      </Storyboard>
    </BeginStoryboard>
  </EventTrigger.Actions>
</EventTrigger>
</Grid.Triggers>

メディア ファイルの Source は、MediaElement ではなく、MediaTimeline で設定していることに注意してください。MediaTimeline では、Storyboard.TargetName 添付プロパティ経由で MediaElement コントロールを対象に指定します。Windows Presentation Foundation のアニメーション エンジンに馴染みがないと、Clock モードに困惑する可能性があるので、Windows Presentation Foundation の基本的なアニメーションに慣れてから、このモードを使用してください。

MediaElement を設定し、そのモードを構成したら、メディア再生のエクスペリエンスを強化するために、多くのプロパティを読み取ったり、書き込んだりすることができます。メディア ファイルの位置を取得または設定するには、Position プロパティに有効な TimeSpan オブジェクトを設定するだけです。BufferingProgress プロパティや DownloadProgress プロパティを読み取ることによって、バッファリングや再生の進行状況をユーザーに表示できます。オーディオ コントロールの場合は、Balance プロパティや Volume プロパティを設定することで、メディアのオーディオ トラックをカスタマイズまたは動作させることができます。では、これらの値を 2 つのスライダにデータバインドした場合を考えてください。そうすれば、ユーザーはメディアの音量を完全に制御できるようになります。

MediaElement は、多くの有益なイベントも発生します。メディアが開始されたか終了されたかを基にアプリケーションの他の部分をトリガする場合、MediaOpened イベントと MediaEnded イベントが便利です。バッファリング中にビデオが停止してしまうときにユーザーに状況を知らせるには、BufferingStarted イベントと BufferingEnded イベントを使用できます。これらのイベントを BufferingProgress プロパティと連携して使用すると、Windows Media Player でストリーミング ビデオを表示している場合と同様に、バッファリングのパーセンテージをユーザーに表示できます。

私の実体験に基づくと (Windows Presentation Foundation メディア チームとの対話の中でさらに強く感じましたが)、Clock モードが決定的に必要な場合を除いて、通常、Independent モードで MediaElement コントロールを使用することをお勧めします。標準のメディアを再生および制御する場合、Storyboard や MediaTimeline を扱う必要はありません。XAML を使用して MediaElement を追加する機能により、デザイナは他の作業をまったく行わずにメディアを実装できるようになるので、メディア統合では既定で MediaElement を選択してください。

MediaPlayer クラスを使用する

MediaElement コントロールが十分な機能を提供せず、作成中のアプリケーション向けに再生を十分制御できない場合、考慮すべき次の選択肢は System.Windows.Media 名前空間にある MediaPlayer クラスを使用することです。これは Windows Presentation Foundation のコントロール要素ではないので、MediaPlayer を実装する場合、C# または Visual Basic を使用する分離コードファイルで MediaPlayer を作成するのが唯一の方法です。以下に、C# を使用して MediaPlayer を実装する方法を示す簡単な例を紹介します。

MediaPlayer mp = new MediaPlayer();
Try {
    mp.Open(new Uri("myMedia.wmv"));
}
catch(Exception ex) {
    MessageBox.Show(ex.Message);
}

MediaPlayer クラスで使用できるプロパティとメソッドは、MediaElement コントロールのプロパティとメソッドとまったく同様です。これは偶然の一致ではありません。MediaElement は、基本的には MediaPlayer クラスの高度なラッパーです。MediaPlayer を使用する場合に主に注意する点は、MediaPlayer には直接的なビジュアル表現がなく、MediaPlayer をビジュアル ツリーに直接追加できないことです。ビデオを再生するには、表示面に MediaPlayer を描画後、その表示面をビジュアル ツリーに追加する必要があります。オーディオを再生する場合、明らかにビジュアル表現は不要なので、MediaPlayer が適切な選択肢になります。ビデオをそのまま再生する場合は、UIElement クラスから継承されており、アプリケーションのビジュアル ツリーに直接追加できる点から、MediaElement コントロールがはるかに優れた選択肢になります。

MediaElement コントロールとまったく同様に、MediaPlayer のインスタンスを Independent モードと Clock モードのいずれかで実行できます。コントロールの再生に関しては、基本的には MediaElement コントロールとまったく同様です。では、MediaPlayer にはどのような違いがあるのでしょう。1 つは LoadedBehavior プロパティと UnloadedBehavior プロパティを扱えないことです。これらは MediaElement コントロール固有のプロパティです。VideoDrawing クラスを使用して 3D の表面にメディアを描画することを計画している場合は、VideoDrawing の Player プロパティは値として MediaElement コントロールを受け取らないので、MediaPlayer を使用する必要があります (ご心配なく。この後すぐに、3D などの表面にビデオを描画する方法を示します)。

もう 1 つの利点 (状況によっては欠点にもなります) は、MediaPlayer は XAML を使って実装されないので、ビジュアル ツリーの一部にならないことです。メディア ファイルでビジュアル表現が必要でなければ、ビジュアル ツリーの一部にならないことが有益である場合があります。また、アプリケーションにメディア ファイルを読み込む方法にもやや違いがあります。MediaElement コントロールでは Source プロパティを設定しますが、MediaPlayer クラスを使用するときは、有効な Uri オブジェクトを渡して、Open メソッドを呼び出します。

したがって、アプリケーションで XAML を使用してメディア ファイルを実装する必要がない場合、またはアプリケーションに自動的なビジュアル表現が備わっている場合、オーバーヘッドが追加される恐れなしに、MediaElement コントロールのすべての機能を使えるため、おそらく Media Player が最適な選択肢になります。

WPF コントロールの内部にビデオを埋め込む

MediaElement コントロールは System.Windows.UIElement クラスから継承されているので、他の UIElement コントロールを配置できるところであればどこでも配置できます。一例として、MediaElement コントロールを Button コントロールの内部に入れ子にして、ビデオ ボタンを作成します。これは、以下の XAML コードを使用して実現できます。

  <Button Name="myVideoButton" Click="playVideo"
          Width="320" Height="240">
      <MediaElement Name="myVid" Source="myMedia.wmv"
          LoadedBehavior="Manual" Width="320"
          Height="240" />
  </Button>

このコードでは、Button コントロールの内部に MediaElement コントロールを埋め込み、LoadedBehavior プロパティを Manual に設定しています。Button の Click イベントでは、MediaElement に再生を指示するメソッド (ここでは示していません) を呼び出します。ほんの数行のコードで、インタラクティブ ビデオ サムネイルを作成しました。Windows Presentation Foundation では MediaElement コントロールがビジュアル ツリー内の他の任意の UI 要素とまったく同様に扱われる点から、あらゆる種類の創造的可能性が広がります。次のセクションで説明するように、MediaElement で再生されるビデオを他の Windows Presentation Foundation オブジェクト上に描画して、信じられないほど充実したユーザー エクスペリエンスを生み出すことができます。

ウエット フロア (Wet Floor) 技法

最近、鏡と反射の効果を使ったインターフェイスが広まっているのが目に付きます。間違いなく、Windows Presentation Foundation でこうした技法の提供を求められる日が来るでしょう。さいわい、これは容易に実現できます。Windows Presentation Foundation に用意された広範なブラシ システムを使用すると、あるコントロールを別のコントロール上に容易に描画できます。この例では、MediaElement コントロールで再生されているビデオを VisualBrush を使用して Rectangle 上に描画し、ビデオの完全な反射像を生み出しています。デザインの世界では、これをウエット フロア (Wet Floor) 効果と呼びます。

ウエット フロア効果を作り出す最も簡単な方法は、StackPanel コントロールの最初のスロットに MediaElement コントロールを配置することです。次に、その下にビデオと同じサイズで Rectangle を作成します。続いて、VisualBrush を使用して Rectangle を塗りつぶし、Rectangle の Visual プロパティを MediaElement コントロールにデータバインドします。最後に、映像を反転して鏡像を作成し、映像が徐々に消えるように OpacityMask を適用します。

図 3 に完成した効果を、図 4 に XAML コードを示します。従来の Win32 プログラミングでは、この種の表示操作は極めて困難でした。Windows Presentation Foundation の優れたデータバインド機能のおかげで、この種の操作がビデオの更新に同期して自動的に更新されるライブ映像になります。

Figure 4 ウエット フロア効果を生み出すために使用する XAML (Close Figure 4)
<Grid x:Name="myGrid">
<StackPanel VerticalAlignment="Center">
    <MediaElement Name="myVid" Source="myMedia.wmv"
        LoadedBehavior="Play" Width="320" Height="240" />
    <Rectangle Width="320" Height="240">
        <Rectangle.Fill>
            <VisualBrush Visual="{Binding ElementName=myVid}" />
        </Rectangle.Fill>
        <Rectangle.OpacityMask>
            <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
                <GradientStop Color="#AA000000" Offset="1" />
                <GradientStop Color="#00000000" Offset="0" />
            </LinearGradientBrush>
        </Rectangle.OpacityMask>
        <Rectangle.RenderTransform>
            <TransformGroup>
                <ScaleTransform ScaleY="-1" />
                <TranslateTransform Y="242" />
            </TransformGroup>
        </Rectangle.RenderTransform>
    </Rectangle>
</StackPanel>
</Grid>
図 3 完成したウエット フロア
図 3 完成したウエット フロア

ビデオを 3D の表面にマップする

Windows Presentation Foundation に関して最大の騒ぎを引き起こしたと考えられる分野を 1 つ挙げるとすれば、ビデオ ファイルを 3D メッシュ オブジェクトにマップする機能になるでしょう。North Face デモでは、視覚的に驚くほど美しいユーザー インターフェイスを作り出すために、この機能がどれほど優れているかを示しました。多くの人々が、この効果は単なる宣伝にすぎないとか、リソースを使いすぎるとかいう記事にしていますが、私の感じでは、適度に行えば、普通のアプリケーションを、際立ったエクスペリエンスをユーザーにもたらすアプリケーションに変換できます。

パフォーマンスに関しては、当然、3D メッシュの表示やアニメーションに関連するオーバーヘッドが相当量生じます。これは、3D メッシュにマップされるビデオとしてフルモーション ビデオ表示を追加する場合の大量のオーバヘッドの話です。モカ フラペチーノを飲む場合とまったく同様に、ほどほどにするのが大切です。全般的には、3D メッシュの複雑性 (ポリゴン数) を必要最低限に抑えている限り、Windows Presentation Foundation の 3D パフォーマンスは優れていることがわかりました。Windows Presentation Foundation メディア チームは、今後のバージョンで 3D メディアの負荷を下げることに取り組んでいます。ただし、2D メディアよりも負荷が高くなるのは当然でしょう。

ビデオを 3D オブジェクトにマップする方法を見ていく前に、まず、Windows Presentation Foundation で 3D シーンを設定するのに何を行うかを復習しましょう。第一に、3D オブジェクトをシーンに配置する必要があります。.NET Framework 3.0 には 3D 基本クラスが含まれていません。ただし、マイクロソフトの社員が作成したと思われる Mesh3DObjects というクラスがあります。このクラスでは、広範な基本オブジェクトを作成できます (このクラスの詳細については、Karsten Januszewski によるチュートリアル (msdn.microsoft.com/library/en-us/dnlong/html/avalon2d-3d.asp (英語) を参照してください)。

Windows Presentation Foundation には、3D シーンを作成するために必要な特定のセットの要素があります。まず、3D オブジェクトが必要です。このオブジェクトには、シーン内で表示可能にするための何らかの種類のマテリアルを備えている必要があります。マテリアルは、SolidColorBrush、LinearGradientBrush、ImageBrush、VisualBrush などのブラシを使用して作成します。ビデオを始めとするビジュアル要素を 3D の表面にマップするために VisualBrush を使用する方法はすぐにわかります。この技法は、基本的には前述のウエット フロア技法と同じです。

3D メッシュとマテリアルは GeometryModel3D 要素の内部にグループ化する必要があります。そうすることで、有効な Windows Presentation Foundation 3D オブジェクトが作成されます。3D シーンを目に見えるようにするためには、シーンに 1 つ以上のライトを追加する必要があります。カメラがなければ写真が撮れないのとまったく同様に、Windows Presentation Foundation の 3D シーンにはこの両方が必要です。最後に、シーンを完成するには、処理をシールするためにこれらすべてのアイテムを Viewport3D コントロールにラップする必要があります。

有効に機能する 3D シーンを適切に作成したら、ビデオなどのメディアを 3D メッシュにマッピングする作業を始めることができます。これは、VisualBrush を使用して、3D の表面にビデオを描画することで実現します。次のコードでは、VisualBrush を 3D オブジェクトの DiffuseMaterial に適用しています。

<GeometryModel3D.Material>
<DiffuseMaterial>
    <DiffuseMaterial.Brush>
        <VisualBrush>
            <VisualBrush.Visual>
                <MediaElement Source="myMedia.wmv" />
            </VisualBrush.Visual>
        </VisualBrush>
    </DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>

図 5 に、Windows Presentation Foundation の 3D の表面にマップされたビデオの 1 フレームを示します。

図 5 WPF 3D の表面にマップする
図 5 WPF 3D の表面にマップする

Windows Presentation Foundation の 3D の表面にビデオをマップするために使用できる別の方法があります。それは、System.Windows.Media 名前空間に含まれる VideoDrawing クラスで、まさにその名が示すとおりの機能を果たします。このクラスでは、MediaPlayer インスタンスからのビデオを DrawingBrush を使用して、任意のブラシ互換の表面に描画できます。これを行うには、DrawingBrush の Drawing プロパティに VideoDrawing クラスのインスタンスを設定する必要があります。この VideoDrawing のインスタンスでは、Player プロパティにビデオ ファイルを含む MediaPlayer インスタンスを設定することによって、ビデオをレンダリングできます。最後に、DrawingBrush を 3D オブジェクトの DiffuseMaterial の Brush プロパティに適用できます。次のコードは、C# を使用したサンプル実装を示します。

MediaPlayer mp = new MediaPlayer();
mp.Open(myValidMediaUri);
VideoDrawing vd = new VideoDrawing();
vd.Player = mp;
DrawingBrush db = new DrawingBrush();
db.Drawing = vd;
diffuseMat.Brush = db;
vd.Player.Play();

VisualBrush を使用する場合と VisualDrawing クラスを使用する場合の大きな違いの 1 つは、VisualDrawing クラスでは (C# または Visual Basic を使用する) 分離コード ファイルを作成および実装する必要がある点です。さらに、VideoDrawing クラスは MediaPlayer インスタンスとのみ連携して機能し、MediaElement コントロールとは連携しません。どちらの方法でもビデオを 3D に正しくマップするために、Windows Presentation Foundation が中間的な描画表示面を作成する必要があるので、パフォーマンスの点では大きな違いはありません。

ビデオ フレームからビットマップをレンダリングする

ビデオ アプリケーションは、その性質上、ビデオ ストリームからスクリーン ショットを取り、ビットマップにレンダリングする必要が生じることがあります。このような例は、市販の DVD 再生ソフトウェアにいくつか見受けられます。こうしたソフトウェアでは、ユーザーが再生中の任意の時点でカメラ アイコンをクリックすると、ビデオからスクリーン ショットを取得できます。他にも、一連のビデオ ファイルのすべてを実際に MediaElement コントロールに接続したくない場合は、この機能を使用してビデオ ファイルのサムネイルを作成できます。また、実行時のビデオ特殊効果を作成することも考えられます。ただし、この種の作業には、DirectShow API を使用する方がはるかに適しています。

Windows Presentation Foundation での大部分の作業と同様に、この種の作業を実現する方法も複数あります。ビデオ フレームのビットマップをレンダリングする場合も同じです。使用する方法とは関係なく、Windows Presentation Foundation で表示要素のビットマップをレンダリングする中核機能は、RenderTargetBitmap クラスで処理されます。このクラスは、Windows.Media.Imaging 名前空間に含まれています。この驚くべきクラスでは、Windows Presentation Foundation アプリケーションのビジュアル ツリー内のすべての項目からビットマップを作成できます。

このクラスを使用してビットマップをレンダリングするには、まず、新しく DrawingVisual オブジェクトを作成する必要があります。このオブジェクトは、RenderTargetBitmap.Render メソッドに渡されます。次に、DrawingVisual.Render メソッドに渡す DrawingContext オブジェクトを新しく作成します。この DrawingContext のインスタンスを使って、すべての種類のグラフィックを DrawingVisual オブジェクトに描画できます。ビデオのフレームを描画するには、DrawingContext.DrawVideo メソッドを呼び出します。このメソッドには、ビデオ ファイルを現在再生している MediaPlayer のインスタンスを渡します。また、ビデオ ファイルのキャプチャ領域を決める Rect 値も渡す必要があります。

この時点で、キャプチャしたビデオ フレームが DrawingVisual インスタンスに含まれることになり、そこから実際にビットマップをレンダリングできます。レンダリングする場合は、このインスタンスを RenderTargetBitmap.Render メソッドに渡す必要があるだけです。これで RenderTargetBitmap インスタンスにはビデオ フレームが含まれるようになりますが、このインスタンスを BitmapFrame.Create メソッドに渡すまではビットマップになりません。ビットマップ フレームを表示するには、Image コントロールの Source プロパティの値としてこのメソッド呼び出しを設定するだけです。次のコードは、こうしたフレームキャプチャ技法を C# で実装したものです。

RenderTargetBitmap rtb = new RenderTargetBitmap(320, 240, 1 / 200,
    1 / 200, PixelFormats.Pbgra32);
DrawingVisual dv = new DrawingVisual();
DrawingContext dc = dv.RenderOpen();
dc.DrawVideo(myMediaPlayer, new Rect(0, 0, 320, 240));
dc.Close();
rtb.Render(dv);
Image im = new Image();
im.Source = BitmapFrame.Create(rtb);

対象のビットマップの形式は、RenderTargetBitmap コンストラクタに有効な PixelFormats 列挙値を渡して設定します。ビデオ フレームからビットマップをレンダリングする機能は、最初は限られた機能のように思えますが、この種の機能は実際には単なるスクリーン キャプチャ ツール以上の多くの使い道があります。

WPF のその他のオーディオ機能

Windows Presentation Foundation のメディア統合についてここまで説明してきたほとんどのことは、ビデオとオーディオの両方に当てはまります。ただし、Windows Presentation Foundation でオーディオの再生を扱う場合に使用できるクラスが他にもいくつかあります。前述のように、Windows Presentation Foundation で再生されるメディアは、実際には一連の Windows Media Player ランタイムによって駆動されます。そのため、ファイルを再生する前にこのランタイムを読み込んでおく必要があります。メディア ファイルとユーザー イベントやアニメーションとの同期を取る際には、これが大きな問題を引き起こすことがあります。ただし、この問題はタイムラインのスリップ動作で緩和できる場合があります。オーディオに関しては、こうした問題を回避できる別の手段がいくつかあります。

System.Media 名前空間に含まれる SoundPlayer クラスには、オーディオ WAV ファイルを読み込んで再生するための簡単なインターフェイスが用意されています。対象となる WAV ファイルは、外部ファイルであっても、ストリーミング ファイルであっても、埋め込みリソースであってもかまいません。この方式のオーディオ再生は、Windows Media Player に再生をまったく依存しないので、MediaElement コントロールを使ってオーディオ ファイルを再生するよりもオーバーヘッドがはるかに少なくなります。

WAV ファイルを SoundPlayer インスタンスに読み込むには、まず、SoundLocation プロパティに WAV ファイルの場所を設定してから、Load メソッドを呼び出して読み込み処理を開始する必要があります。ファイルが完全に読み込まれたら、SoundPlayer.Play メソッドを使用して再生できます。ファイルがすべてメモリに読み込まれないと、ファイルの再生は開始されません。

この簡易オーディオ再生は、SoundPlayerAction コントロールを使用して XAML でも使用できます。XAML では、EventTrigger 内部で WAV ファイルを再生できます。たとえば、ボタン ロールオーバーのインターフェイス音のようなものに、これが非常に役立ちます。これを使用するには、単純に、EventTrigger の EventTrigger.Actions ブロック内に配置します。以下に、マウス ボタンの MouseEnter イベントに応じて呼び出される SoundPlayerAction のコードを示します。

<EventTrigger RoutedEvent="Button.MouseEnter" SourceName="myButton">
<EventTrigger.Actions>
        <SoundPlayerAction Source="media/overSound.wav"/>
    </EventTrigger.Actions>
</EventTrigger>

まとめ

この記事では、Windows Presentation Foundation アプリケーションにオーディオとビデオを統合する際に使用できる方法について調査しました。Windows Presentation Foundation ではプロジェクトに必要なレベルの制御を行えない場合は、いつでも、メディア統合向けの DirectShow API の使用に戻ることができます。ただし、XAML を使ってオーディオとビデオを追加する機能によって、デザイナが簡単にメディア統合を処理できるようになるので、開発者はより複雑なコーディング作業に力を注ぐことができます。私は、かつてのように懐疑的ではなくなりました。Windows Presentation Foundation で利用できる驚くほど豊富なビジュアル機能により、メディア統合が単なる可能性ではなくなり、信じられないほど魅力的なものになります。

Lee Brimelow は、frog design 社の Senior Design Technologist で、Flash と WPF を専門とする受賞歴のあるインタラクティブ デザイナです。また、www.thewpfblog.com (英語) で WPF テクノロジに関する人気のあるブログを運営しています。


WiX の秘訣: MSBuild と Windows Installer XML を使ったリリースの自動化

WiX の秘訣

MSBuild と Windows Installer XML を使ったリリースの自動化

Sayed Ibrahim Hashimi
この記事の内容 : :
  • Windows Installer XML の概要
  • WiX パッケージ化の指示を作成する
  • WiX と MSBuild を統合する
  • ビルドとパッケージ化を自動化する
この記事は次のテクノロジを使用しています:
Visual Studio、Windows Installer XML (WiX)、MSBuild
コードのダウンロード : : WiX2007_03.exe (498KB)
コンテンツ
WiX の概要
WiX ファイルを作成する
WiX と MSBuild
MSBuild のバッチ処理
ビルドとパッケージ化を自動化する
MSBuild スクリプトの詳細
インストーラ作成方法のまとめ
プロセスをカスタマイズする
まとめ

開発工程の中で、ビルド プロセスを自動化することは重要です。

また、リリースを行う手段を自動化することも同じく重要です。残念なことに、多くの組織、特に小規模組織では、リリースは自動化されていません。リリース直前になってリリース物が作り上げられているのが一般的です。しかし、少し時間を取って、ビルドとリリースを自動化する計画を考案すると、多くの時間が節約され、その時間をプロジェクトのビルドとリリース以外の作業に充てることができます。

この記事では、Microsoft® Build Engine (MSBuild) と Windows® Installer XML (WiX) のツールセットを使用して、組織のビルドとリリースのプロセスを自動化し、繰り返し使えるプロセスにする方法を示します。この記事は WiX v2 に対応しています (WiX v3 がリリースされると、構文サンプルの一部をそのまま使用できない場合もあることに注意してください)。WiX はリリースを作成するプロセスを単純にしますが、WiX を使用するかどうかにかかわらず、リリースを作成する場合、ここで説明する技法が関係してきます。また、ここで説明する技法の一部を変更すれば、Microsoft .NET Framework 2.0 を使用しないで開発されたアプリケーションにも当てはめることができます。

この記事は MSBuild に精通していることを前提としています (記憶を新たにするには、MSDN® マガジン 2006 年 6 月号の記事「インサイド MSBuild: Microsoft ビルド エンジンのカスタム タスクを使用した、お好みの方法でのアプリケーションのコンパイル」を参照してください)。ここでは、WiX ツールセットを使い慣れていない方のためにその概要を示します。関連する記事やツールのリファレンスについては、「MSBuild と WiX リソース」のリンクを参照してください。この記事では、説明の目的で自作の Sedodream MSBuild プロジェクトを使用します。最新のソースは、www.codeplex.com/Sedodream (英語) から入手できます。

WiX の概要

通常、アプリケーションを作成する工程の最後には、運用コンピュータにそのアプリケーションをインストールして実行することになります。この最後の工程に WiX ツールセットが役に立ちます。ここでは WiX について説明し、WiX を使用してインストーラを作成する方法を紹介します。

WiX では、インストールするアプリケーションがインストール先のコンピュータでどのように見えるかを記述します。Windows インストーラは、Visual Studio® から Microsoft Office まで、マイクロソフトの多くのアプリケーションで使用されています。WiX が、実は sourceforge.net でホストされているオープン ソース プロジェクトであるとわかると驚かれるかもしれません。WiX のバイナリとソースは、wix.sourceforge.net (英語) からダウンロードできます。WiX をダウンロードしてインストールすると、図 1 にまとめたように、多くの実行可能ファイルがインストールされるのがわかります。ここでは、Candle.exe ツールと Light.exe ツールの使い方に注目します。

Figure 1 WiX コンポーネント (Close Figure 1)

名前 | 説明
Candle.exe | WiX ソース ファイルを中間表記に変換します。実際には別の XML ファイルに生成されますが、ここで生成されたファイルを手作業で編集しないでください。
Dark.exe | MSI ファイルを適切な WiX ソース ファイルに変換します。インストーラの "逆コンパイル" と考えることができます。
Light.exe | WiX ソース ファイルの中間表記から Windows インストーラを生成します。
Lit.exe | 他のインストーラ パッケージのビルドに使用できる WiX ライブラリを生成します。
Tallow.exe | インストール ディレクトリまたはファイル内のファイルやフォルダを複製するための WiX ソース XML の作成に使用します。
WixCop.exe | WiX ソース ファイルで問題が生じる可能性のある領域をチェックします。FxCop と同様です。

WiX は、手続き型の言語ではなく、宣言型の言語を使用します。つまり、WiX はアプリケーションをインストールするために実行する必要のある手順ではなく、インストールしたアプリケーションがどのように見えるかを記述します。これまで使い慣れた方法とは異なるかもしれませんが、理解するのが驚くほど容易であることがわかります。通常、WiX ソース ファイルには、インストール先のコンピュータにインストールするファイルを記述します。ここでは、こうしたコンポーネントに重点を置いて説明します。

WiX ソース ファイルには、インストール予定のファイルに関連する主な要素として、ファイル、コンポーネント、および機能の 3 つがあります。ファイル要素は、個別のファイルへの参照です。ファイルはインストールの最小単位で、コンポーネントに含まれていなければなりません。つまり、コンポーネントに 100 個のファイルが含まれている場合、そのコンポーネントをインストールすると、そのコンポーネントに含まれているすべてのファイルがインストールされます。逆に、そのコンポーネントをインストールしなければ、コンポーネントに含まれているファイルは一切インストールされません。多くのファイルを含むコンポーネントを作成することはお勧めしません。

コンポーネントは常に機能要素に含まれ、同じコンポーネントを複数の機能に含めることができます。機能は一連のコンポーネントで、一連のサブ機能とも考えられます。使用するインストーラに、インストールするアイテムをユーザーが選択できるグラフィカル インターフェイスがある場合、実際にはユーザーは機能を選択していることになります。

WiX ファイルを作成する

WiX ソース ファイルは、任意のテキスト エディタまたは XML エディタで作成できます。また、Visual Studio を使用すれば、IntelliSense® を利用して WiX ソース ファイルを作成できます。WiX ソース ファイルを編集する際に IntelliSense が有効にならない場合は、wix.xsd ファイルを Visual Studio スキーマ ディレクトリにコピーするだけで有効になります。通常、このディレクトリは %Program Files%\Microsoft Visual Studio 8\Xml\Schemas にあります。また、WiX ソース ファイルを作成するときに、多くの GUID を生成することも必要になります。Visual Studio にはこの目的に使用できるツールがあります。また、Visual Studio マクロも使用できるので、新しい GUID を直接挿入するためにショートカットを割り当てることができます。

では、新しい WiX ソース ファイルを作成しましょう。最初の要素は、必ず Wix 要素になります。Wix 要素の子要素には、Product、Fragment、Module、および PatchCreation があります。どの要素を使用するかは、目的の出力の種類によって決まります。ここでのプロジェクトの最終目的はインストーラ データベース (MSI) なので、Product 要素を使用します。サンプル プロジェクトの WiX v2 ソース ファイル (Sedodream.wxs) の先頭部分を以下に示します。

<?xml version='1.0' encoding='UTF-8'?>
<Wix xmlns='http://schemas.microsoft.com/wix/2003/01/wi'>
   <Product Name='Sedodream MSBuild Project'
        Id='C9D25926-FCE0-4EB6-8FF5-4686EE5AB089'
        Language='1033' Codepage='1252' Version='1.0.0'
        Manufacturer='SedoTech'
        UpgradeCode='9C5E4073-EFDE-419B-935D-CE2632BC560E'>

      <Package Id='????????-????-????-????-????????????'
         Keywords='Installer'
         Description='Sedodream MSBuild Project Installer'
         InstallerVersion='100' Languages='1031'
         Compressed='yes' SummaryCodepage='1252' />
...

ご覧のとおり、Product 要素には多くの属性がありますが、最も重要な属性は Id と Name です。Id を使用して、製品を一意に識別します。新しいメジャー リリースの場合は、これを変更する必要があります。この値は、後から参照するために書き留めておきます。Name 属性の値は、コントロール パネルの [プログラムの追加と削除] に表示されます。また、インストーラに UI があれば、この名前が最初のページに表示されます。

ここには、Product 要素の最初の子要素として Package 要素も示しています。この要素では Id 属性の GUID の形式に一連の疑問符文字 (?) が含まれていることに注目してください。この構文を使用して、Id をビルド時に生成することを指定します。インストーラを作成するときは、作成するインストーラごと、さらには毎回のビルドごとに異なる Package Id が必要です。これは GUID を自動生成する場合のみで、他の場合はすべて同じ GUID を使用する必要があり、後で参照するために記録しておきます。

ほぼすべてのインストーラは少なくとも 1 つのファイルをインストール先のコンピュータに配置しますが、ここで説明するインストーラは違います。WiX は宣言型方式を使用するので、ここではディレクトリ構造と、その構造をインストール先のコンピュータに再作成することを宣言します。これは、以下のように一連の Directory 要素を使用して行います。

<Media Id='1' Cabinet='Sedodream.cab' EmbedCab='yes'/>

<Directory Id='TARGETDIR' Name='SourceDir'>
   <Directory Id='ProgramFilesFolder' Name='PFiles'>
      <Directory Id='MSBuildDir' Name='MSBuild'>
         <Directory Id='INSTALLDIR' Name='Sedodrea'
          LongName='Sedodream'>

このサンプルの Media 要素では、キャビネット ファイルを生成して、インストーラ データベースに配置することを指定します。これは必須要素で、インストールを複数のメディアに展開する場合は、この要素を複数含めることができます。たとえば、アプリケーションを CD で配布する場合は、複数枚のディスクに展開する必要がありますが、ほとんどの場合、このような問題を心配する必要がなくなります。

Media 要素の後には、一連の Directory 要素があります。最初の Directory 要素は、単に他のエントリをカプセル化する仮想要素です。TARGETDIR Directory 要素の下のエントリの Id 値は ProgramFilesFolder です。ご想像どおり、これはよく知られた場所で、Windows インストーラの起動時に値が設定されます。使用できる他のシステム フォルダの完全な一覧については、msdn.microsoft.com/library/en-us/msi/setup/system_folder_properties.asp (英語) を参照してください。こうしたシステム プロパティは、常に、完全なパスに解決されます。

次の要素では、カスタム ディレクトリの名前を指定します。ディレクトリには、可能であれば LongName 属性を使用して名前を付けますが、可能でなければ Name 属性を使用します。指定したディレクトリが存在しない場合は、Windows インストーラによってインストール時に適切なディレクトリが作成されます。上記のコードでは完全なツリーが宣言され、結果は C:\Program Files\MSBuild\Sedodream になります。

ディレクトリ構造を宣言したら、いよいよコンポーネント宣言を作成します。前述のように、インストールできる最小単位はコンポーネントです。コンポーネントは、ファイル、ショートカット、レジストリ キー、証明書など、多種多様なアイテムから構成できます。コンポーネントは、相互に無関係になるようにデザインします。つまり、コンポーネントをインストールまたはアンインストールするときに、他のコンポーネントに悪影響を及ぼさないようにします。本質的には、アイテムは 1 つのコンポーネントのみに含める必要があります。そのようになっていない場合は、コンポーネントの編成方法を再検討します。コンポーネントにファイルを 1 つしか含まないこともよくあります。

各コンポーネントの宣言に、Id 属性と GUID 属性が必要です。Id はコンポーネントの参照に使用する名前で、GUID は Windows インストーラが使用する一意の識別子です。コンポーネントには、同じ GUID を使用しないことが極めて重要です。

図 2 に、サンプル WiX ソース ファイルのコンポーネントを示します。4 つの File 要素と 2 つの XmlFile 要素が含まれているのがわかります。File 要素は、単にインストール先のデバイスに配置する必要のあるファイルへの参照です。File 要素には多種多様の属性を指定できますが、よく使用されるのは図 3 に示すように、Id、Name、LongName、Source、および DiskId です。.

Figure 3 ファイル属性 (Close Figure 3)

属性名 | 説明
Id | ファイルの参照に使用できる識別子。
Name | 8.3 形式のファイルの短い名前。
LongName | ファイルの長い名前。対象のコンピュータで長いファイル名がサポートされる場合は、この名前を使用します。サポートされない場合は、Name 値が使用されます。
Source | インストーラ データベースを作成しているコンピュータでのファイルへの相対パス。
DiskId | このファイルが含まれる Media アイテムの識別子。
Figure 2 コンポーネントを定義する (Close Figure 2)
<Component Id='TaskBinFiles' Guid='38736E2E-BEB7-48A9-A2B3-138A57A69D45'>
   <File Id='MSBCommonDLL' Name='CommoDLL'
      LongName='Sedodream.MSBuild.Common.dll'
      Source='Sedodream.MSBuild.Common.dll' Vital='yes' DiskId='1'/>
   <File Id='SedodreamLoggersDLL' Name='Logger'
      LongName='Sedodream.MSBuild.Loggers.dll'
      Source='Sedodream.MSBuild.Loggers.dll' Vital='yes' DiskId='1'/>
   <File Id='SedodreamTasksDLL' Name='Tasks'
      LongName='Sedodream.MSBuild.Tasks.dll'
      Source='Sedodream.MSBuild.Tasks.dll' Vital='yes' DiskId='1'/>

   <File Id='SedodreamTASKS' Name='SeTasks' LongName='Sedodream.tasks'
      Source='Sedodream.tasks' Vital='yes' DiskId='1'/>
   <!-- Merge in the Xsd for the custom tasks we have created to have
      IntelliSense -->
   <XmlFile Id='SedoSche'
      File='C:\Program Files\Microsoft Visual Studio 8\
               Xml\Schemas\1033\Microsoft.Build.xsd'
      Action='createElement' ElementPath='//xs:schema'
      Name='xs:include' Permanent='no' Sequence='0'/>
   <XmlFile Id='SedoSch2'
      File='C:\Program Files\Microsoft Visual Studio 8\
               Xml\Schemas\1033\Microsoft.Build.xsd'
      Action='setValue'
      ElementPath='//xs:schema/xs:include[\[]not(@schemaLocation)[\]]'
      Name='schemaLocation' Value='MSBuild\Sedodream.MSBuild.xsd'
      Permanent='no' Sequence='1' />
</Component>

XmlFile 要素は、インストール先コンピュータ上の既存の XML ファイルを実際に変更します。これは新しい要素を Microsoft.Build.xsd ファイルに挿入し、サンプル プロジェクトに含まれるカスタム MSBuild タスク用に IntelliSense を有効にします。

機能は、インストーラが UI を表示する場合に、インストール時にユーザーが選択する項目です。以下に、サンプル プロジェクトの Feature 宣言を示します。

<Feature Id='Complete'
   Title='Sedodrem Core' Description='MSBuild libraries'
   Display='expand' Level='1' ConfigurableDirectory='INSTALLDIR'>
   <ComponentRef Id='TaskBinFiles'/>
   <ComponentRef Id='NUnitFiles'/>

   <Feature Id='Samples' Title='Samples'
      Description='Contains samples of Sedodream usage'
      Display='expand' Level='100' ConfigurableDirectory='INSTALLDIR'>
      <ComponentRef Id='Samples'/>
   </Feature>
</Feature>

他の多くの WiX 要素と同様に、Feature 要素には多くの属性があり、よく使用される属性をいくつか図 4 に示します。

Figure 4 機能属性 (Close Figure 4)

属性名 | 説明
Id | 機能の参照に使用できる識別子。
ConfigurableDirectory | プロパティからのインストール場所の設定を可能にします。これは、UI を使ってユーザーが決定できます。
Description | 機能の説明。UI 上でユーザーに表示されます。
Level | 機能のインストール レベルを指定します。値ゼロ (0) は、その機能がインストールされないことを示します。ゼロ以外の値で、INSTALLLEVEL 以下の値であれば、その機能はインストールされます。
Title | 機能のタイトル。UI を使用する場合、この値が機能ツリーに表示されます。

この WiX では、主要機能 Complete に、2 つのコンポーネントへの参照と、サブ機能が含まれています。サブ機能参照は、子 Feature 要素によって実現されます。WiX ソース ファイルには、必要に応じて多くのサブ機能を含めることができます。コンポーネント参照には、 ComponentRef 要素を使用します。ComponentRef 要素を使用するときは、参照を試みている Component 要素で使用しているのと同じ ID 値を使用する必要があります。

WiX と MSBuild

WiX v2 の最新のリリースには、wix.targets ファイルがあります。このファイルには、MSBuild.からの WiX リリースの作成を支援する定義が含まれています。WiX チームは、wix.targets ファイルから参照される一連の MSBuild タスクを作成しました。このようなタスクは、すべて WiXTasks.dll アセンブリに含まれています。これらを図 5 にまとめました。

Figure 5 WiX 用 MSBuild タスク (Close Figure 5)

名前 | 説明
Candle | WiX コンパイラを呼び出して、WiX ソース ファイルの中間表記を作成します。
Lit | 中間表記から WiX ライブラリを作成します。
Light | WiX リンカを呼び出して、中間表記から最終インストーラを作成します。

インストーラをビルドする方法は、現在、マネージ プロジェクトをビルドする方法に非常によく似ています。たとえば、C# プロジェクトを作成するときに、プロジェクト ファイルにはプロジェクトのビルドに必要なプロパティやアイテムがすべて含まれますが、プロジェクトのビルド手順は Microsoft.CSharp.targets ファイルに含まれます。このファイルは、MSBuild Import 要素を使ってプロジェクト ファイルに含めます。WiX の場合は、プロジェクト ファイルで必要なすべてのプロパティとアイテムを定義し、その後 wix.targets ファイルを含めて、最終パッケージをビルドする方法に関する計画を定義します。図 6 に、wix.targets ファイルを使用して正しくインストーラをビルドするための必要最低限の宣言を一覧します。

Figure 6 インストーラのビルドに必要な宣言 (Close Figure 6)

名前 | 説明
ToolPath | WiX インストール ディレクトリへのパスを保持します。ここに wix.targets ファイルが存在します。
OutputName | 作成する出力のパスと名前。この名前は、中間表記のファイルと最終パッケージ ファイルに使用するので、拡張子は含めません。このプロパティは、Candle.exe と Light.exe の OutputFile 引数に似ています。
OutputType | 最終ファイルの種類を表します。この値は、package、module、library、および object にできます。この値によって、Light タスクまたは Lit タスクを呼び出すかどうか、および作成する最終ファイルの拡張子が決まります。ここではインストーラを作成するので、このプロパティの値に package を指定しています。
Compile | コンパイルする WiX ソース ファイルを保持する MSBuild アイテム。
BaseInputPath | ソース ファイルの入力パスとして機能します。技術的には必須ではありませんが、ほとんどの場合に使用します。指定しない場合は、適切なディレクトリから msbuild.exe が呼び出されることを確認する必要があります。

インストーラを作成するには、必要なプロパティとアイテムを定義する MSBuild ファイルを作成する必要があります。ここでは、このファイルに SedodreamMSI.wproj という名前を付けます。このファイルを図 7 に示します。

Figure 7 MSBuild ファイルの SedodreamsMSI.wproj (Close Figure 7)
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Build">
  <!--===============================================================
    These must be declared BEFORE the statement
    that imports the wix.targets file
   ==================================================================-->
  <PropertyGroup>
    <!-- The location pointing where WiX is installed -->
    <ToolPath>C:\Data\Development\WiX\</ToolPath>
    <!-- Required Property by WiX -->
    <OutputName Condition="$(OutputName)==''" >
      $(Configuration)Sedodream</OutputName>
    <!-- Required property by WiX -->
    <OutputType
      Condition="$(OutputType)==''" >package</OutputType>
    <!-- Input path to source files  -->
    <BaseInputPath  Condition="$(BaseInputPath)==''">
      $(PackageRoot)$(Configuration)</BaseInputPath>
  </PropertyGroup>

  <ItemGroup>
    <!-- Required WiX item.
      Files in this item are sent to the Candle tool.
    -->
    <Compile Include="$(BaseInputPath)\Sedodream.wxs"/>
  </ItemGroup>

  <Import Project="$(ToolPath)wix.targets"/>
</Project>

ご覧のように、この MSBuild ファイルにはそれほど多くのアイテムは含まれておらず、プロパティがいくつかとアイテムが 1 つ定義されているだけです。ファイルは、WiX ソース ファイル (Sedodream.wxs) が既に作成されていて、BaseInputPath ディレクトリに存在していることを前提としています。また、WiX ファイルでもファイルが配置される場所に関していくつか想定を行っています。ここでは、すべてのファイルが必要な場所に存在しているものとしましょう。

インストーラをビルドするには、プロジェクト ファイルで MSBuild を呼び出す必要があります。Visual Studio 2005 コマンド プロンプトを開き、ファイルが存在する場所に移動して、以下のコマンドを実行します。

msbuild Sedodream.wproj

このコマンドの出力を図 8 に示します。

実行するターゲットを定義しなかったので、DefaultTargets が実行されました。これは、Sedodream.wproj ファイルの Build に定義されています。出力からは、bin\Release フォルダに ReleaseSedodream.msi というファイルが作成されたことがわかります。この時点でインストーラを実行して、期待どおりに機能することを確認できます。この記事の主な目的に向けて話しを進める前に、次のセクションで、MSBuild の高度なトピックのバッチ処理について説明します。製品とそのリリースをビルドするプロセス全体でバッチ処理を使用するので、この説明が必要です。

図 8 MSBuild からの WiX インストーラ作成の出力
図 8 MSBuild からの WiX インストーラ作成の出力(画像を縮小するには、ここをクリックします)
図 8 MSBuild からの WiX インストーラ作成の出力
図 8 MSBuild からの WiX インストーラ作成の出力 (画像を拡大するには、ここをクリックします)

MSBuild のバッチ処理

MSBuild を使用していると、ループ処理を行う構造がないことに気付きます。ループの代わりにバッチ処理を利用できます。バッチ処理では、アイテム メタデータを使用して、アイテムをバッチやバケットと呼ばれるさまざまなカテゴリに分割します。バッチは、1 つ以上のアイテムで構成されます。アイテムを分割すると、各バッチを繰り返すことができます。バッチは、マネージ プロジェクトのビルド プロセス全体で使用されます。バッチ処理のカテゴリには、大きくタスク バッチ処理とターゲット バッチ処理の 2 つがあります。タスク バッチ処理では、1 つのタスクを実行するときに、MSBuild エンジンによって作成が必要なバケットが判断され、これらのバケット経由でタスクが実行されます。タスク バッチ処理を使用するには、タスクにアイテムのメタデータを提供する必要があります。

バッチ処理の例を示すために、次のようなシナリオに直面しているとします。ある場所から 1 つ以上の別の場所に一連のファイルをコピーする必要があり、これには次の 2 つの選択肢があります。1 つは場所別にコピー タスクを使用して各場所にファイルをコピーする方法です。もう 1 つはバッチ処理を使用して、1 つのコピー要素でこれを実現する方法です。最初の方法は単純ですが、メンテナンスが困難になるため、適切なソリューションではありません。2 番目の方法を説明するために、次に示す非常に簡単な MSBuild ファイル (batching01.proj) を見てみましょう。

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
         DefaultTargets="CopyFiles">
  <ItemGroup>
    <SourceFiles Include="*.txt"/>
    <Dest Include="One;Two;Three;Four;Five"/>
  </ItemGroup>

  <Target Name="CopyFiles">
    <Copy SourceFiles ="@(SourceFiles)"
         DestinationFolder="%(Dest.FullPath)"/>
    <Message Text="Fullpath: %(Dest.FullPath)"/>
  </Target>
</Project>

このファイルでは、1 つのアイテム SourceFiles を宣言して、.txt で終わるすべてのファイルを含め、Dest アイテムも宣言して、5 つの場所を含めています。ターゲット CopyFiles では、Copy タスクを使用して、SourceFiles を各場所にコピーします。FullPath メタデータを Copy タスクに渡しているので、MSBuild エンジンによって、個別の FullPath 値を備えた Dest アイテムをすべて含むバッチが作成され、バッチごとに Copy タスクが呼び出されます。これが、タスク バッチ処理です。これを実行する場合、batching01.proj ファイルが存在するディレクトリから以下のコマンドを呼び出すことができます。

msbuild.exe batching01.proj

ここで、ターゲット バッチ処理を使用して同じ結果を得る方法を示します。ターゲット バッチ処理の場合、バッチはターゲットの Inputs と Outputs を基に作成されます。そのため、これらの値にアイテムのメタデータを渡す必要があります。

<Project xmlns="http://schemas.
  microsoft.com/developer/msbuild/2003"
         DefaultTargets="CopyFiles">
  <ItemGroup>
    <SourceFiles Include="*.txt"/>
    <Dest Include="One;Two;Three;Four;
                   Five"/>
  </ItemGroup>

  <!-- These targets demonstrate target
       batching -->
  <Target Name="CopyFiles"
          Inputs="@(SourceFiles)"
          Outputs="%(Dest.FullPath)">
    <Copy SourceFiles="@(SourceFiles)"
          DestinationFolder=
          "%(Dest.FullPath)"/>
  </Target>
</Project>

ターゲット バッチ処理は、ターゲットの Inputs と Outputs から駆動されます。Outputs には Dest.FullPath 宣言が含まれているので、ターゲットはバケット単位に呼び出されます。これは、図 9 に示すように、ターゲット全体のコンテンツがバッチ単位に繰り返されることを除けば、タスク バッチ処理と似ています。

図 9 ターゲット バッチ処理の MSBuild 出力
図 9 ターゲット バッチ処理の MSBuild 出力(画像を縮小するには、ここをクリックします)
図 9 ターゲット バッチ処理の MSBuild 出力
図 9 ターゲット バッチ処理の MSBuild 出力 (画像を拡大するには、ここをクリックします)

この出力からおわかりのように、ターゲットの CopyFiles は Dest アイテムのバッチごとに呼び出されています。ビルドとパッケージ化のプロセスを自動化する例では、構成 Platform と Flavor の組み合わせごとにインストーラを作成するために、バッチ処理を使用します。

ビルドとパッケージ化を自動化する

ビルドとパッケージ化に必要な手順は、使用するテクノロジや組織の要件などにより、組織ごとに微妙に異なります。ここでは、多くの組織に等しく必要だと考えられる一連の手順にまとめました。ただし、特定のシナリオによっては、若干の微調整が必要になるかもしれません。ビルドとパッケージ化のプロセスを自動化する目的は 2 つあります。1 つは、繰り返し使える共通のビルド プロセスを作成することです。もう 1 つは、自動化され、繰り返し使えるパッケージ化プロセスを作成することです。主な手順には、最新ソースの入手、ビルド、パッケージ化という 3 つがありますが、こうした手順は図 10 に示すように、それぞれ細かい多くの手順に分解されます。

図 10 ビルド プロセス
図 10 ビルド プロセス(画像を縮小するには、ここをクリックします)
図 10 ビルド プロセス
図 10 ビルド プロセス (画像を拡大するには、ここをクリックします)

中でもソースにラベルを付けることは、非常に重要な手順です。ソースにラベルがないとプロセスを繰り返すことができないので、この手順が必要になります。運用環境に送り出すビルドを作成する際は、配置済みのコードをメンテナンスするために、まったく同じビルドを作成できる必要があります。この手順は、ソース管理プロバイダに使用するツールによって異なります。さまざまなソース管理プロバイダ向けにこれを実現する多種多様な MSBuild タスクをダウンロードして入手できます。詳細については、「MSBuild と WiX リソース」のリンクを参照してください。

MSBuild スクリプトの詳細

ここでは、サンプル アプリケーションのビルドとパッケージ化に使用できる MSBuild スクリプトについて説明します。全体的な構造は、マネージ プロジェクトをビルドする方法に非常によく似ています。つまり、処理内容を含むプロジェクト ファイルを定義し、プロセス フローを定義する別のプロジェクト ファイル (Sedodream.Package.targets) をインポートします。これらのプロセスを複数の文と考えてください。パッケージ化のプロジェクト ファイルでは名詞を定義し、インポートされるファイルでは動詞を定義します。ここでは、ソリューション ファイルで MSBuild タスクを使用することで、プロジェクトをビルドします。インストーラをビルドするには、前述のプロジェクト ファイル (SedodreamMSI.wproj) を使用します。

独立した複数の要素から成るプロセスがあるときは、それらを何らかの方法でまとめる必要があります。これは、プロパティ、アイテム、およびターゲットの共通のセットを定義することで行います (図 11 参照)。

Figure 11 Connect 要素に使用するプロパティとアイテム (Close Figure 11)

名前 | 説明
SolutionFilePath | 製品のビルドに使用するソリューション ファイルの場所を定義するプロパティ。
PackageRoot | プロジェクトのビルド先を定義し、WiX の一時ビルド場所として機能するプロパティ。
WiXSourceFiles | コンパイル用に Candle ツールに送信されるすべての WiX ファイルを保持するアイテム。
AllConfigurations このプロセスでビルドするためのすべての構成を保持するアイテム。各構成には、FlavorToBuild と PlatformToBuild の両方のメタデータ値を含みます。以下に宣言の例を示します。
<AllConfigurations Include="Release|Any CPU"> <FlavorToBuild>Release</FlavorToBuild> <PlatformToBuild>Any CPU</PlatformToBuild> </AllConfigurations>
OtherFiles パッケージ ディレクトリにコピーされるその他のファイルを保持する、省略可能なアイテム。これらのアイテムは Destination メタデータを含みます。このメタデータは、コピー先のパスをパッケージ ルートへの相対パスで定義します。以下に宣言の例を示します。
<OtherFiles Include= "..\Sedodream.MSBuild.Tasks\SampleTargets\**\*"> <Destination>Samples\</Destination> </OtherFiles>

このプロセスでは、ソリューション ファイルをビルドし、WiX ソース ファイルを同じディレクトリにコピーします。このパスに留意して WiX ソース ファイルを定義してください。ソース管理で WiX ファイルが含まれる場所とは無関係に、製品のビルド先と同じディレクトリに WiX ファイルがビルドされます。必要な他のファイルも CopyFilesForPackaging ターゲットによって同じディレクトリにコピーされます。

また、このプロセスでは、PackageRoot プロパティを使用して、プロジェクトのビルド先の場所および WiX がインストーラをビルドする場所も定義します。サンプルでは、このディレクトリはソリューション ファイルと同じレベルですが、ビルド コンピュータの任意の場所に設定できます。ただし、この場所にネットワーク共有を使用することはお勧めしません。

メイン ターゲットは、図 12 に示すように、Sedodream.Package.targets ファイル内で宣言します。これらのすべてのターゲット (およびターゲット ファイル内の多くの他のアイテム) は、プロパティで依存関係が定義されるので、プロセス内のイベントのシーケンスを完全に変更することができます。たとえば、プロジェクト ファイルで Build ターゲットを実行する前に手順を挿入する必要がある場合、Sedodream.Package.targets ファイルの Import ステートメントの後に次のコードを挿入するだけです。

<BuildDependsOn>
    CustomBeforeBuild;
    $(BuildDependsOn);
</BuildDependsOn>

Figure 12 サンプル プロジェクトでのビルド ターゲット (Close Figure 12)

名前 | 説明
Package | DefaultTargets リスト内唯一のターゲット。プロセス全体を実行するために呼び出されます。
Build | ソリューション ファイルをビルドします。MSBuild タスクの呼び出し時に OutputPath プロパティをオーバーライドすれば、このビルドの出力を適切なディレクトリにルーティングできます。
CopyFilesForPackaging | ビルド ディレクトリに組み込まれていなくても必要なファイルをすべてコピーします。
DeployPackage | プロセスの最後近くで呼び出されます。サンプルではこのターゲットは空ですが、実装では、このターゲットをオーバーライドして、パッケージを QA ゲートや配置チームに受け渡すことができます。
Clean | 他のターゲットによって生じた混乱状態をクリーンアップします。

BuildDependsOn プロパティが再定義されたので、既存のタゲット ファイルを変更しなくても、現在のプロセスに CustomBeforeBuild 手順が効果的に挿入されます。この問題の詳細については、以前に執筆した 2006 年 6 月の記事 (前述) を参照してください。

インストーラを作成する前に、ソリューションをビルドする CoreBuild ターゲットを少し見てみましょう (図 13 参照)。これは、定義済みの構成ごとにソリューションをビルドするためにターゲット バッチ処理を使用する方法を示しています。

Figure 13 CoreBuild ターゲット (Close Figure 13)
<Target Name="CoreBuild"
   Inputs="%(AllConfigurations.PlatformToBuild);
           %(AllConfigurations.FlavorToBuild)"
   Outputs="%(AllConfigurations.PlatformToBuild);
            %(AllConfigurations.FlavorToBuild)" >
   <Message Text="Building for Flavor/Platform:
       %(AllConfigurations.FlavorToBuild)/
       %(AllConfigurations.PlatformToBuild)"/>
   <MSBuild Projects="@(SolutionFile)"
      Targets="Build"
      Properties="OutputPath=$(PackageRoot)
         %(AllConfigurations.FlavorToBuild);
      Configuration=%(AllConfigurations.FlavorToBuild);
      Platform=%(AllConfigurations.PlatformToBuild)">
      <Output ItemName="OutputFiles" TaskParameter="TargetOutputs"/>
   </MSBuild>
</Target>

具体的には、まず、Inputs と Outputs が目に付きます。これらは AllConfigurations アイテムのメタデータ値で、これにより Platform/Flavor のペア単位にターゲットが実行されます。次に、適切なプロパティを使用して、ソリューション ファイルをビルドするための MSBuild タスクが呼び出されます。OutputPath は、出力を別のディレクトリにリダイレクトするため指定しています。

インストーラ作成方法のまとめ

ここまでは、WiX、いくつかの MSBuild の高度な概念、および MSBuild パッケージ スクリプトの構造を使用して、インストーラを作成する方法について説明してきました。サンプル プロジェクト用インストーラの作成に関して残っている問題は、必要なプロパティを定義する MSBuild プロジェクト ファイルの作成についてのみです。これは、驚くほど単純です。最後に、ニーズに合うようにプロセスをカスタマイズする方法を示します。

図 14 に、サンプル プロジェクトのプロセスを起動する MSBuild ファイル (Sedodream.Package.dproj) を示します。前述のように、このプロジェクト ファイルの主目的は、ビルドに必要なことを記述することです。最終製品を作成する方法の詳細については、すべてインポートされるファイル (Sedodream.Package.targets) に記載されているので、ビルド プロセスに手順を挿入することで、プロセスをカスタマイズできます。

Figure 14 Sedodream.Package.dproj MSBuild プロジェクト (Close Figure 14)
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
         DefaultTargets="Package">

   <!-- Required properties by the deployment.targets file -->
   <PropertyGroup>
      <SolutionFilePath>..\Sedodream.MSBuild.sln</SolutionFilePath>
      <PackageRoot>..\Package\</PackageRoot>
   </PropertyGroup>

   <ItemGroup>
      <WiXSourceFiles Include="Sedodream.wxs"/>
      <OtherFiles Include="..\Sedodream.MSBuild.Tasks\
         SampleTargets\**\*">
         <Destination>Samples\</Destination>
      </OtherFiles>
      <!-- Copy the license to the BaseSearchPath so it can
         be included into the installer -->
      <OtherFiles Include="License.rtf">
         <Destination></Destination>
      </OtherFiles>
      <!-- Copy the custom installer bitmaps -->
      <OtherFiles Include="bitmaps\dlgbmp.bmp">
         <Destination>bitmaps\</Destination>
      </OtherFiles>
   </ItemGroup>

   <!-- Define all the configurations that you want to build here -->
   <ItemGroup>
      <AllConfigurations Include="Debug|x86">
         <FlavorToBuild>Debug</FlavorToBuild>
         <PlatformToBuild>Any CPU</PlatformToBuild>
      </AllConfigurations>
      <AllConfigurations Include="Release|Any CPU">
         <FlavorToBuild>Release</FlavorToBuild>
         <PlatformToBuild>Any CPU</PlatformToBuild>
      </AllConfigurations>
   </ItemGroup>

   <PropertyGroup>
      <DropLocation>C:\Data\Drops\MSBuild-Wix\</DropLocation>
   </PropertyGroup>

   <!-- Import the deployment target to do all the work for us -->
   <Import Project="Sedodream.Package.targets"/>
</Project>

このプロジェクト ファイルには、必要な SolutionFilePath プロパティが定義されているのがわかります。これは、製品のビルドに使用するソリューション ファイルへのパスです。また、インストーラのコンパイルに使用する一連のファイルを含む、WixSourceFiles アイテムの宣言もあります。これらのファイルは、コンパイルのために Candle.exe ツールに渡されます。

この宣言と共に OtherFiles アイテムの宣言もあります。この宣言には、インストーラの作成に使用する場所にコピーされるファイルを含みます。これらのファイルは、インストーラの作成に使用する BaseInputPath にコピーされます。Destination メタデータ値を使用すれば、そのパスの下の任意の場所にファイルをコピーできます。この例では、インストーラの生成に使用するカスタム ビットマップを bitmaps フォルダにコピーしています。MSI ファイルを作成するために Light.exe ツールが呼び出されるときに、ローカル ディレクトリにあるすべての既定ファイルの前に、このディレクトリにあるファイルが選択されます。

DropLocation プロパティの宣言に注意してください。このプロパティを宣言すると、PackageRoot 内のすべてのファイルが DropLocation 内のディレクトリにコピーされます。すばやく変更を加える必要がある場合は、バイナリやインストーラ ファイルが 1 か所にあると便利です。

サンプル プロジェクト用に実行されるビルドとパッケージを参照するには、Visual Studio 2005 コマンド プロンプトを開き、プロジェクトの配置フォルダに移動します。続いて、次のコマンドを実行します。

msbuild SedodreamPackage.dproj

コマンド プロンプトに多くの出力が生成されます。出力では、Package ディレクトリの下に Debug と Release の 2 つのディレクトリがあるのがわかります。これらの各ディレクトリには、生成されたインストーラとその関連ファイルが含まれます。

プロセスをカスタマイズする

製品用にこのプロセスをセットアップするときは、SedodreamPackage.dproj ファイルと同様のファイルを作成する必要があります。作成したファイルの内部に、カスタマイズした内容を直接配置できます。必ずしも、Sedodream.Package.targets ファイル自体を変更する必要はありません。手順を挿入する場合は、前述したプロセスに従うことができます。ターゲットを再定義する場合は、Import ステートメントの後でターゲットを再定義することで、単純に以前の定義をオーバーライドすることができます。たとえば、Sedodream.Package.targets ファイルでは GetLatest ターゲットを定義していますが、このターゲットは空です。このターゲットに、リポジトリから最新のソースを取得するのに必要なタスクを配置します。以下に、プロジェクトがどのようになっているかの概要を示します。

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
       DefaultTargets="Package">
   <!-- Your properties and items defined here -->

   <!-- Import the deployment target to do all the work for us -->
   <Import Project="Sedodream.Package.targets"/>

   <!-- Place overriding customizations after this point -->

   <Target Name="GetLatest">
      <Message Text="Getting latest sources"/>
      <!-- Insert tasks to get latest from source control -->
   </Target>
</Project>

図 15 に、Sedodream.Package.targets ファイルで定義されている重要なターゲットの一覧を示します。これらのターゲットをニーズに合うようにカスタマイズできます。

まだ説明していないトピックは、このプロセスを自動化する方法です。これは、極めて簡単になりました。配置ファイル (ここでは SedodreamPackage.dproj) で MSBuild.exe を自動的に実行するだけです。これを行う方法は、現在共通のビルドの生成に使用している方法によって異なります。Visual Studio Team Foundation Server を使用している場合は、チーム ビルドを使用して実行できます。TFSBuild.proj ファイルでは、MSBuild タスクを使用できます。他のテクノロジを使用している場合、多くのテクノロジに MSBuild のネイティブ サポートが含まれています。こうした種類のテクノロジを採用していない場合は、Windows スケジューラを使用して、定期的に実行することができます。

まとめ

この記事では、WiX ツールセットに基づいて、ビルドとパッケージ化のプロセスを自動化する方法について説明しました。この統合を完了すると、信頼性の高い、繰り返し使える方法で製品のビルドとパッケージ化を行うことができます。このことは、配置先に送り出されるアプリケーションにとって非常に重要です。

WiX 以外のインストーラ テクノロジを使用する場合でも、一部を変更するだけで、ここで示した考え方やファイルをそのまま使用できます。私が知っているすべてのインストーラ テクノロジでは、何らかの形式のコマンドライン実行をサポートします。こうした他のテクノロジを使用してインストーラを作成する場合は、このようなコマンドライン実行を使用することになります。サードパーティ製のインストーラ テクノロジの中には、MSBuild タスクとターゲットが付属しているものもあります。このような場合は、ここで説明したプロセスを作り直すのが有効かもしれません。

MSBuild と WiX リソース
  • インサイド MSBuild: Microsoft ビルド エンジンのカスタム タスクを使用した、お好みの方法でのアプリケーションのコンパイル
  • Using the WiX Toolset to Integrate Setup into Your Development Process (英語)
  • WiX Project Home (英語)
  • Windows Installer SDK (英語)
  • rRob Mensching’s Blog (WiX Project Lead) (英語)
  • Justin Rockwood’s Blog (WiX Developer) (英語)
  • Sayed Hashimi’s Blog (英語)
  • MSDN MSBuild Forum (英語)
  • MSBuild Team Blog (英語)
  • MSBuild MSDN ドキュメント
  • MSBuild MSDN リファレンス
  • MSBuild WiKi (英語)

Sayed Ibrahim Hashimi は、フロリダ大学で学士号を得たコンピュータ エンジニアです。フロリダ州ジャクソンビルで開発者およびアーキテクチャとして活動する、金融、教育、債権回収の各分野の専門家です。彼は Latitude Software で .NET テクノロジに取り組んでいます。


ASP.NET 2.0: カスタム プロファイル プロバイダを使用した Web ユーザーの管理

ASP.NET 2.0

カスタム プロファイル プロバイダを使用した Web ユーザーの管理

Jason N. Gaylord
この記事の内容 : :
  • ASP.NET 2.0 プロファイル プロバイダ
  • カスタム プロバイダの作成
  • 認証と承認
この記事は次のテクノロジを使用しています:
ASP.NET、XML
コードのダウンロード : : ProfileProviders2007_03.exe (268KB)
コンテンツ
カスタム プロファイル プロバイダの使用
CreateUserWizard コントロールのカスタマイズ
登録プロシージャの作成
プロファイル バージョンのチェック
アプリケーション内での処理
参考情報

ASP.NET 2.0 では、コードを 1 行も書くことなく、Web サイトに認証、承認、およびプロファイルを追加できます。従来の方法に比べ、明らかに進歩しています。ASP.NET 2.0 の多くの新機能がそうであるように、認証、承認、およびプロファイルは、それぞれ独自のビルトイン プロバイダがあります。プロバイダとは、特定の作業に必要なメソッドを含んだモジュールと考えることができます。プロバイダは柔軟性に優れています。情報を渡す属性を指定してカスタマイズすれば、動作をさまざまに変更できます。しかし、カスタマイズを追求するなら、場合によってはビルトイン プロバイダをカスタムまたはサードパーティのプロバイダと差し替える必要があります。

ASP.NET のプロファイル機能を使用すると、ユーザー別の設定やデータを指定できます。そのデータはすべて匿名プロファイルに保存できるので、ユーザーがサイトにログインしなくても設定を取得できます。ただし、ユーザーがアカウントを作成した場合は、その設定をログイン プロファイルに移行できます。匿名プロファイル機能を無効にすることもできます。

ユーザーのプロファイル プロパティを保存したり取得したりするには、図 1 に示すように、プロファイル プロパティに名前を付け、web.config ファイルの profile 要素に追加します。こうすることで、プロファイル プロパティがプロファイルの SettingsPropertyValueCollection に格納されます。また、各プロパティのデータ型と既定値を指定できます。

Figure 1 web.config の profile セクション (Close Figure 1)
<profile>
  <properties>
    <add name="FirstName" />
    <add name="LastName" />
    <add name="Address1" />
    <add name="Address2" />
    <add name="City" />
    <add name="State" />
    <add name="Zip" />
    <add name="Phone" />
    <add name="ProfileVersion" type="int" defaultValue="0" />
  </properties>
</profile>

ASP.NET により web.config ファイルが読み取られ、profile セクション グループが検索されます。セクションが見つかると、型または接続文字列の属性 (図 1 にはありません) が検索されます。どちらも見つからない場合は、machine.config ファイルが検索されます。machine.config ファイルは、ASP.NET をインストールしたときから変更されていないシナリオがほとんどです。machine.config ファイルには、プロファイル機能の既定のプロバイダとして System.Web.Profile.SqlProfileProvider が既定で登録済みです。SqlProfileProvider は、プロファイル情報が格納されたデータベースへの接続に LocalSqlServer 接続文字列名を使用するように定義されています (すべての接続文字列の名前と属性は、web.config ファイルの connectionStrings というグループに格納されます)。LocalSqlServer 接続文字列は既定で SQL Server™ 2005 Express データベースとして定義されていて、このデータベースは |DataDirectory|aspnetdb.mdf に配置できます (ASP.NET では \App_Data\aspnetdb.mdb と変換されます)。

SqlProfileProvider によりプロファイル情報の格納に使用されるデータベースは、既知のスキーマで構成する必要があります。SQL Server 2005 Express データベースを使用している場合、データベース ファイルが動的に作成され、必要なテーブル、ビュー、ストアド プロシージャなどが適宜作成されます。また、データベースの作成を ASP.NET に任せず、Microsoft® .NET Framework SDK の aspnet_regsql ツールを使用することもできます。テーブルの 1 つ、aspnet_Profile テーブルは各ユーザーのプロファイル情報の保存に使用します。ユーザーが自分のプロファイルにプロファイル プロパティを指定すると、新しい行が追加されます。行の内容は、ユーザーの一意の UserID、各プロファイル プロパティの名前、および各プロファイル名に関連するデータが各列に格納されています。図 2 に、BLOB として保存されたそのデータを示します。プロパティの名前と値に使用できるデータ形式は、連続文字列、XML データセット、データをシリアル化したバイナリなど、さまざまです。その結果、ビルトイン プロファイル プロバイダでプロファイル データを保存すると、開発者やデータベース管理者にとって、SQL を使用するクエリまたはデータ変更が難しくなる場合があります。既定のプロバイダの機能がニーズに合わない場合、カスタム プロバイダを使用できます。

図 2 プロパティの名前と値
図 2 プロパティの名前と値(画像を縮小するには、ここをクリックします)
図 2 プロパティの名前と値
図 2 プロパティの名前と値 (画像を拡大するには、ここをクリックします)

カスタム プロファイル プロバイダの使用

ASP.NET チームは、ASP.NET の新バージョンを開発する間も、既存のバージョンの強化に励んでいます。チームが作成した便利なリソースは、通称サンドボックス プロジェクトと呼ばれていて、ASP.NET の公式 Web サイト sandbox.asp.net (英語) からダウンロードできます。ダウンロードできるプロジェクトの多くは、運用環境で使用できる点が特長です。ここでは、Web 展開プロジェクト、テーブル プロファイル プロバイダ サンプルなど、ASP.NET の優れたアドインを入手できます。今回のプロジェクトでは、asp.net/sandbox/samp_profiles.aspx?tabid=62 (英語) にあるテーブル プロファイル プロバイダ サンプルを使用します。

テーブル プロファイル プロバイダ サンプルは、ビルトインの ASP.NET プロファイル プロバイダとは異なり、人間が読みやすい形式でプロファイル データを保存できます。ダウンロードの内容は、2 つのサンプル プロバイダと説明書です。SqlTableProfileProvider は、データをプロファイル テーブルの個別の列に保存する機能があります。テーブルの必須要素は、主キーになる一意の識別子 UserID と、日時フィールド LastUpdatedDate です。UserID フィールドがあることで、aspnet_Membership テーブルおよび aspnet_Users テーブル内のユーザー アカウントに各プロファイル レコードを関連付けることができます。どちらのフィールドも、web.config ファイルで指定する必要はありません。もう一方のプロファイル サンプル SqlStoredProcedureProfileProvider は、プロファイル プロパティの各値を SQL ストアド プロシージャの個別のパラメータに渡します。その結果、ストアド プロシージャで、プロファイル プロパティの保存場所を指定したり、データを変換してから保存するビジネス ロジックを使用したりできます。

サンプルを ASP.NET プロジェクトで使用する前に、App_Code フォルダにサンプルのコードをコピーする必要があります。コードは C# で記述されているので、Visual Basic® でアプリケーションを開発している場合は、サンプルを App_Code フォルダの下の独立したフォルダに格納する必要があります。そのフォルダは、web.config ファイルの中で、次のようにしてコード サブディレクトリとして追加する必要があります。

<compilation debug="true" strict="false" explicit="true">
  <codeSubDirectories>
    <add directoryName="ProfileProvider" />
  </codeSubDirectories>
</compilation>

コード サブディレクトリとして追加したフォルダ内のすべての C# ファイルは、Web アプリケーション プロジェクトの他の部分とは別にコンパイルされます。実行時、アプリケーションの ASP.NET 一時フォルダに 2 つの DLL が生成されます。

このアプリケーションで SqlTableProfileProvider を使用するため、新しいテーブル CustomProfile を作成し、それぞれの列にユーザーの姓名、省略しない住所、電話番号、およびユーザー プロファイルの現在のバージョンを示す数字を格納します。テーブルには他に、主キーになるユーザーの一意の識別子、および最終列に最終更新日を格納します。図 3 に、profile セクションに追加するプロバイダの 例を示します。各プロパティが properties セクション グループにどのように追加されるかも示します。各プロパティは、.NET 型および既定値だけでなく、カスタム プロバイダのデータ属性も指定する必要があります。このデータ属性には、SQL テーブルの列名と、その列の SQL 型を格納します。

Figure 3 profile セクション (Close Figure 3)
<profile defaultProvider="MyCustomProfileProvider">
  <providers>
    <add name="MyCustomProfileProvider"
        type="Microsoft.SqlTableProfileProvider"
        connectionStringName="LocalSqlServer"
        table="CustomProfile"
        applicationName="/"
        profileVersion="1"
        />
  </providers>
  <properties>
    <add name="FirstName" type="string" defaultValue="[null]"
        customProviderData="FirstName;nvarchar" />
    <add name="LastName" type="string" defaultValue="[null]"
        customProviderData="LastName;nvarchar" />
    <add name="Address1" type="string" defaultValue="[null]"
        customProviderData="Address1;nvarchar" />
    <add name="Address2" type="string" defaultValue="[null]"
        customProviderData="Address2;nvarchar" />
    <add name="City" type="string" defaultValue="[null]"
        customProviderData="City;nvarchar" />
    <add name="State" type="string" defaultValue="[null]"
        customProviderData="State;nvarchar" />
    <add name="Zip" type="string" defaultValue="[null]"
        customProviderData="Zip;nvarchar" />
    <add name="Phone" type="string" defaultValue="[null]"
        customProviderData="Phone;nvarchar" />
    <add name="ProfileVersion" type="int" defaultValue="[null]"
        customProviderData="ProfileVersion;int" />
  </properties>
</profile>

CreateUserWizard コントロールのカスタマイズ

ASP.NET 2.0 は認証機能をサポートするビルトイン プロバイダがあるだけでなく、認証と承認を行うビルトイン Web コントロールとして、Login、LoginView、PasswordRecovery、LoginStatus、LoginName、CreateUserWizard、ChangePassword などがあります。CreateUserWizard コントロールを使用すると、ASP.NET アプリケーション内でアカウントを作成できます。プロバイダと同様に、コントロールもコーディングの必要はあるとしてしてもわずかです。CreateUserWizard に代表されるこれらのコントロールは、ツールボックスから ASP.NET ページまたはユーザー コントロールにドラッグするだけで使用できます。

CreateUserWizard は 2 つの手順から構成されるウィザード コントロールです。最初の手順は "新しいアカウントの登録" といって、ユーザー名、パスワード、電子メール アドレス、セキュリティの質問と回答などの情報を入力します。場合により、requiresQuestionAndAnswer プロパティが false に設定されている状況で、web.config で定義されているメンバシップ プロバイダを構成できることがあります。その場合、実行時にセキュリティの質問と回答を入力するフィールドが表示されません。2 番目の手順は "完了" であり、ユーザー アカウントが作成されたことを確認し、[完了] ボタンを表示します。[完了] がクリックされたら、ContinueDestinationPageURL プロパティで指定した URL にユーザーをリダイレクトするか、クリック イベントのイベント ハンドラが用意されている場合はそのハンドラを実行します。通常は、ContinueDestinationPageURL プロパティを使用して、ホームページまたはリファラにユーザーをリダイレクトします。CreateUserWizard コントロールの詳細については、msdn.microsoft.com/msdnmag/issues/05/10/ExtremeASPNET (英語) および msdn.microsoft.com/msdnmag/issues/04/11/CuttingEdge (英語) を参照してください。

登録プロシージャの作成

カスタムのユーザー登録プロセスを作成する方法は、いくつかあります。まず、ビルトインの CreateUserWizard コントロールで、"新しいアカウントの登録" 手順の前に操作を追加する方法です。次に、各手順をナビゲーション テンプレートに変換し、任意にカスタマイズする方法があります。ユーザー情報およびプロファイル情報を異なる手順に移動したり、すべてのフィールドを同じ手順に残したりできます。最後にメンバシップ API の CreateUser メソッドを使用して、独自の登録コントロールを作成する方法です。どの方法を採用するかは、アプリケーションの設計と、カスタマイズを加える程度によって異なります。今回は、最小限のコーディングでプロファイル フォームを追加できる、"新しいアカウントの登録" の前に手順を追加する方法にします。

新しい手順でユーザーは、上記の ProfileVersion プロパティを除く各プロファイル プロパティの値を送信します。なお、UserID および LastUpdatedDate はプロバイダにより設定されるので、ユーザーが値を入力する必要はありません。ProfileVersion プロパティは、アプリケーション設定や web.config の他のセクションから値を取得してフォームを送信するときに設定されます。独自の構成セクションを作成するための詳細については、www.microsoft.com/japan/msdn/msdnmag/issues/06/06/ConfigureThis を参照してください。

図 4 に、マイクロソフトが提供する SqlTableProfileProvider に追加するプロパティを示します。プロファイルのバージョンの値を指定するため、profile セクション グループを使用します。ユーザー アカウントを作成したら、Profile がすぐに更新されます。ProfileCommon クラスとして定義するオブジェクトは、ユーザー プロファイルの最新のプロパティおよび値を読み込みます。このプロファイルとユーザー アカウントはシステムにとって新しいデータなので、すべての値が Null です。プロファイル プロパティのすべての値が、Null 以外になるように更新されます。最後に、ProfileCommon が保存され、プロバイダを使用してデータをデータベースに書き込めるようになります。ProfileCommon が保存されない場合、データの書き込みが必要であることを ASP.NET が認識する方法はありません。

Figure 4 SqlTableProfileProvider クラス (Close Figure 4)
public class SqlTableProfileProvider : ProfileProvider
{
    ...
    private string _profileVersion;

    public override void Initialize(
        string name, NameValueCollection config)
    {
        ...

        // Check to see that a profile version exists
        _profileVersion = config["profileVersion"];
        if (string.IsNullOrEmpty(_profileVersion))
        {
            throw new ProviderException("No profile version specified");
        }

        // Validate the profile version
        int testProfileVersion;
        if (!Int32.TryParse(_profileVersion, out testProfileVersion))
        {
            throw new ProviderException(
                "Profile version must be an Integer");
        }

        ...

        config.Remove("profileVersion");
    }

    // Public property to obtain the ProfileVersion from the web.config
    static public Int32 ProfileVersion
    {
        get
        {
            ProfileSection profileConfig = (ProfileSection)
                WebConfigurationManager.GetSection("system.web/profile");
            return Int32.Parse(profileConfig.Providers[1].
                ElementInformation.Properties["profileVersion"].
                    Value.ToString());
        }
    }

プロファイル バージョンのチェック

ASP.NET を使用する利点の 1 つはモジュール方式であることです。ASP.NET 1.0 以来、ASP.NET の HTTP ハンドラやモジュールを作成することが可能です。HTTPModule を記述して、任意のユーザーのプロファイルがあるかどうか、および正しいバージョンかどうかを確認できます。プロファイルが存在しないか不適切なバージョンである場合は、一元化されたプロファイル更新ページにユーザーをリダイレクトできます。

ユーザーが現在登録に使用しているプロファイルのバージョンが適切かどうかを判断するには、アプリケーションの AuthorizeRequest メソッドを処理する必要があります。処理の際、ユーザーを認証してから、訪問先ページのアクセス許可を与えるまでの間に、カスタム メソッドを実行します (図 5 参照)。ログインしていないユーザーのプロファイルは取得できないので、それらのプロファイルの要求は調査対象から除外する必要があります。また、現在 editprofile.aspx ページにアクセスしているユーザーも、リダイレクトのループでキャッチしないようにするため除外します。2 番目に登録されているプロファイル プロバイダ SqlTableProfileProvider で、profileVersion プロパティの値を取得できます。プロバイダ コレクションの最初のプロバイダは、明示的にコレクションから削除しない限りビルトインのプロファイル プロバイダなので、取得しません。アプリケーションの現在のプロファイル バージョンを取得したら、ユーザー アカウントのプロファイルを読み込んで、プロファイル バージョンをチェックします。そのため、ProfileCommon オブジェクトを新しく作成し、ユーザー名を基にプロファイルを読み込みます。ユーザーのプロファイル バージョンがアプリケーションのプロファイル バージョンより古い場合、ユーザーを editprofile.aspx ページにリダイレクトします。バージョンが一致している場合、ユーザーのアクセスしたページを読み込みます。

Figure 5 ProfileHttpModule クラス (Close Figure 5)
Public Class ProfileHttpModule
    Implements IHttpModule

    Public Sub Dispose() Implements System.Web.IHttpModule.Dispose
    End Sub

    Public Sub Init(ByVal context As System.Web.HttpApplication) _
            Implements System.Web.IHttpModule.Init
        AddHandler context.AuthorizeRequest, AddressOf Me.CheckProfile
    End Sub

    Public Sub CheckProfile(ByVal s As Object, ByVal e As EventArgs)
        Dim objApp As HttpApplication = CType(s, HttpApplication)
        Dim objContext As HttpContext = objApp.Context

        'User is authenticated, check profile
        If objApp.User.Identity.IsAuthenticated And _
                objContext.Request.Path <> "editprofile.aspx" Then
            Dim ProfileVersion As Integer = _
                SqlTableProfileProvider.ProfileVersion

            Dim UserProfile As New ProfileCommon
            If CInt(UserProfile.GetProfile(objApp.User.Identity.Name). _
                ProfileVersion.ToString) < ProfileVersion And _
                !objContext.Request.Path.EndsWith("editprofile.aspx") _
                Then
                    objContext.Response.Redirect("~/editprofile.aspx")
            End If
        End If
    End Sub
End Class

アプリケーションをビルドする前に、web.config ファイルをフォーム認証用に構成します。この構成を行わないと、アプリケーションは machine.config ファイルでの定義に従って Windows® 認証を使用するものと見なされます。web.config ファイルには、認証されていないユーザーによるプロファイル編集ページへのアクセスを禁止するための location セクション グループも必要です。ユーザーがサイトにログインするためのログイン コントロールも追加する必要があります。ログイン コントロールは、メイン ページまたは専用のログイン ページに追加できます。ユーザーを登録ページに誘導するため、ログイン コントロールへの登録リンクを追加することをお勧めします。

アプリケーション内での処理

おさらいします。ユーザーがサイトを訪問し、ユーザー アカウントを作成するためのリンクをクリックすると、登録ページにリダイレクトされ、次の画面でプロファイル情報を入力します。[Next] をクリックして、次の画面でユーザー名とパスワードを入力すると、既定のページにリダイレクトされます。それぞれのページを読み込むとき、web.config ファイルで指定されているプロファイル バージョンがユーザーのプロファイル バージョンより新しいかどうかが HTTPModule によりチェックされます。ユーザーのプロファイル バージョンより新しい場合、ユーザーをプロファイル編集ページにリダイレクトします。バージョンが一致した場合、閲覧を続行します。

モジュールを適切に動作させるには、カスタム プロファイル テーブルに列を追加します。この列は Null 値を許可する必要があります。そうしないと、ユーザーがページを参照したときにエラー メッセージが表示されます。このプロパティは、すべてのユーザーの分が設定されているわけではなく、web.config ファイルにも追加されていません。このプロパティを追加し、プロファイルをバージョン 1 から 2 に変更します。web.config ファイルを保存してサイトにログオンした場合、仕様どおりにページを参照するにはまずプロファイルを更新する必要があります。

web.config ファイルでプロファイル バージョンを保存するときに注意点があります。ユーザーが Web サイトを訪問しているときに構成ファイルを変更すると、アプリケーションがリセットされてユーザーが締め出されます。ユーザーは次にログオンするときに、プロファイル編集ページにリダイレクトされます。このことに注意して、単純なテキスト ファイルか XML ファイルを作成してプロファイルのバージョン番号を読み取ることをお勧めします。

参考情報

どこから着手すればよいかわからない方のため、Visual Studio® Installer (VSI) プロジェクトを用意したのでそれをダウンロードしてください。このプロジェクトには、Visual Basic および ASP.NET で記述した完全なソース コードが含まれていて、これを基にアプリケーション開発に着手できます。プロジェクトは、MSDN® Magazine Web サイトからダウンロードできます。今回見たのは、ASP.NET 2.0 プロファイル プロバイダのカスタマイズ可能な部分のほんの一部です。後はご自分で確かめることで、さまざまなことがわかるでしょう。それこそが .NET の魅力です。つまり、自分だけのサンドボックスがあるようなものです。

Jason N. Gaylordはペンシルベニア州 Wilkes-Barre の有力なリスク管理情報サービス会社でプロジェクト マネージャとシニア アプリケーション デベロッパを務めるかたわら、.NET Valley ユーザー グループを運営しています。連絡先は、www.jasongaylord.com および jason@jasongaylord.com (いずれも英語) です。


編集者のメモ: 不正取得を質す

編集者のメモ
不正取得を質す
Joshua Trupin

単に 編集室にいて、Windows ベースの開発に関する最高のコンテンツをお届けするために働いているからという理由だけで、私たちがニュース、ゴシップ、噂話に興味がないわけではありません。

最近、興味深いニュースがありました。それは、太平洋岸北西部にある大手ソフトウェア会社が Windows Vista を評価するためにブロガーにラップトップ コンピュータを提供したというニュースで、この話に憤慨したキャスターもいました。これは明らかに道義を逸脱した史上最悪の行為で、ブログ社会にジャーナリストとしての倫理に関する疑問を投げ掛けました。

出版業界では、評論家に商品を提供することは伝統的な慣習です。このような書評についてはどう思いますか。また、雑誌では、新しい MP3 プレーヤーをどのように評価していると思いますか。評論家は、自費で評価する商品を買うわけではありません。10 個の異なるバージョン管理プログラムのうち、どのプログラムの性能が最もよいのかを調査するために、苦労して稼いだお金を費やしていると思っていましたか。

事実として、私たちのスタッフは評価する際に謝礼を受け取ることはありません。すべて整備された新しいラップトップが提供されたらどんなにすばらしいかと思いますが、私たちの多くは、半年も前のモデルを使用して四苦八苦しながら商品を評価しています。これから半年の間に "評価する" 可能性がある商品の一覧を掲載することは役に立つのでしょうか。プラズマ TV が支給されたらいいですね。小さな 42 インチのものではなく大きなものがよいです。30 台の Zunes を評価することも検討しています。おっと、30 ではなく 32 にしましょう。あと、ポニーの評価もするかもしれません。

この商品講評騒動のポイントは、ブロガーがプロのジャーナリストの倫理観に従って行動する必要があるということです。カンファレンスでプロのジャーナリストを見かけたことはありますか。片方のポケットに粗品のペンを、もう片方のポケットには軽食として出された子エビのカクテル サラダを乱雑に入れている人たちです。もちろん、このコラムを読んでいる人は例外だと思いますが、私たちはペンよりもスーパーボールが好きでしょう。

ブログは、爆発的に普及し、多くの企業では企業文化の一部にもなっており、顧客への情報伝達の方法に変化をもたらしました。おそらく、一般ユーザーや企業が 10 年ほど前にインターネットを使用し始めたときと同じくらいの影響があるでしょう。このブログの普及は、基本的に、すべてのユーザーにメリットをもたらします。マイクロソフトの社員は、各自がかかわっている商品についての情報をブログで発信し、そのブログの読者は普段使っているテクノロジについての理解を深めることができます。MSDN Magazine は定期的にブログ (blogs.msdn.com/msdnmagazine、英語) で記事を公開しています。月刊の MSDN Magazine ほど詳細な情報は提供していませんが、ブログは不足している必要な知識を補うには手頃なツールです。

もちろん、他の通信テクノロジの発達と同様、ブログでも不正利用が見られます。たとえば、ブログ スパム、やらせブログなどがあります。スパム送信者や不正行為を働くユーザーは、私たちの隙を突いて、つまらないものを送りつけてきます。ブログのコメントについては、おわかりだと思いますが、「かぎ針編みについてのすてきなブログですね。私のブログ musclemaxsupplement.contoso.com にもぜひ遊びに来てください。」のようなコメントが、これに該当します。このようなコメントのリンクをクリックする際には、かぎ針編みに関するブログにアクセスできることを想定すると思いますが、リンク先はかぎ針編みとは何の関係もないページであったりします。

やらせブログ (偽のブログ) にも危険が潜んでいます。企業や広告代理店は、優れた新しいテクノロジについて草の根レベルの議論をする場としてブログを立ち上げます。かぎ針編み用の毛糸の新色がどれだけすばらしく、蛍光緑色の手編みポンチョが今年の流行になるという記事をユーザーが読んだとしても、その記事が本当に蛍光緑色のアクリルを売り出している企業が発信した情報であるのかどうかを知ることはできません。"本物" のブログの匿名コメントも、このように作成されることがあります。通常、コメントを書いたユーザーが、特定のテクノロジ、候補者、または企業を擁護するだけのためにコメントを投稿した場合は、それを簡単に見破れます。しかし、ここで紹介したものは、ブログの世界で発生している事件の一場面にすぎず、新聞の読者からの投稿欄でも同じようなトリックが使用されているのを目にするかもしれません。

世間ではちょっとした騒動が起きているようですが、私たちは、Zunes が送られてくるのを気長に待つことにします。宛先はご存知ですよね。-J.T.

Thanks to この記事の執筆に携わったマイクロソフトの技術部門の専門家は、Moustafa Ahmed、Bashar Badawi、Joe Duffy、Shawn Farkas、Matt Gibbs、Jayu Katti、Joe Klug、Mark Lawrence、Rob Mensching、Tom Mulcahy、Gruia Pitigoi-Aron、Stefan Schackow、Greg Schechter、Bob Schmidt、Tim Sneath、Aaron Stebner、および Robert Wlodarczyk です。

Active Accessibility、Active Directory、ActiveX、Authenticode、BizTalk、DirectShow、IntelliSense、Internet Explorer、JScript、Microsoft、Microsoft Press、MSDN、Virtual Earth、Visio、Visual Basic、Visual Studio、Windows、Win32、Windows Media、Windows NT、Windows PowerShell、Windows Server、および Windows Vista は、米国またはその他の国におけるマイクロソフトの登録商標または商標です。ここに記載されている商標または会社名は各社の所有物です。 MSDN Magazine に記載されているコードまたはその他の情報に関し、明示または黙示にかかわらず一切の表明または保証を行うものではありません。MSDN Magazine は、コードまたはその他の情報の使用に対していかなる責任も負わないものとします。MSDN Magazine に記載されている推薦事項および技術ガイドラインは、特定の環境および構成に基づいたものです。これらの推薦事項やガイドラインは、異なった環境や構成には適用できない場合もあります。MSDN および Microsoft は、米国またはその他の国におけるマイクロソフトの登録商標または商標です。MSDN Magazine、MSDN、および Microsoft のロゴは、CMP が許可を得て使用しています。MSDN Magazine は、CMP Media LLC によって発行されています。CMP Media LLC は、マイクロソフトの系列企業ではなく独立した企業です。マイクロソフトは、MSDN Magazine の編集内容のみに責任を負うものとします。

MSDN Magazine does not make any representation or warranty, express or implied with respect to any code or other information herein. MSDN Magazine disclaims any liability whatsoever for any use of such code or other information.


ツールボックス: デスクトップの共有、簡易コード編集

ツールボックス
デスクトップの共有、簡易コード編集
Scott Mitchell

コンテンツ
デスクトップを共有する
簡易コード編集
グラフを使ってモデル化する
推薦図書

デスクトップを共有する

世界各地の顧客と自宅で仕事を行う契約社員にとっては、顧客や同僚と迅速かつ効率的にコミュニケーションできることがきわめて重要です。通常は、電子メールやインスタント メッセージなどの情報伝達ツールで十分ですが、問題のトラブルシューティングを行う際には、顧客にデスクトップを表示したり、顧客のデスクトップを確認したりすることが必要になることもあります。同僚がデバッグを行う際にそれをサポートしたり、見込み客に対してデモや宣伝を行ったりするときには、ビデオ会議も非常に役に立ちます。

ビデオ会議やデスクトップの共有に役立つテクノロジは、コスト、セットアップや使用方法の容易さ、機能などの点でさまざまです。無償のテクノロジの多くは、特定のファイアウォールのポートを開いたりソフトウェアをインストールしたりするなど、技術的知識が少ない同僚や顧客であれば当惑するような手順を必要とします。さいわい、会議を開くための、低コストで使いやすいソリューションが数多くあります。

そのような製品の 1 つに、Citrix Online の GoToMeeting があります。これを使用すると、月額 49 ドルで、1 会議あたり 10 人まで参加できるビデオ会議を無制限にホストできます (参加者を 25 人までホストする必要がある場合には GoToMeeting Corporate を、1,000 人までの場合には GoToWebinar を使用することを検討してください)。会議を始めるには、タスク バーの GoToMeeting アイコンをダブルクリックするだけです。この操作で会議が開始され、デスクトップに招待する他の参加者に送信する URL が作成されます。招待されたユーザーが会議に参加するには、その URL をブラウザのアドレス バーに貼り付けるだけです。ソフトウェアをインストールしたりファイアウォールの設定を細かく調整したりする必要はありません。

アクセスが完了したすべての参加者は、デスクトップを表示してシステムの操作を確認することができます。また、デスクトップの制御を他の参加者に渡すこともできます。ただし、GoToMeeting は、単にデスクトップを共有するだけの簡単なプログラムではなく、多くの機能を備えています。ボタンをクリックすることで、発表者を切り替え、発表者に選択した参加者のデスクトップを他のすべての参加者に表示できます。

GoToMeeting では、音声会議の機能も無料で提供されます。参加者は、ある番号に電話をかけるだけで電話会議に参加できます。接続が完了すれば、会議に参加しているすべての参加者と会話できます。

多くの場合、遠隔地にいる顧客や同僚にデスクトップを示すことができれば、時間を大幅に節約できます。在宅勤務の場合や、遠隔地にいる顧客や同僚と定期的にやり取りする場合には、GoToMeeting のようなビデオ会議サービスに投資することで、時間とお金の両方を節約できます。

価格 : 参加者 10 人までの会議、回数無制限 - 月額 49 ドル

www.GoToMeeting.com (英語)

簡易コード編集

Visual Studio® は、デザイン時に豊富なサポートを提供するため、UI 中心のアプリケーション開発にとっては最適ですが、コマンドライン プログラム、クラス ライブラリ、Web サービスなど、コードのみのアプリケーションでは、Visual Studio のさまざまな機能が過剰になることがあります。Windows® ベースの開発を行うために Visual Studio を習得し、さらに別の環境で開発を行うために別のコード エディタも習得する必要があるとすれば、多くの時間が必要になります。このことは、特に、複数のプラットフォームにまたがるプロジェクトを扱う開発者に当てはまります。SlickEdit Inc の SlickEdit version 11 は、余分な機能を省いたテキスト エディタの操作性と、Visual Studio の複雑な機能セットとの間の中間的な存在として、優れたサポートを提供します。

(画像を縮小するには、ここをクリックします)

SlickEdit は、一度に 1 つのファイルを編集する簡単なテキスト エディタとして使用できます。さらに、Visual Studio のプロジェクトやソリューションのように、プロジェクトとワークスペースにファイルを分類することができます。ワークスペースは手作業で作成することも、Visual Studio のプロジェクト ファイルやソリューション ファイルからインポートすることもできます。コード編集時の操作性は、Visual Studio とほとんど同じです。コードは色分けされ、自動的にインデントが設定されます。オート コンプリート機能は、Visual Studio の IntelliSense® と同等です。また SlickEdit は、(多数の他の一般的なエディタと同様に) Visual Studio と同じキーボード コマンドとショートカット キーをサポートするように構成できます。SlickEdit には、共通のデザイン パターンを素早く実装するための、さまざまなリファクタリング オプションやビルド済みコード テンプレートも用意されています。

SlickEdit には、Visual Studio にはないさまざまな機能もあります。ファイルをマージしたり、ファイル内の相違点を強調表示したりするための、組み込みの正規表現テスタやメニュー オプションがあります。高度な構成が可能な "整理" オプションでは、書式ルール (インデント、コード ブロックの先頭と末尾のスタイル、コメントのスタイルなど) を指定して、ボタンをクリックすることで、すべてのファイルの見た目を整理できます。

しかし、SlickEdit が最も優れている点は、余分な機能が省かれていることにあります。データベースの編集ツールや GUI 作成用のデザイナはありません。SlickEdit は、コード編集という中核作業に焦点を合わせることで、重要な機能の質を落とすことなく、応答性の高い簡易コード編集環境を提供します。

SlickEdit は、複数の環境を扱う開発者や開発チームにとって最適な選択肢です。SlickEdit では、Windows、Linux、Mac OS X、Solaris、AIX、その他さまざまな種類の UNIX など、複数の OS に対するサポートが提供されます。

価格 :1 ユーザー ライセンス - 284 ドル

www.slickedit.com (英語)

グラフを使ってモデル化する

グラフやフローチャートは、エンティティ間の関係をグラフィカルに表示、作成、変更したり、ワークフローのプロセスやネットワークを示したり、XML などの階層データを表示したりするのに優れたユーザー インターフェイス要素です。

このように優れたモデル化インターフェイスを自身のアプリケーションに追加することは困難に思われるかもしれませんが、このような機能を提供するようにデザインされたサードパーティ製のコントロールを使用すれば、実際には非常に簡単なプロセスになります。このようなコントロールの 1 つに MindFusion の FlowChart.NET version 4.2 があります。このコントロールは、フローチャート、レーン図、階層など、さまざまな種類の分類グラフを生成できます。

図 FlowChart.NET FlowChart.NET を使用して専門家のようにグラフやチャートを追加する
図 FlowChart.NET FlowChart.NET を使用して専門家のようにグラフやチャートを追加する(画像を縮小するには、ここをクリックします)
図 FlowChart.NET FlowChart.NET を使用して専門家のようにグラフやチャートを追加する
図 FlowChart.NET FlowChart.NET を使用して専門家のようにグラフやチャートを追加する (画像を拡大するには、ここをクリックします)

このようなグラフの 1 つを作成するには、FlowChart コントロールをツールボックスからフォームにドラッグするだけです。四角形、表、矢印、組み込みやカスタムのあらゆる .NET コントロールなど、さまざまなオブジェクトをプログラムから FlowChart に追加できます。

エンド ユーザーはフローチャートを操作し、新しい要素を追加したり、それらのサイズ変更や配置変更を行ったり、アイテムの選択や変更を行ったりすることができます。FlowChart コントロールには、ユーザーがさまざまな方法でグラフを操作すると発生する多くのイベントがあり、必要に応じてユーザー操作を検出して応答することが容易になります。フローチャートは、ストリーム ファイルかバイナリ ファイル、テキスト ファイル、または XML ファイルに保存され、ほんの数行のコードで再度読み込まれます。Pro エディションでは、フローチャートを PDF、Visio®、および SVG 形式にエクスポートすることもサポートされます。

FlowChart.NET のダウンロードには、グラフとフローチャートを使用した Web サイト マップの作成、データベース スキーマの操作、アセンブリから読み込んだクラス ダイアグラムの表示など、このコンポーネントの機能をいくつか紹介する一連のデモが含まれています。これらのデモの UI は非常に強力で、エンド ユーザーは、サイト マップやデータベース スキーマをグラフィカルに作成したり、クラス構造を表示したりできます。こうしたすべての機能は、事実上、コンポーネントに直接組み込まれています。それぞれのデモの機能のためのコードはほとんどなく、簡単に理解できます。

価格 :1 ユーザー ライセンス - 250 ドル (標準バージョン)、500 ドル (プロフェッショナル エディション)

www.mindfusion.org (英語)

推薦図書

.NET プログラムはどの .NET プログラミング言語でも作成できますが、ここ数年の間に、C# を選択する開発者が増えてきました。C# 人気の理由の 1 つは、その簡単なセマンティクスと使いやすさにあります。

C# の習得に特に役立つ書籍の 1 冊に、Trey Nash の『Accelerated C# 2005』(Apress、2006 、英語) があります。この書籍は、オブジェクト指向のプログラミング言語を扱ってきた中級レベルの開発者を対象とし、C# 専門家になるための支援を目標としています。

この書籍の前半部分では、C# 構文およびセマンティクスに重点を置き、型、メソッド、イベント、オブジェクトの初期化と破棄、インターフェイス、オーバーロード、継承などの中核となる概念について説明します。ここでは C# プログラミング言語の基本事項を凝縮した概要が提供され、C# への切り替えを行っているユーザーにとって非常に役立ちます。既に日常的に C# を使用しているユーザーでも、こうした前半の章から学べることは少なくありません。

この書籍の後半部分では、C# での共通デザイン パターンの実装について説明します。各章では、例外の最適な処理方法、配列やコレクションの効率的な使用方法、デリゲートとイベントの使用、およびマルチスレッド アプリケーションの作成について解説します。ジェネリック、反復子、無名関数など、C# 2005 の新機能に関する内容も多数あります。

この書籍は、ほとんどのプログラミングのテキスト本よりも薄い 401 ページで、簡潔な記述を好む読者にとって魅力的です。補足記事や追加例はほとんどなく、あっても、無駄な説明を除いた情報のみです。

価格 :39.99 ドル

www.apress.com (英語)

ご意見やご質問は、

Scott toolsmm@microsoft.com.

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

記載価格は印刷開始時点のものであり、変更されている可能性があります。このコラムに記載されている見解は、執筆者の個人的な見解であり、必ずしもマイクロソフトの見解を反映するものではありません。

MSDN Magazine does not make any representation or warranty, express or implied with respect to any code or other information herein. MSDN Magazine disclaims any liability whatsoever for any use of such code or other information.


CLR 徹底解剖: .NET アプリケーションの拡張性、第 2 部

CLR 徹底解剖
.NET アプリケーションの拡張性、第 2 部
Jack Gudenkauf and Jesse Kaplan

コードのダウンロード : : AddIns2007_03.exe (177KB)
コンテンツ
パイプライン アーキテクチャ
パイプライン コンポーネント
パイプラインの実装
ホストとホスト ビューを変更する
ホストとアドインの互換性
まとめ

前回のコラムでは、Base Class Library (BCL) に新しく System.AddIn 名前空間を導入し、ホスト アプリケーション全体に拡張性を持たせるためにユーザーが取り組む代表的なプロセスを調べました。また、包括的なアドイン ソリューションを提供するのに必要な機能を説明し、サンプル ホスト コードや、ホストで使用する新しい System.AddIn API についても見てきました。コラムの最後には、サンプル アドイン コードをいくつか紹介し、アドインの開発者は、ホストが提供するオブジェクト モデルを利用する際に、基になるメカニズムをまったく意識する必要がないことを明確に示しました。

前回は、System.AddIn パイプラインの説明を次回に持ち越し、ホストとアドインのバージョンが個別に変化したときに、互換性を確保できるモデルの利用について説明することを約束しました。今回は、まず、このモデルについて詳しく説明します。次に、先月レビューしたアプリケーションのバージョン 2.0 を見ていきます。ここでは、このバージョンを使用してパイプラインのしくみを詳しく調べ、このシステムを使用して、ホストが変更になってもアドインが機能し続ける方法を例を挙げて示します。

今回のコラムを読み進める前に、[用語] リンクを見ておくと役に立つかもしれません。また、System.AddIn 名前空間とその関連クラスは Visual Studio® 2005 の次にリリース予定の Visual Studio (コードネーム "Orcas") に含められる予定です。この新しいライブラリは、Visual Studio "Orcas" の完全版がリリースされる前に、Community Technology Preview を通じてダウンロードできます。詳細については、CLR アドイン チームのブログ (blogs.msdn.com/clraddins) (英語) を参照してください。

用語

以下に、このコラムで使用する用語をいくつか説明します。

アドイン (アドオン、拡張機能、プラグイン、スナップインとも呼ばれます) はカスタム コードで、通常サード パーティによって作成されます。アドインは、ホスト アプリケーションによって (ホストの起動時やドキュメントの読み込み時など) 状況に応じて動的に読み込まれ、アクティブになります。

下位互換性 (旧バージョンとの互換性) とは、現バージョンのアプリケーションやコンポーネントが、以前のバージョンのアプリケーションやコンポーネントを対象としたコンポーネントを使用できることです。たとえば、共通言語ランタイム (CLR) 2.0 はバージョン 1.1 を対象とするアプリケーションやコンポーネントを実行できるので、バージョン 1.1 と下位互換性があります。

ホストとアドインの観点からは、以前のバージョンのホスト アプリケーションに対して作成されたアドインが、新しいバージョンでも変更なしに実行される場合、ホスト アプリケーションには以前のバージョンとの下位互換性があると言います。

上位互換性 とは、現バージョンのアプリケーションやコンポーネントが、将来のバージョンのアプリケーションやコンポーネントを対象としたコンポーネントを使用できることです。上位互換性は将来に関わることなので、あくまで理論上にすぎません。上位互換性が完全に確保されるのは、将来のバージョンでまったく変更が行われない場合に限ります。たとえば、CLR 2.0 機能に対して作成されたアプリケーションでも CLR によってサポートされておらず、CLR 1.1 に対しても実行できれば、上位互換性があることになります。

ホストとアドインの観点からは、最新のバージョンのホスト アプリケーションに対して作成されたアドインが、以前のバージョンでも変更なしに実行される場合、ホスト アプリケーションには上位互換性があります。

パイプライン とは、ホストとアドインが、バージョンの変化に柔軟に対応できるプロトコル経由で通信できるようにする通信のしくみです。

パイプライン アーキテクチャ

図 1 に、ホストとアドインの通信を可能にするパイプライン コンポーネントを示します。パイプライン コンポーネントは、アドインのアクティブ化とアドインとの通信に使用されます。

図の中の各コンポーネントは、実際には多種多様なアセンブリです。つまり、ホストとアドインの間で交換される複雑なオブジェクトの種類ごとに、境界の両側に対応するビュー、コントラクト、およびアダプタの各種類があります。図は分離境界を挟んで完全に左右対称になっていることに注意してください。このモデルに従った場合の一方の側を 図 2 に示します。ホストとアドインは、実際の実装を提供し、ユーザーに値を示す実体です。こうしたモデルの高度な目標の 1 つ、およびこのような設計パターンを使用する主な動機は、境界の一方の側または両側で変更が行われた場合でも、各コンポーネントが相互に通信および相互作用できるようにすることです。

図 1 ホストとアドイン間の通信パイプライン
図 1 ホストとアドイン間の通信パイプライン(画像を縮小するには、ここをクリックします)
図 1 ホストとアドイン間の通信パイプライン
図 1 ホストとアドイン間の通信パイプライン (画像を拡大するには、ここをクリックします)

図 1 は、重要な事実を数多く示しています。まず、ホストとアドインは分離境界で相互に切り離されています。それぞれは自身のビューしか参照しないので、通信に使用するコントラクト上に抽象層がもうけられます。さらに、ホストとアドイン自体には直接参照がないので、ホストまたはアドインを、通信を行う他のすべてのコンポーネントから相対的に分離できます。

図 2 ホスト/アドイン モデル
図 2 ホスト/アドイン モデル(画像を縮小するには、ここをクリックします)
図 2 ホスト/アドイン モデル
図 2 ホスト/アドイン モデル (画像を拡大するには、ここをクリックします)

パイプライン コンポーネント

コントラクト : コントラクト アセンブリには、バージョン管理されない型 (コントラクト) を含みます。コントラクトは、分離境界をまたぐ通信メカニズムとして、その境界の両側に読み込まれます。ここでは、境界をまたぐすべての通信をコントラクトに制限することで、ホストとアドインの実装の形式が境界を越えて相手側に影響し、バージョン管理の問題の原因になることを防いでいます。このコラムのシステムでは、すべてのコントラクトが、"オブジェクト" 型として機能する基本 IContract から継承されます。コントラクト アセンブリの変更は許可されていないことに注意してください。ただし、ホストとアドインは通信に異なるコントラクトを使用でき、どちらの実体もそのことを認識しません。COM に精通していれば、次のコードを見ると、IContract の機能と IUnknown の機能 (QueryInterface、AddRef、および Release) が似ているのがわかります。

public interface IContract
{
    int AcquireLifetimeToken();
    int GetRemoteHashCode();
    IContract QueryContract(string contractIdentifier);
    bool RemoteEquals(IContract contract);
    string RemoteToString();
     void RevokeLifetimeToken(int token);
}

コントラクトは IContract から継承し、インターフェイスにする必要があります。そのため、固有の実装は許可されません。コントラクトは、統合と実装の形式を分離します。コントラクトのシグネチャは、mscorlib で定義されているシリアル化可能な型 (Int32, String、DateTime など)、他のコントラクト、およびコントラクト アセンブリで定義される列挙値に制限されます。また、コントラクト インターフェイスは Windows® Communication Foundation のコントラクト モデルとの調整も行い、このような形式での実装がないことを保証し、多重継承を許可します。

ビュー : ビューは、ホストとアドインのビューをそれぞれ表す抽象クラスと、ホストとアドイン間を流れる型を表す抽象クラスを含むアセンブリです。ホストとアドインに、両者間の通信に使用するコントラクトに依存しない安定した見方を与えることがビューの目的です。また、ビューによってホストとアドインのそれぞれの実装が境界の両側に分離されます。図では、ホストとアドインは自身のビューのみに依存し、ビュー自体には依存関係がないことが示されています。これにより、ホストやアドインに影響を与えずに、アダプタやコントラクトを取り替えることができます。

アダプタ : アダプタ アセンブリには、ビューとコントラクト間の変換に使用するアダプタ クラスを含みます。図の中ではアダプタが境界のどちら側に存在するかを説明するために、"側" という言葉を大まかに使用しています。実際には、呼び出しの方向、つまりアダプタがビューからコントラクトへの変換を行うか、コントラクトからビューへの変換を行うかによって決まります。したがって、呼び出しを双方向に行う場合、つまり、ホストからアドインを呼び出し、アドインからもホストを呼び出す場合は、境界の片 "側" に 2 つのアダプタがあります。

アダプタ アセンブリのクラスで、ビューからコントラクトへの変換を行うのはビューからコントラクトへのアダプタです。このクラスは、クラスのコンストラクタに渡されたビューを呼び出すことによってコントラクトを実装し、境界を越えてコントラクトとしてマーシャリングされます。コントラクトからビューへのアダプタは、コントラクトからビューへの変換を行うアダプタ アセンブリのクラスです。このクラスはビューから継承し、クラスのコンストラクタに渡されたコントラクトを呼び出すことによって、抽象メソッドを実装します。

図では、アダプタ アセンブリがビューとコントラクトの両方を参照していることを示しています。その結果、両者の間の接続が提供されます。アダプタは、ホストとアドイン間の接続を維持し、一方または両方に大幅な変更が加えられた場合でも機能し続けるようにする、接着剤の役割を果たします。この後、さらに詳しく見ていきましょう。

パイプラインの実装

では、サンプル コードを調べ、実装が実際にどのように動作するかを見てみましょう。今回は、前回説明したサンプル アプリケーションのバージョン 2.0 を使用して、ホストをどのように変更したか、およびホストとアドインがパイプラインでどのように繋がるかを調査します。その後、最初のバージョンのホストに対して前回ビルドしたアドインが、新しいバージョンに対しても変更しないで機能できることを保証するために、ここで紹介したモデルを使用する方法を見ていきます。

システムは、アドインをアクティブにし、そのアドインをホストに戻す際のみに関与します。これが行われたら、そこから離れ、パイプラインが型の流れを処理できるようにします。アクティベーション パスはホストとアドインの対話の最初の手順なので、詳しく見ることにしましょう。

アドインのアクティベーションは、ホストが使用可能なアドインを発見し、アクティブ化するアドインを選択した後に行われます (詳細については、ダウンロードに含まれる Program.cs、または前回のコラムを参照してください)。サンプルでは、次の簡単なコードでアクティベーションが行われます。

//Activate the selected AddInToken in a new AppDomain
Calculator calculator =
    calcToken.Activate<Calculator>(AddInSecurityLevel.Internet);

ホストの観点からは、単純に Activate を呼び出し、ビュー "Calculator" として型指定されて返されるアドインを受け取ります。ただし、この際、システムによってアドインが起動され、パイプラインに結び付けられます。

これを行うには、まず、アドインの実行場所となる AppDomain を作成し、アドイン、アドイン ビュー、アドイン側アダプタ、およびコントラクト アセンブリを、新しく作成したドメインに読み込みます。アドイン ビューは、図 3 ではアドイン ベースとして示されており、パイプラインがアクティベーションに使用するアドイン ビューを指定するために使用する単なる属性です。次に、システムは既定のコンストラクタとアドイン アダプタを使ってアドインのインスタンスを作成し、アドイン ベースとして型指定されたアドインをアダプタのコンストラクタに渡します。その後、コントラクト (この場合は ICalculatorContract2) として型指定されたアドインを AppDomain 境界を越えて渡し、ホスト側アダプタ アセンブリを読み込みます。さらに、コントラクトとして型指定されたアドイン アダプタをホスト アダプタのコンストラクタに渡して、ホスト アダプタのインスタンスを作成します。最後に、ホスト アドイン ビュー (この場合 Calculator) として型指定されたホスト側アダプタをホストに返します。

図 3 アクティベーション パス
図 3 アクティベーション パス(画像を縮小するには、ここをクリックします)
図 3 アクティベーション パス
図 3 アクティベーション パス (画像を拡大するには、ここをクリックします)

これには多くのオーバーヘッドが生じるように思えますが、このモデルを使用して境界を越えて通信を行う場合のパフォーマンス コストは、実際には、境界を越えてインターフェイス上で直接通信を行う場合に比べても最小限に抑えられます。

アドインのアクティベーション パスを見てみると、アクティベーションは、実際にはアドインからホストに渡されているオブジェクトの一例にすぎないことがわかるでしょう。唯一の違いは、システム自体がアドインのインスタンスを作成し、コンポーネントに接続する点です。したがって、システムが接続するコンポーネントを識別し、コンポーネントのインスタンスを正しく作成するために、アクティベーションに関与する各コンポーネントは、コンポーネントの識別に適用されるカスタム属性を備えている必要があります。一般に、アクティベーションには各パイプライン コンポーネント内の 1 つの型が関与するので、コンポーネントに適用される属性の中から 1 つを備えることになります。ただし、唯一の例外はホスト ビューです。ホスト ビューは、API を通じて直接システムに提供されるので、属性を備える必要はありません。

コラムの例では、アドインは自身をサービスとしてホストに提供しています。そのため、アドインとホスト間で受け渡されるデータ型は double と string のみです。このため、サンプルにはアクティベーション パスに関与する型しか含まれていません。複雑なデータをホストとアドイン間で双方向に受け渡す例については、チームのブログを参照してください。

ホストとホスト ビューを変更する

前述のように、ホスト アプリケーションには、サービスを提供するアドインが必要でした。具体的には、アドインは Add メソッド、Subtract メソッド、Multiply メソッド、および Divide メソッドの形式で、基本的な電卓機能を提供する必要があります。

アドイン本来のホストのビューは次のようなものでした。

public abstract class Calculator {
    public abstract double Add(double a, double b);
    public abstract double Subtract(double a, double b);
    public abstract double Multiply(double a, double b);
    public abstract double Divide(double a, double b);
}

ただし、新しいバージョンでは、ホストはアドインから前回よりも多くの機能を要求し、電卓サービスが提供する機能に柔軟性を持たせることにしました。そのため、アドインの新しいホストのビューを次のように定義しました。

public abstract class Calculator {
    public abstract string Operations { get; }
    public abstract double Operate(
        string operation, double a, double b);
}

ホストは、アドインの 4 つの特定の演算に依存するのではなく、電卓がサポートする演算の種類を認識し、こうした演算を実行できるようになりました。この種の変更は、あるサービスをアドインに依存しているホストでは一般的です。つまり、ホストは時間の経過と共にアドインからの追加機能が必要になることを検出するか、拡張ポイントに柔軟性を持たせることを決めます。このコラムのモデルでは、新しく更新されたビューに対してのみプログラミングを行い、この新しいビューにレガシ アドインを接続する場合はパイプラインに依存します。

サンプルでは、HostCalculatorView プロジェクトの CalculatorView2.cs 内に新しいビューの定義があります。ホストからこの新しいビューを使用する方法の詳細については、SampleExtensibleCalculator.csproj 内の Program.cs を参照してください。

ここでのカスタム コントラクト (ICalculatorContract2) は、分離境界をまたぐ通信に使用する型です。元のコントラクトは次のようになっていました。

[AddInContract]
public interface ICalculatorContract : IContract
{
    double Add(double a, double b);
    double Subtract(double a, double b);
    double Multiply(double a, double b);
    double Divide(double a, double b);
}

今回はアドインに関するホストのビューが変更され、別のコントラクトに移行されて、次のコントラクトを使用するようになりました。

[AddInContract]
public interface ICalculatorContract2 : IContract
{
    string GetAvailableOperations();
    double Operate(String operation, double a, double b);
}

ホスト ビューとホスト ビューに行われた変更は、通常、コントラクト内でミラー化されます。

アドイン ビューは、通常、ホスト ビューをミラー化し、コントラクトに行われたのと同じ方法で変更されます。このサンプルでアドイン ビューとホスト ビューが区別される点は、各ビューが含まれるアセンブリと、AddInBase (アクティベーションに使用される型) とマークされた属性のみです。

[AddInBase]
public abstract class Calculator2
{
    public abstract string Operations{ get; }
    public abstract double Operate(
    string operation, double a, double b);
}

アドインは、アドイン基本クラスから継承し、アクティベーション時にアドインを特定するために属性を適用する必要があります。ダウンロードに含まれる完全な実装のサンプル コードでおわかりのように、アドインの定義は次のようになっています。

[AddIn("Basic V2 Add-in", Version="2.0.0.0")]
public class SampleV2AddIn : Calculator2
{
    public override string Operations
    {
        get { ... }
    }

    public override double Operate(
        string operation, double a, double b)
    {
        ...
    }
}

サンプル コードでは、アドインはホスト アプリケーションにサービスを提供しています。そのため、ホストの機能を拡張するには、ホストからアドインを呼び出す必要があります。このシナリオでは、アドイン側アダプタがビューからコントラクトへのアダプタの役割を提供しています。

図 4 に示すサンプル コードは CalculatorView2ToContract2AddInAdapter.cs にも含まれており、ダウンロードに含まれる AddInSideAdaptersV2.csproj プロジェクトでも確認できます。AddInAdapterAttribute は、アドイン アダプタ クラスを検索してアクティブ化するために、System.AddIn.Hosting API で使用される属性です。

Figure 4 CalculatorViewToContractAddInAdapter (Close Figure 4)
[AddInAdapter]
public class CalculatorViewToContractAddInAdapter : ContractBase,
    ICalculatorContract2
{
    private Calculator_view;

    public CalculatorViewToContractAddInAdapter(
            Calculator2 calculator) {
        view = calculator;
    }

    public string GetAvailableOperations(){
        return _view.Operations;
    }

    public double Operate(string operation, double a, double b){
        return _view.Operate(operation, a, b);
    }
}

アドイン アダプタは ContractBase クラスから派生します。このクラスは、System.AddIn.Pipelineで提供される基本クラスです。このクラスは、MarshalByRefObject、IContract、および ISponsor から派生します。ISponsor とホスト アダプタの有効期間管理については、この後説明します。アドイン アダプタも、カスタム コントラクトである ICalculatorContract から派生します。このコントラクトを使用して、新しいアドインを電卓の GetAvailableOperations メソッドと Operate メソッドに提供します。これらのメソッドはどちらもアドインのバージョン 2.0 (SampleV2AddIn.cs) で実装されます。アドイン アダプタ コンストラクタでは、アドイン (SampleV2AddIn) をアドイン ビュー (CalculatorContractsBase.Calculator) として取り込みます。コンストラクタは、(アドイン ベース Calculator 2 として型指定された) アドインを呼び出すことによって、ICalculatorContract2 の必要な機能を実装します。

サンプル コードでは、アドインから提供されたサービスを使用しているため、ホストの機能を拡張するために、ホストからアドインを呼び出す必要があります。このシナリオでは、ホスト側アダプタがコントラクトからビューへのアダプタの役割を提供しています。ホストは、アドイン アダプタから提供された機能を経由して呼び出しを行います。図 5 のコード (HostSideAdapters.csproj プロジェクトの CalculatorContract2ToView2HostAdapter.cs) がこの動作を示しています。

Figure 5 CalculatorContractToViewHostAdapter (Close Figure 5)
[HostAdapter]
public class CalculatorContractToViewHostAdapter : Calculator
{
    private ICalculatorContract2 _contract;

    private System.AddIn.Pipeline.LifetimeTokenHandle _handle;

    public CalculatorContractToViewHostAdapter(
            ICalculatorContract2 contract)
    {
        _contract = contract;
        _handle = new LifetimeTokenHandle(contract);
    }

    public override string Operations {
        get {  return _contract.GetAvailableOperations(); }
    }

    public override double Operate(string operation, double a, double b) {
        return _contract.Operate(operation, a, b);
    }
}

この例では、ホスト アダプタ クラス (CalculatorContractToViewHostAdapter) を検索してアクティブ化するために、System.AddIn.Hosting API によって HostAdapterAttribute が使用されます。ホスト アダプタは、ホスト ビューから派生します。その名前からもわかるように、ホスト ビューはホストからのアドインのビューです。ホスト アダプタ コンストラクタでは、カスタム コンストラクタ (この場合は ICalculatorContract2) として型指定されたアドイン アダプタを取り込みます。

ホスト アダプタのコードとアドイン アダプタのコードはよく似ていることがわかります。主な違いは、一方がコントラクトからビューに変換するのに対して、もう一方がビューからコントラクトに変換する点です。ホスト アダプタでは、コンストラクタにコントラクトを取り込んで、ビューから継承するのに対して、アドイン アダプタではその反対のことを行います。ここに示したコードの中で他に興味深いのは LifetimeTokenHandle のみでしょう。これについては、この後説明します。

ホストはアドインを経由して機能を呼び出すため、アドインの AppDomain に常駐するリモート オブジェクトへの参照を保持する必要があります。そのため、オブジェクトの有効期間は、アドインではなく、ホストが判断する必要があります。ここでは、リモート処理を基にビルドした簡単なメカニズムを用意しています。このメカニズムでは、アドインがアドインの AppDomain に用意したオブジェクトへの参照をホストが保持している間はそのオブジェクトが存続していることを保証します。ホストがこの参照を解放すると、ガベージ コレクタ (GC) によってホストが参照していたオブジェクトのガベージ コレクションが行われ、リモート オブジェクトのリース期間が満了します。こうした有効期間管理のメカニズムによって、ホストとアドインの基になるメカニズムから複雑性が排除されます。アドインの開発者もホストの開発者も、使用および提供しているオブジェクト (ビュー) が、リソース用のメモリを備え、GC によって管理されるローカル リソースであるかのようにプログラミングを行うことができます。

こうした参照メカニズムは、ホストに実装される形式でも、アドインに実装される形式でも機能します。アドインの有効期間を判断する場合も同じメカニズムを使用します。ホストがアドイン (オブジェクトの 1 つにすぎません) への参照を解放すると、アドインがアンロードされます。そのアドインが、ホストが所有する AppDomain で実行されている最後のオブジェクトの場合は、パイプラインによって AppDomain がアンロードされます。

CalculatorContract2ToView2HostAdapter.cs 内の以下のサンプル コードは、有効期間管理が驚くほど容易に実装されることを示しています。

private System.AddIn.Pipeline.LifetimeTokenHandle _handle;
_handle = new System.AddIn.Pipeline.LifetimeTokenHandle(contract);

LifetimeTokenHandle クラスは、境界を越えて受け渡されるオブジェクトの有効期間を管理するために、IContract を実装するクラス (ほとんどの場合はシステム提供の ContractBase クラス) と連携して機能します。

ContractToViewAdapter (コントラクトの受け取り側) で LifetimeTokenHandle クラスのインスタンスが作成されるときに、コントラクトから LifetimeToken を取得します。GC では、そのアダプタへの参照が存在しなくなったと判断すると、オブジェクトのファイナライズを行ってから、LifetimeToken を破棄します。一方 ViewToContractAdapter (コントラクトの送信側) では、アダプタが未解決の LifetimeToken を監視し、参照カウントがゼロ (0) になった時点で、自身のガベージ コレクションを行うことができます。さらに、これがアドインの AppDomain で、ホストからそのドメインへの参照が他にない場合は、AppDomain がアンロードされます。

ホストとアドインの互換性

ホスト (ホスト ビュー) またはそのデータ型のバージョンが変化すると、古いアドインを新しいホスト ビューで機能させるために、ホストでは 2 つ目のアドイン側アダプタを作成します。このアダプタは、新しいコントラクトから継承し、新しいデータ型を理解し、そのデータ型を元のアドイン ビューに変換します。ホストまたはパイプラインの開発者は、新しいメソッドに既定の機能を提供する必要があります (図 6 参照)。

このようなアダプタのサンプル コードは、ダウンロードに含まれる AddInSideV1toV2Adapter.csproj にあります。実装方法の完全な詳細については、サンプルを参照してください。ただし、重要な部分はクラスとクラスのコンストラクタの定義です。

図 6 新しいホストと古いアドインの下位互換性
図 6 新しいホストと古いアドインの下位互換性(画像を縮小するには、ここをクリックします)
図 6 新しいホストと古いアドインの下位互換性
図 6 新しいホストと古いアドインの下位互換性 (画像を拡大するには、ここをクリックします)

図 7 で重要なのは V1 アドインを V2 コントラクトに適合させている部分です。ここでは V2 コントラクト (ICalculatorContracts2) から継承し、V1 アドイン ベースをコンストラクタ (Calculator) に取り込むアドイン アダプタをビルドする必要があるだけです。

Figure 7 CalculatorV1ViewToV2ContractAddInAdapter (Close Figure 7)
[AddInAdapter]
public class CalculatorV1ViewToV2ContractAddInAdapter : ContractBase,
    ICalculatorContract2
{
    CalculatorContractsBase.Calculator _view;

    public CalculatorV1ViewToV2ContractAddInAdapter(Calculator calc)
    {
        _view = calc;
    }

    public string GetAvailableOperations()
    {
        return  "+, -, *, /" ;
    }

    public double Operate(string operation, double a, double b)
    {
        switch (operation)
        {
            case "+": return _view.Add(a, b);
            case "-": return _view.Subtract(a, b);
            case "*": return _view.Multiply(a, b);
            case "/": return _view.Divide(a, b);
            default:
                throw new InvalidOperationException(
                    "This add-in does not support: " + operation);
        }
    }
}

この方式のメリットは、変更があっても古いアドインが引き続き機能し、新しいアドインが新しいオブジェクト モデルをビルドおよび利用できる点です。アプリケーション自体は異なるバージョンのアドインを監視する必要はなく、ホストはホスト ビューを 1 つだけ処理します。バージョンごとに異なるアドイン アダプタがホスト ビューを別のバージョンのアドインに変換します。これにより、ホストのコード ベースが非常に単純になり、ホストにはレガシ バージョンのアダプタを単に同梱しないことで、そのバージョンへのサポートを削除できます。

まとめ

このモデルを設計するにあたって目標にしたことは、バージョンの変化に柔軟に対応できる、分離可能なコンポーネントを動的に構成できるようにすることでした。アドイン開発者の作業が、この目標の達成に関係するしくみによって煩雑になることを確実に回避することに、主に注意を払いました。

System.AddIn モデルは、System.AddIn 名前空間にある BCL と、コンポーネント間の通信に適用される設計パターンのガイダンスから構成されます。今月のコラムでは、パイプライン アーキテクチャ、パイプラインの実装、有効期間管理、およびこのモデルを使用してホストとアドインのバージョンを独立させ互換性を確保する方法について説明しました。

ご意見やご質問は、

James clrinout@microsoft.com..

までお送りください。
Jack Gudenkauf は、Microsoft の CLR チームに所属するソフトウェア アーキテクトです。現在、.NET アーキテクチャの採用に関する問題を確認するため、ISV/VAR/SI のお客様の応対に精力的に当たっています。Jack は Development Manager、MIS Director、ソフトウェア エンジニアリング、およびコンサルティング サービスなどの役職、業務に携わっています。Microsoft 入社前は、自身で起業したコンサルティング会社 The Logical Choice を経営していました。

Jesse Kaplan は、Microsoft で CLR チームの Program Manager for Extensibility and Add-Ins を務めています。これまでの経験としては、互換性とマネージ/ネイティブ相互運用性などがあります。


テストの実行: カスタム変換ストリームをテストする

テストの実行
カスタム変換ストリームをテストする
Dr. James McCaffrey

コードのダウンロード : : TestRun2007_03.exe (156KB)
コンテンツ
RotateStream クラス
カスタム RotateStream クラスをテストする
2 つの追加シナリオ
サイズを維持する
まとめ

Microsoft .NET Framework の使用経験がない開発者が最初に習得するのは、System.IO 名前空間の FileStream クラスのメソッドを使用してテキスト ファイルに対してデータの読み取りと書き込みを行う方法です。しかし、.NET の I/O ストリーム モデルでは、カスタム ストリーム クラスを作成する強力な方法も用意されています。今月のコラムでは、このようなカスタム ストリーム クラス (特に、別のストリームから読み取ったり、別のストリームに書き込んだりするデータを変換するクラス) のテストに使用できる重要な手法を紹介します。

図 1 のスクリーンショットから、このコラムの最終目的を感じ取ってください。図 1 の出力を生成したプログラムの完全なソース コードは、このコラムのコード サンプルに含まれています。

FileStream オブジェクトは、プログラムの抽象化です。このオブジェクトを使用すると、バックグラウンドで行われている多くの技術的なことに対処する必要なく、ファイルに対して簡単に読み取りと書き込みの処理を行うことができます。図 1 に示した最初の数行の出力は、次のように FileStream クラスを使用してファイルからデータを読み取った場合の一般的な例を示しています。

FileStream fs = File.OpenRead("..\\..\\TextFile1.txt);
byte [] bytes = new byte[fs.Length];
fs.Read(bytes, 0, bytes.Length);
Console.WriteLine("TextFile1.txt contains: " +
    Encoding.UTF8.GetString(buffer));
Console.WriteLine("File in hexadecimal   : " +
    BitConverter.ToString(bytes));

その後に続く数行の出力は、次のように私が実装したカスタム ストリーム クラスと FileStream クラスを組み合わせて使用する例を示しています。

fs.Position = 0;
Console.WriteLine("Filtering TextFile1.txt through " +
                   "custom RotateStream");
RotateStream rs = new RotateStream(fs, 2);
int b = rs.ReadByte();
Console.WriteLine(
    "First byte through the transform is " + b.ToString("X") + "h");

Position プロパティを使用して FileStream オブジェクトの現在の位置をストリームの先頭にリセットした後、RotateStream という名前のプログラム定義のカスタム ストリーム クラスのインスタンスを作成します。このクラスでは、ビット回転の変換を行います (これについては、この後説明します)。RotateStream コンストラクタでは 2 つの入力引数を受け取ります。1 つ目の引数はストリームです (この場合は、ファイル TextFile1.txt と関連付けられている FileStream です)。2 つ目の引数は、変換処理で回転するビット位置の数です (この場合は、2 です)。私が作成したカスタム RotateStream クラスの ReadByte メソッドを呼び出すと、入力の先頭バイトが読み取られ、2 ビット左回転して変換されます。つまり、入力の先頭バイトが 'H' = 0x48 (バイナリ値 01001000) の場合、RotateStream フィルタを介して読み取ると、結果は 0x21 (バイナリ値 00100001) になります。

アプリケーションの開発者やテスト担当者がカスタム ストリーム クラスに馴染みがない理由の 1 つには、このクラスが必要不可欠のものではないことが挙げられます。つまり、カスタム ストリーム クラスを作成しなくても、カスタム ストリームの実装による恩恵を受けられます。たとえば、図 1 の出力は、次のコード行を使用して実現できます。

create a FileStream object, fs
associate fs with file TextFile1.txt
read a byte with fs.ReadByte()
rotate the byte 2 positions to the left
display the transformed byte

図 1 カスタム I/O ストリームのデモ
図 1 カスタム I/O ストリームのデモ(画像を縮小するには、ここをクリックします)
図 1 カスタム I/O ストリームのデモ
図 1 カスタム I/O ストリームのデモ (画像を拡大するには、ここをクリックします)

つまり、組み込みの .NET ストリーム クラスをバイト レベルのプログラミング手法と組み合わせて、カスタム ストリーム クラスと同じ効果を実現することができます。

では、なぜカスタム ストリーム クラスを記述する必要があるのでしょうか。カスタム ストリーム クラスを記述することが考慮に値する主な理由は 2 つあります。1 つ目は、.NET ストリームがファイルや TCP/IP ソケットなどのバイト シーケンスの抽象化であり、.NET ストリーム オブジェクトは (通常、各コンストラクタを介して) 簡単に相互に組み合わせることができるので、カスタム ストリーム クラスでは、基盤となっているデバイス、トランスポート、またはアルゴリズムの詳細に煩わされることなく、あらゆる種類の入力と出力の汎用的なビューを提供できる点です。2 つ目は、バイト レベルの入力、出力、および変換ルーチンをクラスにラップすることで、コードをモジュール化し、テストと管理が簡略化される点です。

このコラムでは、テストすることを正確に理解するために、まず、カスタム RotateStream クラスの実装について説明します。その後、カスタム ストリーム クラスのテスト方法について説明します。最後に、各自のニーズを満たすために、このコラムで示したコードや手法を調整および拡張する方法について簡単に説明します。カスタム ストリーム クラスは、仕事の役割に関係なく、確実に各自のスキル アップにつながります。

RotateStream クラス

一般的なカスタム ストリーム クラスでは、バイトレベルで何らかの変換処理を行います。たとえば、System.Security.Cryptography 名前空間の CryptoStream クラスでは、Advanced Encryption Standard (AES) などのいくつかの手法を使用してバイトをエンコードおよびデーコードできます。通常、ストリーム クラスには読み取りと書き込みの両方のメソッドが実装されています。また、通常、変換ストリーム クラスでは、バイトのエンコードに書き込みメソッドを使用し、バイトのデコードに読み取りメソッドを使用します (ここでは "エンコード" と "デコード" という用語はそれほど厳密な意味では使用していませんが、"書き込み操作によって行われるバイト変換" や "書き込み操作によって行われた変換操作を元に戻すバイト変換" など、より的確なフレーズを使用するよりもわかりやすいので、これらの用語を使用しています)。

このセクションでは、クラス Write メソッドでビット レベルの右回転の変換 ("エンコード") を実行し、クラス Read メソッドでビット レベルの左回転の変換 ("デコード") を実行するカスタム RotateStream ストリーム クラスの実装について説明します。カスタム RotateStream クラス全体の構造は、図 2 のようになります。

Figure 2 カスタム ストリーム クラスの構造 (Close Figure 2)
public class RotateStream : Stream
{
  private Stream stream;
  private int n;

  public RotateStream(Stream stream, int n) { ... }

  public override bool CanRead { ... }
  public override bool CanWrite { ... }
  public override bool CanSeek { ... }
  public override long Length { ... }
  public override long Position { ... }

  public override void Flush() { ... }
  public override void Close(){ ... }
  public override long Seek(long offset, SeekOrigin origin) { ... }
  public override void SetLength(long value) { ... }

  public override int Read(byte[] array, int offset, int count) { ... }
  public override void Write(byte[] buffer, int offset, int count) { ... }

  private static byte ROR(byte b, int n) { ... }
  private static byte ROL(byte b, int n) { ... }
}

カスタム ストリーム クラスは、System.IO.Stream クラスから直接または間接的に派生しています。Stream クラスは、すべての .NET ストリーム クラスの基礎となる抽象基本クラスです。

カスタム RotateStream クラスでは 2 つのプライベート フィールド メンバが必要です。stream という名前の Stream オブジェクトは、Read メソッドの処理ではソース ストリームとして、Write メソッドの処理ではターゲット ストリームとして機能する汎用的なバイト シーケンスを表します。n という名前のフィールドは、Read メソッドと Write メソッドで回転させるビット位置の数を保持します。カスタム RotateStream コンストラクタでは、バイト変換と関連付けられる既存のストリームと、回転するビット位置の数を Read メソッドと Write メソッドに指定する整数値を受け取ります。一部のプログラミング言語 (特に Java) では、ストリーム クラスは読み取りまたは書き込みのいずれかの処理を行うクラスで、両方には対応していません。しかし、.NET ベースのカスタム ストリーム クラスは、読み取りと書き込みの両方に対応するようにすることができます。

カスタム ストリーム クラスを作成する際には、CanRead プロパティと CanWrite プロパティに get アクセサを実装して、呼び出し側のプログラムがこれらの機能を照会できるようにする必要があります。また、CanSeek プロパティを実装して、カスタム ストリーム クラスがストリーム内のランダムな位置へのシークをサポートしているかどうかを呼び出し側のコードが判断できるようにする必要もあります。これ以外に、抽象基本プロパティとして Length と Position も実装する必要があります。ストリームがシークに対応していない場合、これらのプロパティは例外をスローするために実装されます。

カスタム ストリームを作成する際には、合計で 5 つのメソッドを実装する必要があります。もちろん、Read メソッドと Write メソッドは、カスタム ストリーム クラスにおいて重要なメソッドです。しかし、Flush、Seek、および SetLength メソッドも実装する必要があります。必須メソッドではありませんが、私は Close メソッドも実装しました。そして、最後に、カスタム ストリーム クラスの Write メソッドと Read メソッドのそれぞれで使用する 2 つのプライベート ヘルパ メソッド ROR (右に回転) と ROL (左に回転) を実装しました。

これまでの説明では、多くのルーチンをオーバーライドする必要があり大変な作業に見えるかもしれませんが、これらのオーバーライドの大半は 1 行のコードを記述するだけで済みます。これについては、この後で説明します。

カスタム RotateStream コンストラクタでは、次に示すように、単に入力引数を対応するクラスのフィールドにコピーしているだけです。

public RotateStream(Stream stream, int n)
{
  if (stream == null) throw new ArgumentNullException("stream");
  if (n <= 0 || n >= 8) throw new ArgumentOutOfRangeException("n");
  this.stream = stream;
  this.n = n;
}

では、カスタム RotateStream クラスの実装を始めましょう。まずは、Write メソッドと Read メソッドのコーディングを行います。次に示すように、Write メソッドでは、出力ストリームにバイトを配置し、ビット右回転の変換を実行します。

public override void Write(byte[] buffer, int offset, int count)
{
  byte[] temp = new byte[count];
  for (int i = offset, j = 0; i < offset + count; ++i, ++j)
    temp[j] = ROR(buffer[i], this.n);
  stream.Write(temp, 0, count);
}

抽象基本クラス Stream の Write メソッドをオーバーライドしているため、カスタム Write メソッドのシグネチャは、FileStream.Write など馴染みのある他の Write メソッドのシグネチャに対応します。buffer パラメータは、出力ストリームに配置する変換前のバイトの配列を保持しています。offset パラメータはインデックスが 0 から始まります。このパラメータは buffer 配列をポイントし、Write メソッドにバイトの取得開始位置を指示します。count パラメータは、buffer 配列から何バイトを出力ストリームに配置するのかを Write メソッドに指示します。カスタム Write メソッドの本文では、まず、適切なサイズのローカル バイト配列を割り当てます。

(このコラムは説明を第一の目的としており、本題であるカスタム ストリームをできるだけ明確にするため、エラー チェックのコードは割愛しました。実稼働環境で使用する際には、buffer パラメータが空でないかどうかの確認など、通常のエラー チェック コードを追加する必要があります。)

ローカル バイト配列を割り当てた後、入力バイト配列で offset で指定されている位置から反復処理を行い、ローカル配列に右回転の変換をした各バイトを格納します。ローカル配列にバイトが設定されると、この配列は出力ストリームに配置されます。この処理は、RotateStream に接続しているストリームの Write メソッドを呼び出すことで行います (RotateStream コンストラクタは既存のストリームを RotateStream に関連付けます)。ROR ヘルパ メソッドでは、次のような従来のビット操作の演算を行います。

private static byte[] _mask =
    { 0, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F };

private static byte ROR(byte b, int n)
{
    int val = b;
    return (byte)
        (((val &  _mask[n]) << (8 - n)) |
         ((val & ~_mask[n]) >> n));
}

右シフト演算と右回転演算の違いを簡単に確認しておきましょう。右シフトでは、上位ビットから下位ビットにビットを右方向に移動し、データのなくなった上位ビットには 0 を設定します (符号付きのバイトを処理している場合は状況が多少異なりますが、.NET の System.Byte 型は符号なしです)。たとえば、バイト 11011101 を 2 ビット右シフトすると、その結果は 00110111 になります。下位ビットは右側からあふれて消えます。これに対して、右回転演算では、下位ビットの値を確認してから、右シフト演算を実行します。右回転演算では、データがない上位ビットに 0 を設定するのではなく、下位ビットに設定されていた値を設定します (これにより、効率よくビットを回転しています)。たとえば、バイト 11011101 を 1 ビット右回転すると、その結果は 11101110 になります。また、この結果をさらに 1 ビット右回転すると、その結果は 01110111 になります。図 3 に、右回転演算の概念を示します。

図 3 1 ビット右回転する
図 3 1 ビット右回転する(画像を縮小するには、ここをクリックします)
図 3 1 ビット右回転する
図 3 1 ビット右回転する (画像を拡大するには、ここをクリックします)

右回転演算をコードに実装する方法はいくつかあります。単純なソリューションは、最下位ビットのコピーを作成し、値全体を 1 ビット右シフトし、最下位ビットの値を最上位ビットに設定することです。この処理は n 回行えます (n は、回転するビット位置の数になります)。しかし、このアプローチには時間がかかります。この演算はストリームから読み取るバイトごとに行われます。つまり、ホット パスにあるため、この演算の効率化を図る必要があります。

前のコード例に示したように、より高速なソリューションでは、ルックアップ テーブルを使用して、上位バイトと下位バイトを迅速に入れ替えます (バイトの区切りは、必要な回転の量によって決まります)。たとえば、ビット シーケンス 11011101 と n=5 の場合、バイトの区切りは 110 と 11101 になります。これらを入れ替えて回転を実行すると、その結果は 11101110 になります。

これを実現するために _mask 配列では 8 つの値を持つビット マスクを格納しています。この各値は 00000000、00000001、00000011、00000111、00001111 というように、1 に設定されたビットが右から増えていきます。この配列は、n の値に基づいて適切なマスクを取得するルックアップ テーブルとして使用されます。

カスタム RotateStream クラスの Read メソッドでは、入力ストリームからバイトを抽出する際に左回転演算を行うことで、デコード変換を実装しています (右回転の "エンコード" 演算を実行する RotateStream.Write 呼び出しの結果を元に戻します)。

public override int Read(byte[] buffer, int offset, int count)
{
  int numRead = stream.Read(buffer, offset, count);
  for (int i = offset; i < numRead + offset; ++i)
    buffer[i] = ROL(buffer[i], this.n);
  return numRead;
}

カスタム Write メソッドと同様に、カスタム Read メソッドにも見慣れた標準的なシグネチャがあります。buffer パラメータは、入力ストリームから読み取られた変換済みのバイトを保持します。offset パラメータは Read メソッドに buffer パラメータのどこから入力を開始するのかを示し、count パラメータは Read メソッドに入力から読み取るバイト数を示します。戻り値は、入力から読み取った実際のバイト数です。カスタム Read メソッドの実装では、関連付けられている入力ストリームから配列にデータを読み取り、バッファを反復処理し、ROL ヘルパ メソッドを使用して左回転の変換を実行します。ROL ヘルパ メソッドは ROR ヘルパ メソッドとほぼ同じですが、シフトの方向とマスク配列が異なります (ビットは右からではなく左から設定されます)。

private static byte[] _leftMask =
    { 0, 0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE };

private static byte ROL(byte b, int n)
{
    int val = b;
    return (byte)
        (((val &  _leftMask[n]) >> (8 - n)) |
         ((val & ~_leftMask[n]) << n));
}

別のストリームをラップするカスタム ストリーム クラスを記述する際、一部のメソッドでは、ラップしたストリームの同じメソッドに処理を委任します。たとえば、私が作成したカスタム ストリーム クラスの Seek メソッドは次のように実装されています。

public override long Seek(long offset, SeekOrigin origin)
{
  return stream.Seek(offset, origin);
}

ここでは、コンストラクタでカスタム RotateStream ストリームと関連付けられている基になるストリームの Seek メソッドを呼び出しているだけです。しかし、すべてのストリームで Seek メソッドがサポートされているわけではありません。Seek メソッドはファイルで使用することを第一の目的としています。コンソール I/O ストリーム、パイプから派生したストリーム、NetworkStream オブジェクトなどのストリームでは Seek メソッドはサポートされていません。カスタム RotateStream クラスをこのような種類のストリームと関連付けている場合に Seek メソッドを呼び出すと、NotSupportedExeception 例外がスローされます。そのため、テストではストリームのシーク機能に注意する必要があります。次に示すように、シークをサポートしないカスタム ストリームの対策として、プレースホルダの実装を提供できます。

public override long Seek(long offset, SeekOrigin origin)
{
  throw new NotSupportedException("Seek not supported.");
}

派生した Read メソッドと Write メソッドについても、同じアプローチを使用できます。派生した Read、Write、および Seek メソッドはサポートされる場合とサポートされない場合があるため、カスタム ストリーム クラスを記述する際には CanRead、CanWrite、および CanSeek プロパティを実装して、呼び出し側のプログラム (およびテスト フレームワーク) がカスタム ストリームを照会できるようにする必要があります。ここで使用する Read、Write、Seek クエリ プロパティは次のようになります。

public override bool CanRead { get { return stream.CanRead; } }
public override bool CanWrite { get { return stream.CanWrite; } }
public override bool CanSeek { get { return stream.CanSeek; } }

カスタム RotateStream クラスはいくつかの既存のソース ストリーム (Read メソッドの場合) とターゲット ストリーム (Write メソッドの場合) と関連付けられているので、関連付けられているストリームで各メソッドがサポートされているかどうかを確認できます。通常、これは RotateStream などのような一般的な変換ストリームに対する有効なアプローチです。FileStream クラスや NetworkStream クラスなど、ソースとターゲットの間でコネクタとしての役割を果たすことを第一の目的としたストリーム クラスを記述している場合は、明示的な true や false 値を返すのが一般的です。Seek メソッドを明示的にサポートしない場合は、次のコードを記述します。

public override bool CanSeek { get { return false; } }

カスタム ストリーム クラスに Length プロパティと Position プロパティを実装するのは、CanRead、CanWrite、および CanSeek プロパティを実装するのと類似しています。カスタム ストリーム クラスが変換型で既存のソース ストリームまたはターゲット ストリームと関連付けられている場合、基になる Length プロパティと Position プロパティを返せます。また、例外を返して、これらの操作をサポートしないという選択をすることもできます。Length プロパティをサポートしない場合は、一貫性を保つため、SetLength メソッドもサポートしないようにする必要があります。

最後にオーバーライドする必要があるのは Flush メソッドです。

public override void Flush() { stream.Flush(); }

既に述べたように、ここでも、関連付けられているストリームの Flush メソッドを呼び出しただけです。このメソッドを呼び出すと、ストリームのすべてのバッファがクリアされ、バッファされたデータが出力に書き込まれます。

そして最後に、カスタム ストリームが閉じられたときに基になっているストリームも閉じようにするため、Close メソッドをオーバーライドします。

public override void Close()
{
  stream.Close();
  base.Close();
}

カスタム RotateStream クラスをテストする

変換型のカスタム ストリーム クラスの作成方法について説明したので、今度は、作成したクラスの機能をテストする方法を紹介します。カスタム ストリーム クラスのテストは原理上は簡単で、必要な作業は実装したプロパティとメソッドをテストするだけです。この点で、カスタム ストリーム クラスのテストは API/単体テストと類似しています。図 4 の上部に示す Read メソッドのテストが最も基本的なテストです。図 5 は、Read メソッドのテスト結果を生成するコードです。

Figure 5 Read メソッドのテスト (Close Figure 5)
Console.WriteLine(“\n==== RotateStream.Read test”);
byte[] inputBytes = new byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F };
byte[] resultBytes = new byte[inputBytes.Length];

using(MemoryStream ms = new MemoryStream(inputBytes))
using(RotateStream rs = new RotateStream(ms, 1))
{
  int nr = rs.Read(resultBytes, 0, resultBytes.Length);
  Console.WriteLine(
    “\nReading “ + nr + “ bytes from memory through RotateStream”);
}

string input = BitConverter.ToString(inputBytes);
string expected = “90-CA-D8-D8-DE”;
string actual = BitConverter.ToString(resultBytes);
Console.WriteLine(“\nTest input      = “ + input);
Console.WriteLine(“Expected result = “ + expected);
Console.WriteLine(“Actual result   = “ + actual + “\n”);
Console.WriteLine(actual == expected ? “Pass” : “Fail”);
図 4 Read メソッドと Write メソッドのテスト
図 4 Read メソッドと Write メソッドのテスト(画像を縮小するには、ここをクリックします)
図 4 Read メソッドと Write メソッドのテスト
図 4 Read メソッドと Write メソッドのテスト (画像を拡大するには、ここをクリックします)

すべてのテスト ケースと同様に、このテストでも、入力と予想結果が必要になります。入力はハードコードしたバイト シーケンス (0x48、0x65、0x6C、0x6C、および 0x6F) として作成しました。しかし、次のような文字列から入力を作成することもできます。

string s = "Hello";
byte[] inputBytes = Encoding.ASCII.GetBytes(s);

文字列では大半のバイト パターンを生成できないため、一般的に、これは有効な方法ではありません。

カスタム RotateStream クラスのインスタンスを作成して Read メソッドを呼び出す前に、基になるソース ストリームを設置する必要があります。これは、MemoryStream オブジェクトを作成して行うことができます (MemoryStream コンストラクタではバイト配列を受け取るので、これは有効なアプローチです)。その後、RotateStream オブジェクトのインスタンスを作成し、MemoryStream オブジェクトと関連付け、1 ビット左回転を指定します。

適切な準備が完了したら、RotateStream.Read を呼び出し、配列 resultBytes に結果を設定します。実際の結果と予想結果を比較するために、BitConverter クラスを使用してバイトを書式設定された文字列に変換し、文字列の比較を行いました。このアプローチはあまり効率的ではありませんが、生のバイト値を比較するよりもわかりやすいアプローチです。このテスト ケースをベースに、ビット回転値や入力の異なる場合を検証するテスト ケースを作成しました。

カスタム ストリーム クラスの Write メソッドのテストは、Read メソッドのテストと類似しています。テストを実行すると、図 6 に示すコードによって図 4 の下部に示す出力が生成されます。このテスト ケースでは、Read メソッドのテスト ケースの予想出力結果をテスト入力として使用しています。Read メソッドでは左回転演算を実行し、Write メソッドでは右回転演算を実行するため、予想結果は元のバイト シーケンス 0x48、0x65、0x6C、0x6C、0x6F になります。カスタム RotateStream オブジェクトで変換されたバイトのソース ストリームとして MemoryStream オブジェクトを使用し、実際の結果と予想結果の確認に単純な文字列比較を行います。このテスト ケースは、今後作成する多くのテスト ケースのモデルとして使用できます。今後作成するテスト ケースとしては、さまざまなバイト入力値と回転引数を指定したり、入力の一部のみを記述したり、使用可能なバイト数よりも多い出力を書き込もうとする場合などが考えられます。

Figure 6 Write メソッドのテスト (Close Figure 6)
Console.WriteLine("\n\n==== RotateStream.Write test");
byte[] inputBytes2 = new byte[] { 0x90, 0xCA, 0xD8, 0xD8, 0xDE };
byte[] resultBytes2 = new byte[inputBytes2.Length];
using(MemoryStream ms2 = new MemoryStream(resultBytes2))
using(RotateStream rs2 = new RotateStream(ms2, 1))
{
  rs2.Write(inputBytes2, 0, inputBytes2.Length);
  Console.WriteLine("\nWriting " + (inputBytes2.Length - 0) +
    " bytes to memory through RotateStream");
}

string input2 = BitConverter.ToString(inputBytes2);
string expected2 = "48-65-6C-6C-6F";
string actual2 = BitConverter.ToString(resultBytes2);
Console.WriteLine("\nTest input      = " + input2);
Console.WriteLine("Expected result = " + expected2);
Console.WriteLine("Actual result   = " + actual2 + "\n");
Console.WriteLine(actual2 == expected2 ? "Pass" : "Fail");

カスタム Seek メソッドをテストするには、RotateStream インスタンスを Seek メソッドをサポートするソース ストリームに関連付ける必要があります。ここでは FileStream クラスが適任です (図 7 参照)。"Hello" という文字列を含む TextFile1.txt という名前のテキスト ファイルを作成し、このファイルを参照し、ソース ストリームとして機能する FileStream オブジェクトのインスタンスを作成します。次に、テスト対象のカスタム RotateStream クラスのインスタンスを作成し、ビット回転の引数に 2 を指定します。シーク オフセットは 3 に設定しました。これは TextFile1.txt で使用されている文字で後ろから 2 番目の文字 l をポイントします。その後、RotateStream.Seek を呼び出し、戻り値を確認して、Seek メソッドでエラーが発生していないことを確認します。

Figure 7 FileStream クラスを使用する (Close Figure 7)
Console.WriteLine("\n==== Seek test");
using(FileStream fs = File.OpenRead("..\\..\\TextFile1.txt")
using(RotateStream rs = new RotateStream(fs, 2))
{
  int inputSeek = 3;
  long x = rs.Seek(inputSeek, SeekOrigin.Begin);
  if (x == -1) throw new Exception("Seek operation error");
  int actualByte = rs.ReadByte();
  rs.Close();

  int expectedByte = 0xB1;
  Console.WriteLine("\nInput Seek    = " + inputSeek);
  Console.WriteLine("Expected byte = " + expectedByte);
  Console.WriteLine("Actual byte   = " + actualByte + "\n");
  Console.WriteLine(actualByte == expectedByte ? "Pass" : "Fail");
}

この時点で、FileStream.Read を実行して現在のバイトをフェッチして、そのバイトが 0x6C (つまり、l) であるかどうかを確認できます。しかし、ここでは、継承した RotateStream.ReadByte メソッドを使用しました。ReadByte メソッドは、基本的に、Read メソッドを count 値 1 で呼び出し、結果バイトを返します。入力が 0x6C の場合、予想結果は、この値を 2 ビット右回転変換したもの (つまり、0xB1) になります。一般的に、同時に 2 つのもの (この場合は Seek メソッドと ReadByte メソッド) をテストするのは避けることをお勧めします。同時に 2 つのものをテストした場合にテスト ケースが失敗しても、失敗の原因が明らかにならないからです。しかし、これは単なるガイドラインにすぎません。特に、大半のテスト ケースが成功することが予想されるリグレッション テストでは、このように 2 つのものを同時にテストするのは、個別のテストを行うよりも効率的です。

もちろん、他にも必要なテストはあります。このコラムで紹介したものは出発点にすぎません。たとえば、Seek メソッドの予想結果が例外である場合 (エラー条件をテストしたり、Seek メソッドがサポートされていない場合)、返される例外をキャッチして、テストに合格したことを通知する必要があります。

2 つの追加シナリオ

このコラムで紹介した RotateStream カスタム ストリーム クラスと、このクラスのテストに使用した手法は、アプリケーション プログラミング シナリオにおける一般的なバイト変換カスタム ストリーム クラスの単純な種類とテストです。このコラムで紹介した情報により、独自のカスタム変換ストリームについて理解し、テストすることは可能です。しかし、このコラムで説明していない主要なシナリオが 2 つあります。ソース ストリームとターゲット ストリームが存在することを想定しているカスタム変換ストリーム以外にも、"カスタム エンドポイント ストリーム" (仮称) を作成することもできます。カスタム エンドポイント ストリームは、カスタム バイト ソースとターゲットの間でコンジットとして機能することを目的としています。実際のところ、FileStream や NetworkStream などの馴れ親しみのある .NET ストリーム クラスの大半はエンドポイント ストリームです。しかし、このような種類のストリームを作成することはアプリケーション プログラミング環境よりもシステム プログラミング環境の方が一般的ですので、このトピックはこのコラムの対象外としました。

このコラムで説明していないもう 1 つの主要なカスタム ストリームのトピックは、非同期の Read メソッドと Write メソッドの作成とテストについてです。しかし、一般的に、非同期メソッドは、変換カスタム ストリーム クラスではなく、エンドポイント カスタム ストリーム クラスに実装されます。そのため、このトピックもコラムの対象外としました。

サイズを維持する

このコラムで紹介したカスタム RotateStream クラスは、サイズを維持する変換ストリームです。つまり、Read メソッドと Write メソッドで行った変換では入力と出力のサイズが維持されます。しかし、すべての変換ストリームでサイズが維持されるわけではありません。架空の PaddingStream クラスについて考えてみましょう。このクラスは、PaddingStream コンストラクタが FileStream などの既存のソース ストリームと PaddingStream オブジェクトを関連付けるという点において RotateStream クラスと類似しています。PaddingStream.Write メソッドは生データに対して 1 バイトおきに 0 バイトを追加してエンコードし、PaddingStream.Read メソッドは 1 バイトおきに 0 バイトを削除してデコードするとします。0x11223344 という生データがある場合、Write メソッドでは出力ストリームに 0x0011002200330044 という値を配置し、Read メソッドでは、この埋め込み処理を元に戻します。

サイズを維持しないカスタム ストリームについては注意が必要です。さいわいにも、アプリケーション開発シナリオで、サイズを維持しないカスタム ストリームを作成することはほとんどありません。

まとめ

要約すると、.NET 環境ではカスタム ストリーム クラスを簡単に作成できます。アプリケーション開発環境で最も一般的に使用されるカスタム ストリーム クラスでは、バイト レベルの変換を実行し、Write メソッドがエンコーダ、Read メソッドがデコーダの役割を果たします。

カスタム変換ストリームの効果は、既存の .NET ストリーム クラスとビット操作の手法を組み合わせて実現できるので、カスタム ストリーム クラスはアプリケーション開発者やテスト担当者の間ではあまり使用されていなく、認知されていません。しかし、カスタム ストリーム クラスを作成すると、変換コードをモジュール化して、テスト、変更、および管理を簡略化することができます。

ご意見やご質問は、

James testrun@microsoft.com.

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


Cutting Edge: ASP.NET クエリ文字列を検証する

Cutting Edge
ASP.NET クエリ文字列を検証する
Dino Esposito

コードのダウンロード : : CuttingEdge2007_03.exe (168KB)
コンテンツ
問題点
戦略を定義する
宣言型のクエリ文字列
クエリ文字列 HTTP モジュールをコーディングする
クエリ文字列を検証する
検討事項と代替策
まとめ

何年にもわたって ASP 開発者は、各ページの先頭に、ユーザー資格情報の取得、Cookie の割り当て、ユーザーのリダイレクトを行う汎用的なコードを挿入してページの認証機能を実装してきました。このように繰り返し使用していたコードは、認証用の ASP.NET HTTP モジュールを使用することで廃止できます。その結果、ASP.NET アプリケーションでは、セキュリティ保護されているすべてのページを認証モジュールにリンクする必要がなくなります。すべての処理は web.config ファイルとログイン ページやメンバシップ データベースなどの外部リソースでの宣言により行えます。

ASP.NET では、繰り返しのコードを最小限に抑えたり、Web アプリケーションの一般的な機能の実装を合理化する他のシステム モジュールやプログラミング手法も導入されました。たとえば、サイト マップ、匿名ユーザー、およびプロファイルは組み込みの機能になり、開発者は、これらの機能を使用するために何度もコードを記述したり、コピーしたりする必要がなくなりました。

セキュリティに重点を置いたことで、ASP.NET には適切な数のネイティブな防御策が設けられることになりました。これにより、開発者が考えられる攻撃の形態の一部については入力データを照合する必要がなくなりました。もちろん、ASP.NET アプリケーションのセキュリティが設計によって確保されるというわけではありませんが、これまでと比較すると既定のセキュリティ レベルは確実に高くなります。しかし、このセキュリティ レベルをさらに高められるかどうかは、開発者の腕にかかっています。

ASP.NET ページは、ページ自体にデータを投稿し、入力パラメータを HTTP POST パケットの本文にグループ化するように設計されています。大半の ASP.NET アプリケーションでは、入力データを渡す際に、従来の ASP アプリケーションほどクエリ文字列を使用しません。それでも、クエリ文字列が、外部データを ASP.NET ページにインポートする適切な方法であることには変わりありません。しかし、このデータはだれが検証するのでしょうか。

最近の統計結果によると、クロスサイト スクリプティング (XSS) 攻撃が猛威を振るっており、確認されている攻撃で最も多くの割合を占めています。XSS 攻撃を受けてしまう場合、その原因は入力データが検証されていないこと、または適切に検証されていないことにあり、このような入力データは、たいていクエリ文字列から取得されたものです。

ASP.NET Version 1.1 以降、ASP.NET では、投稿されたデータ (フォームやクエリ文字列) を事前処理し、XSS 攻撃者によって送信される可能性のある疑わしい文字の組み合わせがないかどうかを確認します。しかし、この防御策は問題を解決する確実な方法ではありません。2006 年 11 月の MSDN® Magazine の記事「安全な習慣 : より安全なコードを開発するための 8 つの簡単なルール」(msdn.microsoft.com/msdnmag/issues/06/11/SecureHabits/default.aspx?loc=jp) で Michael Howard が説明したように、開発者が確実に問題を解決する必要があります。ASP.NET ページでクエリ文字列パラメータを使用する場合は、パラメータを使用する前に適切な検証を行う必要があります。では、どのように検証したらよいのでしょうか。

このコラムでは、予想されるクエリ文字列の構造をハードコードした XML ファイルを読み取る HTTP モジュールを作成します。このモジュールは、指定のスキーマに対して要求されたページのクエリ文字列を確認します。このモジュールの使用にあたって ASP.NET ページのコードを変更する必要はありません (XSS 攻撃を回避する方法の詳細については、「Microsoft Anti-Cross Site Scripting Library v1.5」(英語) を参照してください)。

問題点

開発者は、クエリ文字列から何の検証も行わずに入力データを取得するページを野放しにしておくわけにいはいきません。値を検証し、クエリ文字列の形式も入念に確認する必要があります。この検証プロセスは、静的な検証と動的な検証という 2 つの独立した手順で構成されます。静的な検証では要求されたパラメータの種類と存在の有無を確認し、動的な検証では指定された値がコードの他の部分で予想されているものと整合性があるかどうかを確認します。動的な検証はページによって異なるため、外部のページにとらわれないコンポーネントに委任することはできません。それに対して、静的な検証は、ページを初期化する必要のない汎用的なチェック リスト (必要なパラメータ、種類、長さなど) に基づいて行われます。

従来のセキュリティで保護されたすべての ASP ページに認証のための汎用的なコードを含める必要があったのと同様に、ASP.NET でも各ページにクエリ文字列の検証コードを含める必要があります。ASP.NET では認証の標準コードを組み込みの HTTP モジュールの小さなグループに移行しましたが、クエリ文字列には対処していません。しかし、最近の XSS や SQL インジェクション攻撃の増加により、入力元を照合しなければならないという問題が生じています。クエリ文字列の静的な検証を厳しく行う外部コンポーネントをアプリケーションにリンクすると、宣言されているスキーマに準拠しないクエリ文字列を含む ASP.NET ページの要求が実行されることがなくなるので、このような外部コンポーネントは大いに役立ちます。

さらに重要なことには、外部コンポーネントを使用すると、ページのソース コードを変更する必要がありません。必要な作業は、構成ファイルを使用してアプリケーションに外部コンポーネントを登録し、ページごとにクエリ文字列の構文を指定する XML ファイルを追加するだけです。次のセクションでは、この方法について詳しく説明します。

戦略を定義する

ASP.NET では、要求されたページのクラスがインスタンス化および処理される前に、実行時のパイプラインに独自のコードを挿入するためのツールとして HTTP モジュールを提供しています。構文の観点から見ると、HTTP モジュールは、指定のインターフェイスを実装する単なるクラスです。より広範なアーキテクチャの観点から見ると、HTTP モジュールは、アプリケーションと同じ有効期間を持つ監視者ということができます。HTTP モジュールでは、アクティビティを処理する要求を監視し、BeginRequest、EndRequest、PostMapRequestHandler などのいくつかの特定のイベントをリッスンするように登録されます。ASP.NET 要求のアプリケーション イベントの一覧については、「System.Web.HttpApplication イベント」(msdn2.microsoft.com/0dbhtdck.aspx) を参照してください。

HTTP モジュールは、インストールすると、監視対象のイベントが ASP.NET ランタイムによって処理された要求で発生するたびに呼び出されます。ASP.NET ランタイムでは、ASP.NET アプリケーションでホストされているすべてのリソースに対する要求を処理するわけではありません。ASP.NET アプリケーションで静的なリソースを処理することが IIS によって許可されていない限り、既定では、カスケード スタイル シート (CSS) や JPG ファイルなどの静的なリソースは、ASP.NET アプリケーションを介さず Web サーバーで直接処理されます。

クエリ文字列 HTTP モジュールは BeginRequest イベントをリッスンし、以前に読み込まれたスキーマに対してクエリ文字列のコンテンツを検証します。パラメータの数が一致し、指定されている値が予想される種類の値に適合する場合、要求は次の段階に進むことができます。この条件に満たない場合、要求は適切な HTTP ステータス コードで終了されるか、ASP.NET 例外がスローされます。

クエリ文字列の構文を格納する XML ファイルについては既に言及しましたが、このファイル形式は XML である必要はありません (XML ファイルを使用すると、開発者が好きなようにスキーマを決められるというメリットがあります)。必要なのはページのクエリ文字列の予想される構造に関する情報を宣言的に保存するデータ ソースです。単純な XML ファイルを使用することも、高度なプロバイダ ベースのサービスを使用することもできます。私が 2006 年 6 月に執筆したコラム (msdn.microsoft.com/msdnmag/issues/06/06/CuttingEdge、英語) では、プロバイダを使用するように設計したカスタム アプリケーション サービスの例を紹介しましたので、必要な場合は参照してください。

宣言型のクエリ文字列

図 1 は、クエリ文字列 HTTP モジュールで認識される XML ファイルとスキーマのサンプルです。ルート ノード <querystring> の下には、アプリケーションに含まれていて、クエリ文字列の値を処理する可能性があるページと同じ数の <page> ノードがあります。図 1 に示したファイルは web.querystring というファイル名で、このコラムのダウンロード コードに含まれています。もちろん、スキーマと同様に、ファイルにも任意の名前を付けることができます。

Figure 1 web.querystring サンプル構成ファイル (Close Figure 1)
<!--
<page url="..." abortOnError="TRUE|false">
  <param name="..."
         type="Int|Text|Bool"
         optional="FALSE|true"
         length="number (for Text type only)"
         casesensitive="false|true" />
</page>
-->

<querystring>

  <page url="/source/test.aspx" abortOnError="true">
    <param name="id" type="Int" optional="true" casesensitive="true" />
    <param name="code" type="Text" length="5" optional="false" />
    <param name="detailed" type="Bool" optional="false" />
  </page>

  <page url="/source/Test1.aspx" abortOnError="false">
    <param name="guid" type="Int" />
  </page>

</querystring>

(セキュリティの観点から、ここで重要なのは、ページでクエリ文字列から値を受け取ることではなく、クエリ文字列から取得した値を使う可能性があることです。ページに含まれているコードで、クエリ文字列から渡された入力データを処理する場合、開発者は、そのデータが安全で悪意がないものであることを保証する必要があります。そのため、XML ファイルには、クエリ文字列から渡されるデータを実際に使用するアプリケーションのページについてのみ、<page> ノードを追加することをお勧めします。)

サンプル スキーマの <page> 要素には url および abortOnError という 2 つの属性があります。1 つ目の属性ではページの相対 URL を指定しています。2 つ目の属性は、入力が不適切な場合にページの要求を中断するかどうかを指定する省略可能なブール値の属性です。ページの要求を中断するように指定すると、クエリ文字列で不適切な入力データが検出された場合は指定された処理に基づいて、ユーザーには HTTP エラーまたは ASP.NET 例外が返されます。指定した処理が何であれ、ASP.NET ページのコードを編集する必要はありません。HTTP モジュールでは、ページ クラスが認識されインスタンス化される前に、要求を中断することができます。

これには、別のアプローチもあります。このアプローチでは、HTTP モジュールは要求に対して処理を行いません。代わりに、HTTP コンテキストに詳細な情報を追加して、ページのクラスに検出した情報を通知します。ページ側では、この情報に基づいて、アドホックなエラー ページを表示するなど適切な対策を講じる必要があります。この場合、ページの作成者は、アプリケーションのエラー処理コンテキストにクエリ文字列の例外を含める必要があります。このアプローチの欠点は、クエリ文字列を使用する各ページのコードを変更する必要があることです (これについては、後で詳しく説明します)。

既定では、abortOnError 属性は true に設定されます。つまり、クエリ文字列に例外データが含まれている場合にはページの要求が中断されます。各 <page> ノードの下にはサポートされている各クエリ文字列パラメータに対応した <param> ノードがあります。サンプル コードでは、パラメータの定義には図 2 に示す属性を使用できます。

Figure 2 <param> ノードでサポートされている属性 (Close Figure 2)

属性 | 説明
Name | クエリ文字列パラメータの名前を示します。
Type | クエリ文字列パラメータの型を示します。この属性に設定できる値は Text、Int、または Bool です。
Optional | パラメータが省略可能であるかどうかを示すブール値の属性。既定値は false です。
Length | Text 型のパラメータの最大長を示します。
CaseSensitive | パラメータの名前で大文字と小文字を区別するかどうかを示すブール値の属性。既定値は false です。つまり、クエリ文字列のパラメータは、任意の小文字と大文字の組み合わせを使用して指定できます。

ASP.NET では、クエリ文字列に渡されるすべての値を文字列として受け取ります。そのため、HttpRequest オブジェクトの QueryString プロパティは、キーと値が文字列になる NameValueCollection オブジェクトになります。しかし、文字列の形式は、単なるシリアル化の形式です。各クエリ文字列パラメータは、文字列だけではなく、ブール値や数値を表すことができます。また、URL、GUID、ファイル名など特殊な文字列のサブタイプも表すことができます。つまり、web.querystring ファイルでは、次に示すようにカスタム enum 型である QueryStringParamTypes の値を使用して予想されるパラメータの種類を指定します。

Friend Enum QueryStringParamTypes As Integer
    Text = 1
    Int = 2
    Bool = 3
End Enum

このサポートされているパラメータの種類のリストは、さまざまな数値型を追加して拡張することもできます。Text 型のパラメータについては、属性 Length を使用して最大長を指定できます。たとえば、クエリ文字列から 5 文字の顧客 ID を受け取るページについては、パラメータの長さを制限することをお勧めします。また、web.querystring ファイルを使用して、パラメータ名の大文字と小文字を確認したり、パラメータをオプションとして指定したりすることもできます。web.querystring ファイルのコンテンツは、クエリ文字列 HTTP モジュールで解析され、メモリ内オブジェクトに変換されます。

クエリ文字列 HTTP モジュールをコーディングする

図 3 に、クエリ文字列 HTTP モジュールのソース コードを示します。既に説明したように、HTTP モジュールのクラスは Init メソッドと Dispose メソッドで構成されている IHttpModule クラスを実装しています。これらのメソッドは、アプリケーションのコンテキストでモジュールがロードまたはアンロードされたときに呼び出されます。Init メソッドでは、通常、HTTP モジュールは、監視対象となるアプリケーション イベントのリスナを登録します。この場合、BeginRequest イベントのハンドラを登録します。また、HTTP モジュールでは、web.querystring ファイルを処理し、このファイルのコンテンツのメモリ内表現を作成します。Init メソッドは、1 つのアプリケーションにつき一度しか呼び出されません。構成ファイルのコンテンツは最初に読み取られキャッシュされるので、web.querystring ファイルに加えた変更は、Web アプリケーションが再起動されるまで検出されません。実稼働環境では、アプリケーションを停止して再起動することなく、web.querystring ファイルに変更を加えることはほとんどないので、このことが問題になるわけではありません。しかし、図 3 のコードを拡張して、web.querystring ファイルに加えられた変更を検出して、ファイルを適宜リロードするファイル ウォッチャー オブジェクトを使用することもできます。

Figure 3 QueryStringModule クラス (Close Figure 3)
Imports System
Imports System.Web
Imports System.IO

Public Class QueryStringModule : Implements IHttpModule

    Private _app As HttpApplication
    Private _queryStringData As Hashtable

    Public Sub Init(ByVal context As System.Web.HttpApplication) _
            Implements System.Web.IHttpModule.Init
        _app = context
        AddHandler _app.BeginRequest, AddressOf OnEnter

        ' Load and cache the XML querystring file
        Dim fileName As String = _
            HttpContext.Current.Server.MapPath("web.querystring")
        _queryStringData = QueryStringHelper.LoadFromFile(fileName)
    End Sub

    Public Sub Dispose() Implements System.Web.IHttpModule.Dispose
    End Sub

    Private Sub OnEnter(ByVal source As Object, ByVal e As EventArgs)
        ' Retrieve the query string data structure for the current page
        Dim currentPage As String = _
            HttpContext.Current.Request.Path.ToLower()
        Dim qsDesc As QueryStringDescriptor = _
            _queryStringData.Item(currentPage)

        ' Validate the query string
        Dim isValid As Boolean
        isValid = QueryStringHelper.Validate( _
           HttpContext.Current.Request.QueryString, qsDesc)

        ' Abort the request if validation fails
        If Not isValid Then
            If qsDesc.AbortOnError Then
                HttpContext.Current.Response.StatusCode = 500
                HttpContext.Current.Response.[End]()
            Else
                ' Add information about the error to Context.Items
                HttpContext.Current.Items( _
                    QueryStringHelper.QueryStringValidationStatus) = _
                        QueryStringHelper.GetErrorCode()
            End If
        Else
            ' Add information for the page to the Context.Items
            HttpContext.Current.Items( _
                QueryStringHelper.QueryStringValidationStatus) = _
                    QueryStringHelper.GetErrorCode()

            ' Add typed values
            HttpContext.Current.Items( _
                QueryStringHelper.QueryStringValues) = _
                    QueryStringHelper.GetTypedValues( _
                        HttpContext.Current.Request.QueryString, qsDesc)
        End If
    End Sub
End Class

web.querystring ファイルのコンテンツは、図 4 に示すように QueryStringDescriptor 型のオブジェクトにマップされます。この記述子はページの URL、検証で不適切なデータが検出された場合の処理を指定するフラグ、サポートされているクエリ文字列パラメータのリストを含みます。各パラメータは、QueryStringParamInfo クラスのインスタンスを使用して記述されます。QueryStringParamCollection は、関連のあるコレクション クラスです。このクラスは、Find メソッドのペアを使用して拡張された汎用的なコレクション クラスです。この Find メソッドの 1 つは、コレクション内に指定の名前のパラメータがあるかどうかを確認するのに使用し、もう 1 つはパラメータ記述子のインスタンスを返すのに使用します。

Figure 4 QueryString モジュールのヘルパ クラス (Close Figure 4)
Friend Class QueryStringDescriptor
    Public Url As String
    Public AbortOnError As Boolean
    Public Parameters As QueryStringParamCollection
End Class

Friend Class QueryStringParamInfo
    Public Name As String
    Public [Type] As QueryStringParamTypes
    Public Length As Integer
    Public [Optional] As Boolean
    Public CaseSensitive As Boolean
End Class

Friend Class QueryStringParamCollection : Inherits Collection( _
        Of QueryStringParamInfo)

    Public Overloads Function Contains(ByVal name As String) As Boolean
        For i As Integer = 0 To Count - 1
            Dim comparison As StringComparison = _
                StringComparison.OrdinalIgnoreCase
            Dim currentItem As QueryStringParamInfo = Item(i)
            If currentItem.CaseSensitive Then
                comparison = StringComparison.Ordinal
            End If

            If String.Equals(currentItem.Name, name, comparison) Then
                Return True
            End If
        Next

        Return False
    End Function

    Public Function Find(ByVal name As String) As QueryStringParamInfo
        For i As Integer = 0 To Count - 1
            Dim currentItem As QueryStringParamInfo = Item(i)
            If String.Equals(currentItem.Name, name, _
                    StringComparison.OrdinalIgnoreCase) Then
                Return currentItem
            End If
        Next

        Return Nothing
    End Function
End Class

<Flags()> _
Public Enum QueryStringErrorCodes
    NoError = 0
    TooManyParameters = 1
    InvalidQueryParameter = 2
    MissingRequiredParameter = 4
    InvalidContent = 8
End Enum

クエリ文字列記述子では、指定されたページのクエリ文字列に関する情報をキャッシュします。しかし、web.querystring ファイルでは、複数のページを参照できます。そのため、web.querystring ファイルで参照されるすべてのページの記述子は、ページの URL をキーとして、1 つのハッシュ テーブルにグループ化されます。次のコードは、HTTP モジュールの BeginRequest ハンドラが、現在要求されているページの記述子を受け取る方法を示しています。

Dim currentPage As String
currentPage = HttpContext.Current.Request.Path.ToLower()
Dim qsDesc As QueryStringDescriptor = _
    _queryStringData.Item(currentPage)

クエリ文字列記述子は、そのページのクエリ文字列の正しい構文のメモリ内表現です。次は、このスキーマに対して投稿されたクエリ文字列の検証について説明します。

クエリ文字列を検証する

検証プロセスには 3 つの段階があります。まず、モジュールが投稿されたクエリ文字列に含まれるパラメータの数をカウントします。投稿されたクエリ文字列に予想されている数よりも多くのパラメータが含まれている場合、検証は失敗します。次に、モジュールが、投稿されたクエリ文字列パラメータを反復処理し、各パラメータが宣言されたスキーマのエントリと一致することを確認します。不明なパラメータが検出された場合にも、検証は失敗します。最後に、モジュールが、スキーマで定義されているすべてのパラメータを反復処理し、必要なパラメータがすべて指定されていること、指定された各パラメータで適切な型の値が使用されていることを確認します。

データの検証では、特定のパラメータの値を、宣言されている型に対して解析します。次に数値の検証に使用するコードを示します。

If paramType = QueryStringParamTypes.Int Then
    Dim result As Integer
    Dim success As Boolean = Int32.TryParse(paramValue, result)
    If Not success Then Return False
End If

仕様では、ブール値は true や false などの文字列としてしか解析されませんが、QueryString HTTP モジュールの検証サブシステムでは yes や no などの文字列も受け取ります。

つまり、クエリ文字列のコンテンツは、要求のパイプラインの最初の手順として解析および型検証されます。問題がなければ、要求は処理されます。問題が検出されると、要求は適切な HTTP ステータス コードですぐに終了されます。次に例を示します。

HttpContext.Current.Response.StatusCode = 500
HttpContext.Current.Response.[End]()

ユーザーには、図 5 のようなページが表示されます。このページでは、IIS エラーの本当の原因がわからないと思われるかもしれませんが、HTTP ステータス コードやエラーの概要から、エラーのソース、つまり要求の処理中にサーバー側で内部エラーが発生したことは特定できます。既に言及した Michael Howard のコラムで説明されているように、潜在的なハッカーに詳細情報を提供するリスクを回避するため、エラー ページに表示する情報量はなるべく抑えるという原則に従うことをお勧めします。この点において、HTTP 500 エラーは、発生したエラーを通知する十分な役割を果たしています。また、上記のコードに示すように、コードには任意の HTTP ステータス コードを設定できます。

図 5 不適切なクエリ文字列が検出された場合の結果
図 5 不適切なクエリ文字列が検出された場合の結果(画像を縮小するには、ここをクリックします)
図 5 不適切なクエリ文字列が検出された場合の結果
図 5 不適切なクエリ文字列が検出された場合の結果 (画像を拡大するには、ここをクリックします)

検討事項と代替策

不適切な形式のデータが検出された場合、要求を中断するのが最適な処理になるのでしょうか。それとも、検証の結果をどこかにキャッシュして、ページのコードで最終的な判断を下すのが最適な処理になるのでしょうか。また、要求のライフサイクルの早い段階でクエリ文字列を取得して処理するのは、はたして得策なのでしょうか。まずは、3 つ目の点について説明しましょう。

図 6 は、要求の処理の特徴を表しているアプリケーション全体のイベントを示しています。要求発生時の早い段階でクエリ文字列を確認しない場合は、どのタイミングで確認すればよいのでしょうか。この確認のタイミングとして適切なのは承認の直後です。承認よりも先の段階に要求が進んだ場合、ページ HTTP ハンドラが呼び出されることを当てにできます。

Figure 6 グローバル アプリケーション イベント (Close Figure 6)

イベント | 説明
BeginRequest | 要求処理が開始したことを示します。
AuthenticateRequest PostAuthenticateRequest | 要求の認証処理をラップします。
AuthorizeRequest PostAuthorizeRequest | 要求の承認処理をラップします。
ResolveRequestCache PostResolveRequestCache | 以前にキャッシュされた出力ページを使用して要求を処理できるかどうかを確認する処理をラップします。
PostMapRequestHandler | 要求を処理する HTTP ハンドラが見つかったことを示します。
AcquireRequestState PostAcquireRequestState | 要求のセッションの状態を取得する処理をラップします。
PostRequestHandlerExecute | 要求を処理する HTTP ハンドラが実行されたことを示します。
ReleaseRequestState PostReleaseRequestState | 要求のセッションの状態を解放する処理をラップします。
UpdateRequestCache PostUpdateRequestCache | 要求されたリソースの出力をキャッシュして使用できるかどうかを確認する処理をラップします。
EndRequest | 要求処理が完了したことを示します。

しかし、確認はどのように先延ばしにできるのでしょうか。一般的に、PostAcquireRequestState までのイベント ハンドラであれば問題ありません。コード ページの作成者が分離コード ファイルまたは ASPX ファイルのインラインに記述したユーザー コードは、PostAcquireRequestState イベントより前の段階で実行されることはありません。その後、PostAcquireRequestState グローバル イベントが呼び出されるまで、ページでクエリ文字列を使用することはできません。しかし、このイベントが呼び出されるまで待つ必要はありません。承認が完了してからページが実行されるまでの間にクエリ文字列を確認すると、セッション状態の取得の参照や出力キャッシュの確認などの余分な操作が不要になります。クエリ文字列で不適切なデータが検出された場合にページを終了するなら、セッションの状態を読み込む必要はありません。特に SQL Server™ などのアウト プロセス ソースからセッション状態を読み込む場合はなおさらです。

つまり、クエリ文字列の確認を行う必要があるアプリケーション イベントは BeginRequest と PostAuthorizeRequest の 2 つだけです。クエリ文字列を処理するのにユーザー情報が必要な場合は PostAuthorizeRequest イベントでクエリ文字列の確認を行うことをお勧めします。たとえば、ユーザーの役割によって特定のパラメータを指定できる場合がこれに該当します。この場合、図 1 のスキーマに roles 属性を追加できます。他のシナリオでも、BeginRequest イベントでクエリ文字列を確認することで、パイプラインの初期段階でページを終了し、それ以降の処理を回避できます。

ページ コードで問題のあるクエリ文字列を処理し、適切にレベルを下げたり、回復させたりする場合は状況が異なります。このような理由により、ページの実行よりも前に発生するイベントでは確認処理は問題なく行えると考えられます。ここでは、ページ コードの実行前にクエリ文字列を確認できるパイプラインの最後のポイントである PostAcquireRequestState でクエリ文字列を確認します。ここではセッションの状態も取得できます。この説明の流れを理解する上で大事なことを言いそびれていましたが、組み込みの Request オブジェクトの QueryString コレクションではクエリ文字列の情報が最初から利用できます。

HTTP モジュールでクエリ文字列を確認し、その結果をパイプラインとページ コードに渡す場合を想定しましょう。この場合、使用できるアプローチはいくつかあります。これらのアプローチについて説明する前に、これらのアプローチがコードに影響を与えること、クエリ文字列を使用する各ページのソース コードを変更する必要があることを先にお伝えしておきます。

HTTP モジュールが特定の要求を処理するハンドラと通信する最も簡単な方法は、データを HttpContext オブジェクトの Items コレクションに設定することです。Items プロパティは、HTTP モジュールのハッシュ テーブルであり、情報の書き込みと読み取りを行うハンドラです。Items テーブルに格納されているデータの有効期間は要求と同じになります。

HTTP モジュールは、次のように HttpContext クラスの静的な Current プロパティを使用して現在の要求のコンテキスト オブジェクトにアクセスします。

HttpContext.Current.Items("QueryStringStatus") = errorCode

Items は System.Collections.Hashtable であるため、キーと値は、どちらも任意の .NET 型を取ることができます。QueryString HTTP モジュールでは、次のようにパブリックな enum 型を使用して、可能性のあるすべてのエラー コードをリストします。

<Flags()> _
Public Enum QueryStringErrorCodes
    NoError = 0
    TooManyParameters = 1
    InvalidQueryParameter = 2
    MissingRequiredParameter = 4
    InvalidContent = 8
End Enum

クエリ文字列にどのような問題があるのかをより詳しく説明する、これらのコードの組み合わせは、ハッシュ テーブルの通称 "スロット" と呼ばれる場所に格納されます。HTTP モジュールとページでは、同じ名前付け規則を使用し、ページで、この情報を取得および使用できるようにする必要があります。HTTP モジュールでは、次のようにスロットの名前を表すパブリック定数を定義します。

Public Const QueryStringValidationStatus As String = _
       "QueryStringValidationStatus"

ページでは、次のコードを使用して HTTP モジュールからメッセージを取得し、その情報をどのように処理するかを判断できます。

Dim result As QueryStringErrorCodes = _
    DirectCast(Context.Items( _
        QueryStringHelper.QueryStringValidationStatus), _
        QueryStringErrorCodes)

有効なクエリ文字列から取得した型指定された値もページに提供するとします。次の URL とクエリ文字列が有効な場合について考えてみましょう。

http://www.yourserver.com/page.aspx?detailed=true

ページには、クエリ文字列の値を解析し、その値をブール値に変換するコードを組み込む必要があります。このような変換は、検証の段階で HTTP モジュールによって行われているので、これらの型指定された値を別の Items スロットにこれらの値のハッシュ テーブルを配置してターゲット ページと共有する作業は非常に簡単です (詳細については、ソース コードを参照してください)。

クエリ文字列を使用する各ページに新しい読み取り専用のプロパティを追加するという、より簡単なアプローチもあります。このプロパティに IsValidQueryString という名前を付けた場合は、次のようになります。

Public Property IsValidQueryString As Boolean
   Get
      Dim result As QueryStringErrorCodes = DirectCast( _
        Context.Items(QueryStringHelper.QueryStringValidationStatus), _
            QueryStringErrorCodes)
      Return (result = QueryStringErrorCodes.NoError)
   End Get
End Property

さらに適切なアプローチとしては、このようなプロパティを基本クラスで定義し、すべてのクエリ文字列に対応したページをこのクラスから派生させることもできます。

まとめ

すべての ASP.NET ページでクエリ文字列が使用されるわけではありません。しかし、クエリ文字列は Web ページの入力として使用できます。そのため、セキュリティ ホールのあるページに対する攻撃の対象となりえます。ページでクエリ文字列に対する防御策が必要な場合は、クエリ文字列を使用するすべてのページで同じコードを繰り返し記述する覚悟をしてください。

このコラムで紹介した QueryString モジュールを使用すると、ソース ページでコーディングを行う必要はなく、投稿されたクエリ文字列が別の XML ファイルに保存されている指定のスキーマに対して自動的に確認されます。つまり、既存のコードへの影響はなく、攻撃者に対しては、新たな組み込みの防御策を設置できることになります。ただし、このモジュールが問題を解決するための完全無欠な方法ではないということはお忘れなく。

ご意見やご感想は

Dino cutting@microsoft.com.

まで英語でお送りください。
Dino Esposito は、Solid Quality Learning の指導者であり、『Programming Microsoft ASP.NET 2.0』(Microsoft Press 発行、2005 年) の著者でもあります。Dino はイタリアに在住し、世界各地で開催される業界のイベントで講演しています。cutting@microsoft.com (英語) で連絡を取るか、weblogs.asp.net/despos (英語) のブログに参加してください。


Wicked Code: ASP.NET の非同期プログラミングを使ったスケール変換可能なアプリケーション

Wicked Code
ASP.NET の非同期プログラミングを使ったスケール変換可能なアプリケーション
Jeff Prosise

コードのダウンロード : : WickedCode2007_03.exe (202KB)
コンテンツ
非同期ページ
非同期 HTTP ハンドラ
非同期 HTTP モジュール
まとめ

秘密を知りたいと思いますか。深くて、暗い、汚れた秘密です。この秘密が暴かれると、ASP.NET コミュニティに大きな恐慌を引き起こし、反マイクロソフト派は即座に大声で「やっぱり」と叫ぶことになるでしょう。

ASP.NET を使ってビルドされたほとんどの Web サイトにはまったく拡張性がありません。こうした Web サイトでは、自ら目に見えない障壁を設け、1 秒間に処理できる要求の数を制限してしまうという弱点があります。トラフィックがこのような目に見えない障壁に達するまでは極めて順調にスケールが変換されますが、その後は、スループットが低下し始めます。やがて、要求が失敗するようになり、通常は、"サーバーを使用できません" というエラーが返されます。

根底にある原因は、MSDN®Magazine で何度も取り上げられてきました。ASP.NET では、共通言語ランタイム (CLR) スレッド プールからのスレッドを使用して、要求を処理します。スレッド プール内に使用できるスレッドが存在する限り、ASP.NET で着信する要求のディスパッチに問題は生じません。しかし、スレッド プールが飽和すると、つまり、スレッド プール内のすべてのスレッドが要求の処理でビジー状態になり、空きスレッドが残っていないと、新たな要求はスレッドが空くまで待機しなければなりません。こうした行き詰まりが重症になり、キューの容量がいっぱいになると、ASP.NET はお手上げ状態になり、新しい要求に対して「もうだめだ」という応答を返します。

1 つの解決策は、多くのスレッドを作成できるように、スレッド プールの最大サイズを増やすことです。これは、ユーザーから "サーバーを使用できません" エラーが発生することを繰り返し報告された開発者がよく採用する対策です。もう 1 つ一般的に行われる対策は、Web ファームにサーバーを追加して、問題の解決をハードウェアに委ねる方法です。しかし、スレッド数やサーバー台数を増やすことは、問題の解決にはなりません。これは、現実の ASP.NET 自体の設計上の問題ではなく、実際のサイトの実装上の問題に対する一時的な救済策にすぎません。アプリケーションがスケール変換されない実際の問題は、スレッド数が不足していることではありません。既に存在するスレッドが効率的に使用されないことに問題があります。

ASP.NET Web サイトを本当の意味でスケール変換可能にする方法は、スレッド プールを最適に使用することです。つまり、要求を処理するスレッドが、入出力の完了を待機するのではなく、確実にコードを実行するようにすることです。すべてのスレッドが CPU を使用している状態になることによってスレッド プールが飽和状態になる場合は、サーバーを追加する以外にできることはほとんどありません。

ただし、大部分の Web アプリケーションは、データベースや Web サービスなどの外部エンティティと対話し、スレッド プールのスレッドがデータベース クエリや Web サービス呼び出しなどの入出力操作の完了を待機することによって、スケーラビリティが制限されます。データドリブン Web ページを対象とする要求は、数千秒をコードの実行に費やし、ほんの数秒間、データベース クエリから戻るのを待機します。クエリが未解決の間は、その要求に割り当てられたスレッドは他の要求にサービスを提供することができません。これが目に見えない障壁です。そして、これがスケーラビリティの高い Web サイトを構築する際に注意して回避しなければならない状況です。スループットを考えるときに、入出力を正しく処理しないと弊害が生じることを覚えておいてください。

ただし、入出力がスレッド プールに悪影響を与えていない場合は弊害にはなりません。ASP.NET では、この弊害対策となるエージェントとして機能する 3 つの非同期プログラミング モデルをサポートします。こうしたモデルは、コミュニティにはあまり広く知られていません。これは、ドキュメントの不足にも原因があります。しかし、最先端の Web サイトを構築するには、こうしたモデルの使い方や使用するタイミングを理解しておくことが不可欠です。

非同期ページ

ASP.NET がサポートする 3 つの非同期プログラミング モデルのうち、一般に最も役に立つのが非同期ページです。非同期ページは、3 つのモデルの中で唯一 ASP.NET 2.0 固有のモデルです。他のモデルは、バージョン 1.0 以降のすべてのバージョンでサポートされます。

非同期ページについては 2005 年 10 月のコラム (msdn.microsoft.com/msdnmag/issues/05/10/WickedCode) (英語) で説明したので、ここでは詳しく説明しません。要点は、比較的長い入出力操作を実行するページは、非同期ページの候補になるということです。たとえば、ページからデータベースにクエリし、大量のデータを処理したり、負荷の高い接続経由のリモート データベースを対象にしているなどの理由でクエリの実行から戻るまでに 5 秒間を要するような場合、その 5 秒間、その要求に割り当てられたスレッドは他の要求に使用できなくなります。すべての要求がこのような動作を行うと、アプリケーションはすぐに行き詰まります。

図 1 は、非同期ページがこの問題をうまく解決するしくみを示しています。要求が到着すると、ASP.NET によってスレッドが割り当てられます。要求の処理がそのスレッドで開始されます。ただし、処理がデータベースにアクセスする時点までくると、要求は非同期 ADO.NET クエリを起動し、使用していたスレッドをスレッド プールに返します。クエリが完了すると、ADO.NET から ASP.NET にコール バックし、ASP.NET はスレッド プールから他のスレッドを取得して、要求の処理を再開します。

図 1 非同期ページのしくみ
図 1 非同期ページのしくみ(画像を縮小するには、ここをクリックします)
図 1 非同期ページのしくみ
図 1 非同期ページのしくみ (画像を拡大するには、ここをクリックします)

クエリが未解決の間は、スレッド プールのスレッドは使用されず、すべてのスレッドが着信する要求にサービスを提供できる空き状態のままになります。非同期に処理される要求は、あまり高速には実行されません。しかし、他の要求はスレッドが空き状態になるのを待機する必要がないので、高速に実行されます。要求がパイプラインに入り、遅延することがほとんどなくなるので、全体のスループットは向上します。

図 2 に、SQL Server™ データベースに対してデータ バインドを行う非同期ページの分離コード クラスを示します。Page_Load メソッドでは AddOnPreRenderCompleteAsync を呼び出して、開始ハンドラと終了ハンドラを登録します。要求の有効期間の後半で、ASP.NET は開始メソッドを呼び出します。開始メソッドは、非同期 ADO.NET クエリを起動し、すぐに戻ります。その直後、要求に割り当てられたスレッドをスレッド プールに戻します。ADO.NET からクエリが完了したことを示すシグナルが通知されると、ASP.NET はスレッド プールからスレッドを取得し (以前に使用していたスレッドと同じスレッドである必要はありません)、終了メソッドを呼び出します。終了メソッドはクエリの結果を受け取り、終了メソッドを実行するスレッド上で、通常どおり要求の残りを実行します。

Figure 2 非同期ページ (Close Figure 2)
using System;
using System.Data;
using System.Data.SqlClient;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.Configuration;

public partial class AsyncDataBind : System.Web.UI.Page
{
    private SqlConnection _connection;
    private SqlCommand _command;
    private SqlDataReader _reader;

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            AddOnPreRenderCompleteAsync(
                new BeginEventHandler(BeginAsyncOperation),
                new EndEventHandler(EndAsyncOperation)
            );
        }
    }

    IAsyncResult BeginAsyncOperation (object sender, EventArgs e,
        AsyncCallback cb, object state)
    {
        string connect = WebConfigurationManager.ConnectionStrings
            ["AsyncPubs"].ConnectionString;
        _connection = new SqlConnection(connect);
        _connection.Open();
        _command = new SqlCommand(
            "SELECT title_id, title, price FROM titles", _connection);
        return _command.BeginExecuteReader (cb, state);
    }

    void EndAsyncOperation(IAsyncResult ar)
    {
        _reader = _command.EndExecuteReader(ar);
    }

    protected void Page_PreRenderComplete(object sender, EventArgs e)
    {
        Output.DataSource = _reader;
        Output.DataBind();
    }

    public override void Dispose()
    {
        if (_connection != null) _connection.Close();
        base.Dispose();
    }
}

図 2 には、ASPX の Page ディレクティブの Async="true" 属性が示されていません。非同期ページにはこの属性が必要です。この属性はページに IHttpAsyncHandler インターフェイスを実装することを ASP.NET に指示します (瞬時にこれ以上のことが行われます)。また、図 2 には、データベース接続文字列も示されていません。接続文字列にも独自の Async="true" 属性を含め、非同期クエリを実行するように ADO.NET に通知します。

AddOnPreRenderCompleteAsync は、非同期ページを構造化する 1 つの方法です。他にも RegisterAsyncTask を呼び出す方法があります。これには AddOnPreRenderCompleteAsync よりも 2 ～ 3 のメリットがあります。最も重要なメリットは、1 回の要求で複数の非同期入出力操作を実行するタスクが簡素化されることです。この詳細については、2005 年 10 月の「Wicked Code コラム」を参照してください。

非同期 HTTP ハンドラ

ASP.NET がサポートする 2 つ目の非同期プログラミング モデルは、非同期 HTTP ハンドラです。HTTP ハンドラは、要求のエンドポイントとして機能するオブジェクトです。たとえば、ASPX ファイルに対する要求は、ASPX ファイルの HTTP ハンドラで処理されます。同様に、ASMX ファイルに対する要求は、ASMX サービスの処理方法を把握している HTTP ハンドラで処理されます。実際、ASP.NET にはさなざまな種類のファイルの HTTP ハンドラが含まれています。このようなファイルの種類とそれに対応する HTTP ハンドラは、マスタ web.config (ASP.NET 1.x では machine.config) ファイルの <httpHandlers> セクションで確認できます。

カスタム HTTP ハンドラを作成することで、他の種類のファイルをサポートするように ASP.NET を拡張できます。しかし、もっと興味深い点は、ASHX ファイルでカスタム HTTP ハンドラを展開し、HTTP 要求の対象としてそのハンドラを使用できることです。この方法は、実行中に画像を生成したり、データベースから画像を取得する Web エンドポイントを構築するのに適しています。ページに <img> タグ (または Image コントロール) を含め、そこで画像を作成または取得する ASHX を指すだけです。ASHX ファイルは処理時にかかるオーバーヘッドが大幅に少なくなるので、要求の対象に ASHX ファイルを指定する方が ASPX ファイルを対象に指定するよりも効率が高くなります。

HTTP ハンドラは、定義によって IHttpHandler インターフェイスを実装します。このインターフェイスを実装するハンドラは、その処理を同期を取って実行します。図 3 の ASHX ファイルにはこのような HTTP ハンドラが 1 つ含まれています。TerraServiceImageGrabber は、実行時に Microsoft® TerraServer Web サービスを複数回呼び出し、市と州を緯度と経度に変換して衛星画像 ("タイル") を取得し、指定した場所の合成画像を形成するために取得した画像を貼り合わせます。

Figure 3 同期 HTTP ハンドラ (Close Figure 3)
<%@ WebHandler Language="C#" Class="TerraServiceImageGrabber" %>

using System;
using System.Web;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;

public class TerraServiceImageGrabber : IHttpHandler
{
    public void ProcessRequest (HttpContext context)
    {
        // Extract user input from the query string
        string city = context.Request["City"];
        string state = context.Request["State"];
        string scale = context.Request["Scale"];

        // If city or state wasn't specified, throw an exception
        if (String.IsNullOrEmpty(city) || String.IsNullOrEmpty(state))
            throw new ArgumentException(
                "City and state must be specified via query string");

        // Determine the scale
        Scale res = Scale.Scale8m;
        if (!String.IsNullOrEmpty (scale))
        {
            switch (scale)
            {
                case "1":
                    res = Scale.Scale1m;
                    break;

                case "2":
                    res = Scale.Scale2m;
                    break;

                case "4":
                    res = Scale.Scale4m;
                    break;

                case "8":
                    res = Scale.Scale8m;
                    break;

                case "16":
                    res = Scale.Scale16m;
                    break;

                case "32":
                    res = Scale.Scale32m;
                    break;
            }
        }

        // Generate the requested image
        using(Bitmap bitmap = GetTiledImage(city, state, res, 900, 600))
        {
            // Set the response's content type
            context.Response.ContentType = "image/jpeg";

            // Write the image to the HTTP response
            bitmap.Save(context.Response.OutputStream, ImageFormat.Jpeg);
        }
    }

    public bool IsReusable { get { return true; } }

    private Bitmap GetTiledImage (string city, string state,
        Scale scale, int cx, int cy)
    {
        // Instantiate the TerraService proxy
        TerraService ts = new TerraService ();

        // Get the latitude and longitude of the requested city
        Place place = new Place ();
        place.City = city;
        place.State = state;
        place.Country = "USA";
        LonLatPt point = ts.ConvertPlaceToLonLatPt (place);

        // Compute the parameters for a bounding box
        AreaBoundingBox abb = ts.GetAreaFromPt (point, Theme.Photo,
            scale, cx, cy);

        // Create an image to fit the bounding box
        Bitmap bitmap = new Bitmap (cx, cy, PixelFormat.Format32bppRgb);
        using(Graphics g = Graphics.FromImage(bitmap))
        {
            int x1 = abb.NorthWest.TileMeta.Id.X;
            int y1 = abb.NorthWest.TileMeta.Id.Y;
            int x2 = abb.NorthEast.TileMeta.Id.X;
            int y2 = abb.SouthWest.TileMeta.Id.Y;

            for (int x=x1; x<=x2; x++) {
                for (int y=y1; y>=y2; y--) {
                    TileId tid = abb.NorthWest.TileMeta.Id;
                    tid.X = x;
                    tid.Y = y;
                    using(Image tile = Image.FromStream(
                        new MemoryStream(ts.GetTile(tid))))
                    {
                        g.DrawImage(tile,
                            (x - x1) * tile.Width  -
                                (int) abb.NorthWest.Offset.XOffset,
                            (y1 - y) * tile.Height -
                                (int) abb.NorthWest.Offset.YOffset,
                            tile.Width, tile.Height);
                    }
                }
            }
        }

        // Return the image
        return bitmap;
    }
}

結果は、印象深いものです。しかし、問題点があります。TerraServiceImageGrabber は、HTTP ハンドラの記述方法の完璧な例ではありません。少し考えてみましょう。TerraServiceImageGrabber では、Web サービスを呼び出し、結果を処理するのに (少なくとも) 数秒を要します。単に Web サービス呼び出しの完了を待機するだけで、大量の時間を費やします。ASHX ファイルに対して繰り返し要求を行うと、たちまち ASP.NET スレッド プールを使い果たし、アプリケーションの他のページにサービスを提供できなくなります (または、少なくともスレッドが使用できるようになるまで、要求がキューに強制的に登録されます)。この方法では、ハードウェアをスケール アウトしない限り、スケール変換可能なアプリケーションをビルドすることはできません。しかし、ソフトウェアを正しく記述すればその負荷を 1 台のサーバーで処理できるのに、Web ファームに数万ドルの投資を行う理由はあるでしょうか。

図 4 TerraServiceImageGrabber の機能
図 4 TerraServiceImageGrabber の機能(画像を縮小するには、ここをクリックします)
図 4 TerraServiceImageGrabber の機能
図 4 TerraServiceImageGrabber の機能 (画像を拡大するには、ここをクリックします)

HTTP ハンドラでは必ずしも同期処理を行う必要はありません。IHttpHandler から派生した IHttpAsyncHandler インターフェイスを実装することで、HTTP ハンドラを非同期にできます。非同期ハンドラを正しく使用すると、ASP.NET スレッドを効率的に使用できるようになります。これは、非同期ページと同じ方法で行います。実際、非同期ページでは、ASP.NET に非同期ページが導入される前から存在していた非同期ハンドラのサポートを活用しています。

図 5 には、図 3 で示したハンドラの非同期バージョンを含んでいます。非同期 TerraServiceImageGrabber はやや複雑ですが、はるかにスケーラビリティが高くなります。

Figure 5 非同期 HTTP ハンドラ (Close Figure 5)

<%@ WebHandler Language="C#" Class="AsyncTerraServiceImageGrabber" %>

using System;
using System.Web;
using System.Drawing;
using System.Drawing.Imaging;
using System.Threading;
using System.IO;

public class AsyncTerraServiceImageGrabber : IHttpAsyncHandler
{
    private TerraService _ts;
    private TerraServiceAsyncResult _ar;
    private Scale _scale = Scale.Scale8m;
    private AreaBoundingBox _abb;
    private Bitmap _bitmap;
    private int _count = 0;
    private int _max;
    private HttpContext _context;
    private Exception _ex;
    private int _cx = 900, _cy = 600; // Width and height of bitmap

    public void ProcessRequest (HttpContext context)
    {
        // Never called
    }

    public bool IsReusable { get { return false; } }

    public IAsyncResult BeginProcessRequest(HttpContext context,
        AsyncCallback cb, object state)
    {
        _context = context;

        // Extract user input from the query string
        string city = context.Request["City"];
        string region = context.Request["State"];
        string scale = context.Request["Scale"];

        // If city or state wasn’t specified, throw an exception
        if (String.IsNullOrEmpty(city) || String.IsNullOrEmpty(region))
            throw new ArgumentException(
                "City and state must be specified via query string");

        // Determine the scale
        if (!String.IsNullOrEmpty (scale))
        {
            switch (scale)
            {
                case "1":
                    _scale = Scale.Scale1m;
                    break;

                case "2":
                    _scale = Scale.Scale2m;
                    break;

                case "4":
                    _scale = Scale.Scale4m;
                    break;

                case "8":
                    _scale = Scale.Scale8m;
                    break;

                case "16":
                    _scale = Scale.Scale16m;
                    break;

                case "32":
                    _scale = Scale.Scale32m;
                    break;
            }
        }

        // Instantiate a TerraServiceAsyncResult
        _ar = new TerraServiceAsyncResult(cb, state);

        // Instantiate the TerraService proxy
        _ts = new TerraService();

        // Make an async call to get the latitude and longitude
        // of the requested city
        Place place = new Place();
        place.City = city;
        place.State = region;
        place.Country = "USA";

        _ts.BeginConvertPlaceToLonLatPt(place,
            new AsyncCallback(ConvertPlaceToLonLatCompleted), null);

        // Return an IAsyncResult that delays EndProcessRequest until
        // the final asynchronous Web service call has completed
        return _ar;
    }

    private void ConvertPlaceToLonLatCompleted(IAsyncResult ar)
    {
        try
        {
            // Complete the async call
            LonLatPt point = _ts.EndConvertPlaceToLonLatPt(ar);

            // Make an async call to compute the parameters
            // for a bounding box
            _ts.BeginGetAreaFromPt(point, Theme.Photo, _scale, _cx,
              _cy, new AsyncCallback(GetAreaFromPointCompleted), null);
        }
        catch (Exception ex)
        {
            _ex = ex;
            _ar.CompleteCall();
        }
    }

    private void GetAreaFromPointCompleted(IAsyncResult ar)
    {
        try
        {
            // Complete the async call
            _abb = _ts.EndGetAreaFromPt(ar);

            // Create an image to fit the bounding box
            _bitmap = new Bitmap(_cx, _cy, PixelFormat.Format32bppRgb);

            int x1 = _abb.NorthWest.TileMeta.Id.X;
            int y1 = _abb.NorthWest.TileMeta.Id.Y;
            int x2 = _abb.NorthEast.TileMeta.Id.X;
            int y2 = _abb.SouthWest.TileMeta.Id.Y;

            _max = (x2 - x1 + 1) * (y1 - y2 + 1);

            // Place concurrent async calls to TerraService to
            // fetch image tiles
            for (int x = x1; x <= x2; x++)
            {
                for (int y = y1; y >= y2; y--)
                {
                    TileId tid = new TileId();
                    tid.Theme = _abb.NorthWest.TileMeta.Id.Theme;
                    tid.Scale = _abb.NorthWest.TileMeta.Id.Scale;
                    tid.Scene = _abb.NorthWest.TileMeta.Id.Scene;
                    tid.X = x;
                    tid.Y = y;
                    _ts.BeginGetTile(tid,
                        new AsyncCallback(GetTileCompleted),
                        new Point(x - x1, y1 - y));
                }
            }
        }
        catch (Exception ex)
        {
            _ex = ex;
            _ar.CompleteCall();
        }
    }

    private void GetTileCompleted(IAsyncResult ar)
    {
        try
        {
            // Complete the async call
            using(Image tile = Image.FromStream(
                new MemoryStream(_ts.EndGetTile(ar))))
            {
                // Draw the tile onto the bitmap
                Point point = (Point)ar.AsyncState;
                int dx = point.X;
                int dy = point.Y;

                lock (_bitmap)
                {
                    using(Graphics g = Graphics.FromImage(_bitmap))
                    {
                        g.DrawImage(tile,
                            dx * tile.Width –
                                (int)_abb.NorthWest.Offset.XOffset,
                            dy * tile.Height –
                                (int)_abb.NorthWest.Offset.YOffset,
                            tile.Width, tile.Height);
                }
            }

            // Increment the tile count and complete the request if all
            // tiles have been fetched
            int count = Interlocked.Increment(ref _count);
            if (count == _max) _ar.CompleteCall();
        }
        catch (Exception ex)
        {
            _ex = ex;
            _ar.CompleteCall();
        }
    }

    public void EndProcessRequest(IAsyncResult ar)
    {
        if (_ex != null)
        {
            // If an exception was thrown, rethrow it
            throw _ex;
        }
        else
        {
            // Otherwise return the generated image
            _context.Response.ContentType = "image/jpeg";
            _bitmap.Save(_context.Response.OutputStream,
                ImageFormat.Jpeg);
            _bitmap.Dispose();
        }
    }
}

class TerraServiceAsyncResult : IAsyncResult
{
    private AsyncCallback _cb;
    private object _state;
    private ManualResetEvent _event;
    private bool _completed = false;
    private object _lock = new object();

    public TerraServiceAsyncResult(AsyncCallback cb, object state)
    {
        _cb = cb;
        _state = state;
    }

    public Object AsyncState { get { return _state; } }

    public bool CompletedSynchronously { get { return false; } }

    public bool IsCompleted { get { return _completed; } }

    public WaitHandle AsyncWaitHandle
    {
        get
        {
            lock (_lock)
            {
                if (_event == null)
                    _event = new ManualResetEvent(IsCompleted);
                return _event;
            }
        }
    }

    public void CompleteCall()
    {
        lock (_lock)
        {
            _completed = true;
            if (_event != null) _event.Set();
        }

        if (_cb != null) _cb(this);
    }
}

ASP.NET がハンドラの BeginProcessRequest メソッドを呼び出すと、非同期処理が始まります。BeginProcessRequest は、TerraService プロキシの BeginConvertPlaceToLonLatPt メソッド経由で TerraService への非同期呼び出しを行います。呼出し後、要求に割り当てられたスレッドをスレッド プールに戻します。非同期呼び出しが完了したら、ConvertPlaceToLonLatCompleted メソッドを実行するために、スレッド プールから別のスレッドを取得します。そのスレッドは最後の呼び出しの結果を取得し、スレッド独自の非同期呼び出しを行ってから、そのスレッドをスレッド プールに戻します。すべての非同期呼び出しが完了するまでこのパターンを繰り返し、ハンドラの EndProcessRequest メソッドが呼び出された時点で、結果のビットマップを要求側に返します。

最後の Web サービス呼び出しが完了するまで EndProcessRequest が呼び出されないように、AsyncTerraServiceImageGrabber では BeginProcessRequest から IAsyncResult の独自の実装を返します。代わりに、BeginConvertPlaceToLonLatPt から返された IAsyncResult が返されると、最初の Web サービス呼び出しの完了直後に EndProcessRequest が呼び出されることになります (その結果、要求が完了します)。

IAsyncResult を実装する TerraServiceAsyncResult クラスは、要求を終了するためにいつでも呼び出せるパブリック メソッド CompleteCall を備えています。通常、AsyncTerraServiceImageGrabber は、最後の Web サービス呼び出しが完了した後にのみ CompleteCall を呼び出します。ただし、BeginProcessRequest から EndProcessRequest までの間に実行されるいずれかのメソッドで例外がスローされると、ハンドラはプライベート フィールド (_ex) に例外をキャッシュし、その要求を終了するために CompleteCall を呼び出し後、EndProcessRequest から例外を再度スローします。そうしないと、例外が失われ、要求が完了しなくなります。

AsyncTerraServiceImageGrabber は、要求の処理に必要な総時間のうち、ほんのわずかな時間しか ASP.NET スレッドを使用しないので、同期処理を行う場合よりもスケーラビリティが高くなります。その時間の大部分は、単に非同期 Web サービス呼び出しの完了を待機しています。

AsyncTerraServiceImageGrabber は、理論上、TerraService の GetTile メソッドを順次繰り返して呼び出しているのではなく、並列に呼び出しを行っているので、TerraServiceImageGrabber よりもパフォーマンスが高くなります。しかし、実際には、特定の IP アドレスを対象とした発信呼び出しは、次のようにランライムの最大接続数の既定の設定を増加しない限り、一度に 2 つしか保留状態にできません。

<system.net>
  <connectionManagement>
    <add address="*" maxconnection="20" />
  </connectionManagement>
</system.net>

他の構成設定も、同時実行に影響する可能性があります。詳細については、サポート技術情報の記事「ASP.NET アプリケーションから Web サービス要求を行うと、競合、パフォーマンスの低下、およびデッドロックが発生する」 (support.microsoft.com/kb/821268) を参照してください。

一度に 1 つしか呼び出しが実行されない場合でも、AsyncTerraServiceImageGrabber のパフォーマンスが TerraServiceImageGrabber よりも低くなることはありません。また、できる限り効率よく ASP.NET スレッドを使用するので、AsyncTerraServiceImageGrabber のデザインの方がはるかに優れています。

非同期 HTTP モジュール

ASP.NET がサポートする 3 つ目の非同期プログラミング モデルは、非同期 HTTP モジュールです。HTTP モジュールは ASP.NET パイプライン内に位置するオブジェクトで、着信する要求や発信される応答を監視でき、変更することさえできます。認証、承認、出力キャッシュなど、ASP.NET 内の多くの重要なサービスは、HTTP モジュールの形式で実装されます。カスタム HTTP モジュールを作成し、それをパイプラインにプラグインすることで、ASP.NET を拡張できます。これを行う場合は、HTTP モジュールを非同期にするかどうかを注意深く検討してください。

図 6 に、RequestLogModule という簡単な同期 HTTP モジュールのソース コードを示します。このモジュールでは、着信する要求を RequestLog.txt というテキスト ファイルに記録します。このファイルはサイトの App_Data ディレクトリに作成され、ユーザーが参照することはできません (たとえば、ASPNET や Network Service で ASP.NET を実行するセキュリティ プリンシパルでは、これが機能するために App_Data に書き込みアクセス許可が必要になることに注意してください)。このモジュールは IHttpModule インターフェイスを実装します。このインターフェイスを実装することは、HTTP モジュールの唯一の要件です。モジュールが読み込まれるときに、Init メソッドによって HttpApplication.PreRequestHandlerExecute イベントのハンドラが登録され、すべての要求に対してこのイベントがパイプラインから毎回発生します。イベント ハンドラでは RequestLog.txt を開き (存在しない場合は作成し)、現在の要求に関する適切な情報を含むテキストを 1 行書き込みます。書き込む情報には、要求が到着した日時、要求が認証される場合は要求者のユーザー名 (認証がオフの場合は要求者の IP アドレス)、要求された URL などがあります。モジュールは、web.config の <httpModules> セクションに登録され、アプリケーションが起動されるたびにモジュールを読み込むことを ASP.NET に指示します。

Figure 6 同期 HTTP モジュール (Close Figure 6)
using System;
using System.Web;
using System.IO;

public class RequestLogModule : IHttpModule
{
    public void Init (HttpApplication application)
    {
        application.PreRequestHandlerExecute +=
            new EventHandler(OnPreRequestHandlerExecute);
    }

    public void Dispose () {}

    void OnPreRequestHandlerExecute(Object sender, EventArgs e)
    {
        HttpApplication app = (HttpApplication)sender;
        DateTime time = DateTime.Now;

        using(StreamWriter writer = new StreamWriter(
            app.Server.MapPath("~/App_Data/RequestLog.txt"), true))
        {
            string line = String.Format(
                "{0,10:d}    {1,11:T}    {2, 32}   {3}",
                time, time,
                app.User.Identity.IsAuthenticated ?
                app.User.Identity.Name :
                app.Request.UserHostAddress,
                app.Request.Url);
            writer.WriteLine (line);
        }
    }
}

RequestLogModule には 2 つの問題があります。まず、すべての要求に対して毎回ファイル入出力が行われます。次に、モジュールでは入出力の実行に 1 つの要求処理スレッドを使用し、それ以外に、新たに着信する要求にサービスを提供するために 1 つスレッドを使用します。このモジュールは非常に単純ですが、スループットに悪影響があります。ファイル入出力操作をバッチ処理することで遅延を緩和できますが、モジュールを非同期にする方が優れています (ファイル入出力をバッチ処理にし、かつモジュールを非同期にする方がさらに優れています)。

図 7 に、RequestLogModule の非同期バージョンを示します。このモジュールは AsyncRequestLogModule と言い、まったく同じ作業を実行しますが、ファイルへの書き込みを行う前に、要求に割り当てられたスレッドをスレッド プールに戻します。書き込みが完了すると、スレッド プールから新たなスレッドを取得し、要求の完了に使用します。

Figure 7 非同期 HTTP モジュール (Close Figure 7)
using System;
using System.Web;
using System.IO;
using System.Threading;
using System.Text;

public class AsyncRequestLogModule : IHttpModule
{
    private FileStream _file;
    private static long _position = 0;
    private static object _lock = new object();

    public void Init (HttpApplication application)
    {
        application.AddOnPreRequestHandlerExecuteAsync (
            new BeginEventHandler (BeginPreRequestHandlerExecute),
            new EndEventHandler (EndPreRequestHandlerExecute)
        );
    }

    IAsyncResult BeginPreRequestHandlerExecute (Object source,
        EventArgs e, AsyncCallback cb, Object state)
    {
        HttpApplication app = (HttpApplication)source;
        DateTime time = DateTime.Now;

        string line = String.Format(
            "{0,10:d}    {1,11:T}    {2, 32}   {3}\r\n",
            time, time,
            app.User.Identity.IsAuthenticated ?
            app.User.Identity.Name :
            app.Request.UserHostAddress,
            app.Request.Url);

        byte[] output = Encoding.ASCII.GetBytes(line);

        lock (_lock)
        {
            _file = new FileStream(
                HttpContext.Current.Server.MapPath(
                    "~/App_Data/RequestLog.txt"),
                FileMode.OpenOrCreate, FileAccess.Write,
                FileShare.Write, 1024, true);

            _file.Seek(_position, SeekOrigin.Begin);
            _position += output.Length;
            return _file.BeginWrite(output, 0, output.Length, cb, state);
        }
    }

    void EndPreRequestHandlerExecute (IAsyncResult ar)
    {
        _file.EndWrite(ar);
        _file.Close();
    }

    public void Dispose () {}
}

何が AsyncRequestLogModule を非同期にしているのでしょう。モジュールの Init メソッドでは、PreRequestHandlerExecute イベントの開始メソッドと終了メソッドを登録するために、HttpApplication.AddOnPreRequestHandlerExecuteAsync を呼び出します。HttpApplication クラスには、他にも要求イベントごとに AddOn メソッドを備えています。たとえば、HTTP モジュールでは BeginRequest イベントの非同期ハンドラを登録するために、AddOnBeginRequestAsync を呼び出すことができます。AsyncRequestLogModule の BeginPreRequestHandlerExecute メソッドでは、フレームワークの FileStream.BeginWrite メソッドを使用して、非同期書き込みを開始します。BeginPreRequestHandlerExecute から戻るとすぐに、スレッドがスレッド プールに戻されます。

AsyncRequestLogModule には、特殊な目的にメリットがあるスレッド同期ロジックがいくつか含まれています。複数のスレッド上で複数の要求が実行されると、ログ ファイルに同時書き込みが行われる可能性があります。同時書き込みによって相互に上書きしないようにするために、AsyncRequestLogModule ではすべてのモジュール インスタンスで共有されるプライベート フィールド (_position) にファイル内の次に書き込む位置を保存します。各モジュールでは BeginWrite を呼び出す前に、このフィールドから書き込み位置を読み取り、次にファイルに書き込む先頭バイトの位置にこのフィールドを更新します。_position を読み取って更新するロジックは、lock ステートメントでラップされているため、複数のスレッドで同時に実行されることはありません。これにより、あるスレッドが位置を更新する前に別のスレッドがその位置を読み取ることを防ぎます。

ここで悪い知らせがあります。BeginWrite はプールから別のスレッドを取得して使用しないため、この例で行っているように、FileStream のコンストラクタへの isAsync パラメータを true に設定する必要があります。ただし、あまり知られてはいませんが、たとえ非同期操作を要求した場合でも、非同期書き込みを開始するために FileStream.BeginWrite を使用した結果、実際に非同期に書き込みが行われるとは限りません。Windows

まとめ

非同期プログラミングは、ASP.NET スレッド プールをできる限り効率的に使用することで、スケーラビリティの高いアプリケーションをビルドする優れた方法です。これまでお会いした ASP.NET 開発者は、非同期プログラミング モデルをめったに使用していませんでした。中には、こうしたモデルの存在を知らない開発者もいました。これまでドキュメントが不足していたことを言い訳にしないでください。今日、非同期プログラミングについて考え始めてください。そうすれば、明日にはアプリケーションをビルドしているでしょう。

このコラムに付属するダウンロード可能なサンプル コードには、C# 版と Visual Basic® 版の両方が含まれています。これまで、サンプルの Visual Basic 版を求める電子メールをたくさん受け取りました。今回は、VB 版を含めているので、そのような電子メールは不要です。

ご意見やご質問は、

Jeff wicked@microsoft.com.

まで英語でお送りください。
Jeff Prosise は、MSDN Magazine に寄稿している編集者で、『プログラミング Microsoft .NET』(2002 年、日経 BP ソフトプレス) など数冊の著書があります。また、ソフトウェアのコンサルティングおよび .NET 専門の教育機関である Wintellect の共同創立者でもあります。


サービス ステーション: BizTalk Server 2006 の Web サービス

サービス ステーション
BizTalk Server 2006 の Web サービス
Aaron Skonnard

コンテンツ
Web サービス アダプタ
SOAP アダプタの使用
SOAP トランスポートのプロパティ
オーケストレーションでの SOAP アダプタの使用
WSE 2.0 アダプタ
WCF アダプタ
まとめ

今月は、BizTalk Server 2006 の胸躍る世界と、同製品の現在の Web サービス テクノロジに対するサポートについて紹介します。現在公開されている SOAP アダプタと Web Services Enhancements (WSE) アダプタの使用法、および BizTalk® Server 2006 R2 リリースに付属して間もなく登場する予定の Windows® Communication Foundation (WCF) アダプタについて説明します (この記事には英語のページへのリンクも含まれています)。

BizTalk Server 2006 は、異種のアプリケーション相互の疎結合メッセージングを促す、柔軟なメッセージング サブシステムを基盤にしています。メッセージング層で、メッセージ ルーティング、スキーマ変換、形式変換などが行われるのがアプリケーション統合の利点です。メッセージング層の中心は、メッセージ ボックスと呼ばれる SQL Server™ データベースです。メッセージング層で処理されるすべてのメッセージは、ルーティング、追跡、およびエラー処理のため、メッセージ ボックスを通過します。メッセージ ボックスは "メッセージを残さない" という理念で動作しています。この理念は信頼性が欠かせない場合に重要です。ただし、パフォーマンスを重視する場合は、この機能がむしろ足かせになる場合があります。

アプリケーション間でメッセージをどのようにルーティングするかは、メッセージ ボックスでメッセージ サブスクリプション ("フィルタ" ともいいます) を定義して制御します。フィルタは、(送信ポートの) 管理コンソールで定義する方法と、論理ポート定義でオーケストレーション内から間接的に定義する方法があります。図 1 に示したとおり、メッセージを公開すると、そのメッセージはメッセージ ボックスでサブスクリプションと照合され、対応するすべてのサブスクライバ (送信ポートまたはオーケストレーション) にルーティングされます。この公開/サブスクライブ アーキテクチャによって、送信者と受信者が完全に分離されます。

図 1 BizTalk メッセージング アーキテクチャ
図 1 BizTalk メッセージング アーキテクチャ(画像を縮小するには、ここをクリックします)
図 1 BizTalk メッセージング アーキテクチャ
図 1 BizTalk メッセージング アーキテクチャ (画像を拡大するには、ここをクリックします)

BizTalk はアダプタを経由して外部と通信します。アダプタは、特定のトランスポート経由でバイト列を受信して新しい BizTalk メッセージを作成し、そのメッセージを、受信パイプラインとマップ (XSLT 変換) が受信メッセージを処理できるようになった後でメッセージ ボックスに公開します (図 2 参照)。送信メッセージを送信するときは、逆のことが言えます。マップと送信パイプラインが送信メッセージを処理できるようになった後、バイト列を生成してトランスポートに変換します。詳細な構成は、BizTalk の送受信ポート経由で行います。

図 2 BizTalk ポートの内部
図 2 BizTalk ポートの内部(画像を縮小するには、ここをクリックします)
図 2 BizTalk ポートの内部
図 2 BizTalk ポートの内部 (画像を拡大するには、ここをクリックします)

BizTalk は一方向および双方向のポートをサポートします。受信側で定義できるのは、メッセージの受信のみ行い、何も返さない一方向の受信ポートか、呼び出し元に応答メッセージを返す要求 - 応答の受信ポートです。送信側でも同じく、一方向のポートか送信請求 - 応答ポートを定義できます。

BizTalk Server 2006 には、さまざまなトランスポートやプロトコルをサポートするために、多様なアダプタが用意されていて、その中には、SOAP および WS-* 用に設計されたアダプタがあります。SOAP および WS-* の他にも、BizTalk は多数の通信メカニズムをサポートしています。このことは、BizTalk の注目すべき特徴の 1 つです。従来のアプリケーションと新しいサービス指向の資産をどちらも含むシステムで、BizTalk をハブとして利用できます。

Web サービス アダプタ

BizTalk Server 2006 と SOAP および WS-* は、図 3 に示した各種 Web サービス アダプタにより統合されます。BizTalk Server 2004 以降のバージョンに付属している SOAP アダプタは、WS-I Basic Profile 1.1 (BP 1.1) メッセージをサポートします。WS-* プロトコルのサポートが必要な場合、現在公開されている WSE アダプタに変更するか、BizTalk Server 2006 R2 リリースに付属する Windows Communication Foundation アダプタの公開を待つ必要があります。

Figure 3 マイクロソフトが提供する BizTalk Web サービス アダプタ (Close Figure 3)

アダプタ | BizTalk Server 2004 | BizTalk Server 2006 | BizTalk Server 2006 R2
SOAP | 付属 | 付属 | 付属
WSE 2.0 | ダウンロード可 | ダウンロード可 (要 SP1) | ダウンロード可 (要 SP1)
WCF | 付属せず | 付属せず | 付属

マイクロソフトは、WSE 2.0 アダプタを microsoft.com/biztalk/evaluation/adapter/adapters/wse でダウンロード提供しています。WSE 2.0 アダプタは BizTalk Server 2004 および BizTalk Server 2006 (BizTalk Server 2006 は SP1 リリースが必要) で動作します。近日公開される Windows Communication Foundation アダプタで同等の機能が提供されることから、マイクロソフトは WSE 3.0 アダプタを実装しない方針に決定しました。ただし、サードパーティ製の WSE 3.0 アダプタが公開されています。

図 4 Web サービス アダプタの使用
図 4 Web サービス アダプタの使用(画像を縮小するには、ここをクリックします)
図 4 Web サービス アダプタの使用
図 4 Web サービス アダプタの使用 (画像を拡大するには、ここをクリックします)

上記のアダプタは、いずれも BizTalk での SOAP メッセージの送受信に使用できます。使用の際は、一方向または双方向のポートと接続して使用します。たとえば、BP 1.1 に準拠した単純な SOAP メッセージの送受信には SOAP アダプタを、WS-Security、WS-Trust、および WS-SecureConversation に従ってセキュリティで保護された SOAP メッセージの送受信には WSE 2.0 アダプタを使用できます。

受信側アダプタの役目は、SOAP および WS-* を処理し、メッセージの本文をメッセージ ボックスに公開することです。図 4 では、ASMX エンドポイントが実質的なアダプタです。実行時、公開のとき設定された構成済み受信パイプラインとマップを使用するため、ASMX コードが BizTalk と通信し、構成データベースから受信場所の詳細を取得します。受信 SOAP メッセージを従来の BizTalk のパイプラインとマップであらかじめ処理できることで、統合の際の選択肢が広がります。

BizTalk では、HTTP ベースのすべての受信場所を、BizTalk が管理するプロセスではなく、IIS などの分離ホスト プロセスで実行する必要があります。どういうことかと言うと、BizTalk は SOAP/HTTP 受信場所の構成を完全には制御しないで、分離ホストで SOAP/HTTP のさまざまな処理の詳細を制御することを前提にしています。そこで、分離ホスト アプリケーション コードを記述 (できれば自動生成が望ましい) する必要があります。

分離ホストで実行するアダプタ コードは、ウィザードで生成するのが一般的です。しかし、ウィザードを使用しなくても、任意の SOAP メッセージを受信して BizTalk に公開できる汎用 SOAP エンドポイントを記述することが可能です。

図 3 に示した Web サービス アダプタは、いずれも BizTalk メッセージをメッセージ ボックスから外部の Web サービスに送信するときに使用できます。いずれかのアダプタを使用する送信ポートを定義して、SOAP メッセージを送信できるようにポートの設定を構成します。

以上のすべての作業は、オーケストレーションを使用せず、BizTalk のメッセージング層で直接行うことができます。もちろん、オーケストレーション内で Web サービスを使用することもできます。後者の方法だと、オーケストレーション デザイナを使用でき、実行時間の長いプロセス、関連付け、エラー処理が大幅にサポートされると共に、開発者の負担が軽減されるので魅力的です。また、オーケストレーションを Web サービスとして公開することもできます。その際、パブリック受信ポートが自動的に公開されます。

では、ここまで説明した作業の一部を、現在公開されている SOAP アダプタと WSE アダプタで行っていきます。

SOAP アダプタの使用

注文を処理する BizTalk ソリューションが既に適切に展開されているとします。既に、MSMQ、FTP などのトランスポート経由で注文を受信できるようにいくつかの受信場所が構成されています。また、公開した注文メッセージを処理するため、サブスクリプションが配置されています。今からそれらに加え、SOAP 経由で注文を受信するサポートを追加します。注文メッセージ スキーマは既に用意されているので、後は ASMX サービスを生成するだけです。

BizTalk Web サービス公開ウィザードを実行すると、既存のスキーマの種類から ASMX サービスを生成できます。このウィザードでは、スキーマを公開するかオーケストレーションを公開するかを選択できます。この例ではスキーマを公開します。

次に、公開する Web サービス コントラクトを記述します。スキーマを公開しているだけなので、Web サービスの名前と、作成する WebMethod 各操作の名前を手動で定義する必要があります。続けて、各 WebMethod について、要求/応答メッセージを BizTalk アセンブリにある特定のスキーマの種類にマップできます (図 5 参照)。その他、生成される ASMX コードについて、ターゲット名前空間、ASMX パラメータの書式、BP 1.1 の適合性表示を行うかどうかなどの詳細を入力します。また、他の不明な SOAP ヘッダーを、後で処理できるようにするためサポートするかどうかも指定します。

図 5 スキーマの公開
図 5 スキーマの公開(画像を縮小するには、ここをクリックします)
図 5 スキーマの公開
図 5 スキーマの公開 (画像を拡大するには、ここをクリックします)

最後に、生成する ASMX コードを展開する仮想ディレクトリを指定します。後で、既存の BizTalk アプリケーションに、対応する受信場所が作成されます。ウィザードを完了すると、ASMX コードが生成され、指定したディレクトリに公開されると共に、そのサービス用に構成された BizTalk 受信場所が作成されます。

しかし、生成された ASMX エンドポイントを参照するには、BizTalk データベースにアクセスする許可を持ったアプリケーション プール内で仮想ディレクトリが実行されることを確認する必要があります (アプリケーション プールの ID は IIS_WPG および BizTalk Isolated Hosts Users のメンバである必要があります)。このとき、IIS および Microsoft® .NET Framework を従来どおりに構成して、HTTP および ASP.NET 処理の他の部分も構成できます。

残りの構成は、BizTalk 受信場所で行います。BizTalk 管理コンソールを開くと、ウィザードで作成した SOAP 受信場所が表示されます。受信場所のプロパティは、図 6 のようなものです。この受信場所は、分離ホスト インスタンスで実行されることがわかります。また、SOAP トランスポートを構成するため [構成] をクリックすると、サービスのアドレス (これをキーにして、受信パイプライン、マップなど、他の BizTalk 構成が実行時に検索されます) しか含まれていないことがわかります。

図 6 SOAP 受信場所の構成
図 6 SOAP 受信場所の構成(画像を縮小するには、ここをクリックします)
図 6 SOAP 受信場所の構成
図 6 SOAP 受信場所の構成 (画像を拡大するには、ここをクリックします)

仮想ディレクトリを適切に構成すると、生成された ASMX エンドポイントを参照できるようになり、従来の ASMX ドキュメント ページが表示されます。操作の詳細を調べると、各 SOAP メッセージの本文が、ウィザードで指定したスキーマの種類になっていることがわかります。これでクライアントは、ASMX エンドポイントにアクセスし、クライアント コードを生成するサービスの Web Services Description Language (WSDL) 定義を取得できます。WSDL コードを生成することで、SOAP を認識するクライアントを簡単に BizTalk ソリューションに統合できます。

ところで、請求書発行サービスが外部にあり、受信した注文メッセージの応答メッセージをそのサービスに送信する必要があるとします。そして、その Web サービスと通信するための Web サービス プロキシ クラスを開発済みだとします (このクラスは、wsdl.exe または [Web 参照の追加] で作成したものとします)。

送信ポートを作成し、それにより外部の請求書発行サービスにメッセージを送信できます。SOAP アダプタを選択し、外部の Web サービスのアドレスを指定します (図 7 参照)。次に、[Web サービス] タブで、使用する Web サービス プロキシのクラスとメソッドを選択します (図 8 参照)。このプロキシ クラスはメッセージを送信するときに使用されます。アセンブリは、実行時にグローバル アセンブリ キャッシュ (GAC) に格納されている必要があります。

図 7 SOAP 送信ポートの構成
図 7 SOAP 送信ポートの構成(画像を縮小するには、ここをクリックします)
図 7 SOAP 送信ポートの構成
図 7 SOAP 送信ポートの構成 (画像を拡大するには、ここをクリックします)
図 8 プロキシ クラスの指定
図 8 プロキシ クラスの指定(画像を縮小するには、ここをクリックします)
図 8 プロキシ クラスの指定
図 8 プロキシ クラスの指定 (画像を拡大するには、ここをクリックします)

実行時に SOAP アダプタに正しいメッセージを渡すのが最大の難関です。指定したプロキシ クラスのメソッドが複数のパラメータを取る場合、各パートが署名のパラメータに対応するマルチパート メッセージが渡されると想定されます。メッセージの各パートのシリアル化を解除し、対応する .NET Framework 型にしてから Web サービス プロキシ メソッドを呼び出します。すべてのデータは後でこのメソッドによって SOAP メッセージにシリアル化されます。

既存の WSDL 定義を実装する必要がある場合は、BizTalk Business Process Execution Language (BPEL) インポート ウィザードで WSDL からオーケストレーションの骨組みを生成できます。多少の手動の操作を伴いますが、従来の wsdl.exe /server を使用して適切なアダプタ コードを生成することもできます。既存のコントラクトを使用する技法の詳細については、msdn.microsoft.com/library/en-us/bts_2004wp/html/7f82d724-4f6f-4440-9b01-921d0cb51fc0.asp を参照してください。

SOAP トランスポートのプロパティ

SOAP アダプタを使用するとき、SOAP 固有のメッセージ コンテキスト プロパティを多数使用できます。それらのプロパティは、すべて SOAP 名前空間にあり、パイプライン コンポーネントおよびオーケストレーション図形からアクセスできます。

たとえば、SOAP.AssemblyName および SOAP.MethodName は、SOAP 送信ポート経由でメッセージを送信するときに使用する Web サービス プロキシのクラスとメソッドを識別します。SOAP.Username および SOAP.Password を使用すると、クライアントの資格情報を動的に構成できます。応答のタイムアウト値を増やすには、SOAP.ClientConnectionTimeout を設定します。これらのプロパティを使用して、送信ポートのさまざまな動的プロパティを実行時に構成できます。SOAP アダプタのプロパティとその動作の詳細については、SOAP アダプタのドキュメントを参照してください。

受信側では、受信した SOAP メッセージの各 SOAP ヘッダーを処理する必要があります。処理を実行する方法は 2 つあります。まず、公開ウィザードを使用するときに定義した既知の SOAP ヘッダーは、SOAPHeader という特別な名前空間のカスタム プロパティとして使用できます。それ以外の不明な SOAP ヘッダーは、すべて SOAP.UnknownHeaders プロパティに保存されます。また、送信する SOAP メッセージに SOAP ヘッダーを追加する必要がある場合、メッセージの送信前に SOAP ヘッダー プロパティを設定できます。

次の話題に移る前に、失敗したメッセージをルーティングする BizTalk Server 2006 の新機能について触れておきます。この機能を送信/受信ポートで有効にすると、失敗したメッセージが、メッセージ コンテキストに (ErrorReport 名前空間の) エラー レポート プロパティが付加されてメッセージ ボックスに再公開されます。この機能を使用し、さまざまな ErrorReport プロパティをサブスクライブして、SOAP 送信/受信ポートでエラーが発生したときに通知を行うことができます。

オーケストレーションでの SOAP アダプタの使用

ここまで、BizTalk オーケストレーションを使用せず、メッセージング層で SOAP アダプタを使用するための基礎を説明してきました。もちろん、SOAP アダプタとオーケストレーションを併用するという、簡単な方法もあります。

BizTalk Web サービス公開ウィザードを実行して、オーケストレーションを Web サービスとして公開することもできますが、今回はオーケストレーションを公開します。ウィザードで、オーケストレーションと、公開するパブリック ポートを選択します。選択したポートからサービス コントラクトが派生するので、以前のようにコントラクトを手動で定義する必要がありません。ウィザードの残りの手順は、以前と同じく IIS および ASP.NET の各種設定と、ASMX サービスの生成です。最も変化した点は、オーケストレーションからコントラクトを自動的に派生できるようになったことです。

オーケストレーション内で Web サービスを使用することもできます。始めに、Visual Studio® の [Web 参照の追加] を使用します。このダイアログ ボックスで、オーケストレーションから外部サービスを呼び出すために必要な、スキーマとポートの種類を追加します。次に、新しいオーケストレーション ポートを定義し、[Web 参照の追加] で追加した Web ポートの種類を選択します。生成された Web ポートの種類を使用することで、Web サービスとの間で送受信される XML メッセージの正しさが保証されます。最後に、Web サービスの呼び出しをモデル化するため、Web ポートに送信図形と受信図形を接続して終了です。

Web サービスは、オーケストレーションの式図形から呼び出すこともできます。その場合、メッセージは BizTalk のメッセージング層を通過しないので、従来の Web サービス呼び出しのように遅延が少なくなります。ただし、この技術を多用すると、展開後のオーケストレーションの管理が難しくなる場合があります。

スコープ図形と例外処理ブロックを使用すると、サービスが返した SOAP エラーをオーケストレーション内から処理できます。SOAP エラーの詳細を処理する場合、System.Web.Services.Protocols.SoapException 型の例外をキャッチできるように例外処理ブロックを構成してください。同様に、サービスとして公開したオーケストレーションは、呼び出し元に SOAP エラーを返す場合があります。

WSE 2.0 アダプタ

WSE 2.0 アダプタを使用するプロセスは、前のセクションで説明した SOAP アダプタを使用するプロセスと似ています。では、両者の違いに目を向けてみましょう。

まず、WSE 2.0 アダプタに独自の公開ウィザードがあることに気付きます。このウィザードを実行すると、最初は SOAP アダプタの公開ウィザードとほとんど変わりません。違いが現れるのは、図 9 に示した、要求、応答、およびエラーの各メッセージに対するセキュリティ ポリシーを構成する手順です。また、使用するトークンの種類と、その詳細の一部を指定できます。

WSE 2.0 のウィザードで生成されたコードを調べると、仮想ディレクトリにある WSE 2.0 ポリシー ファイルを使用して、アプリケーションが構成されていることがわかります。このポリシーは、ウィザードで指定した設定を基に生成されています。場合によっては、ウィザード完了後に WSE 2.0 構成ツールを実行してポリシー構成を完了する必要があります。構成ツールを使用することで問題がなくなります。

また、生成されるコードは ASMX プログラミング モデルではなく WSE 2.0 メッセージング API を使用します。生成される BizTalk 受信場所は、SOAP バージョンとほぼ同じですが、WSE トランスポート構成ダイアログにセキュリティ関連の構成オプションが追加されている点が異なります。

送信側は、SOAP アダプタよりも簡単になっています。WSE 2.0 送信ポートは、Web サービス プロキシ クラスを指定する必要がありません。Web サービスの URL と適切なポリシー構成を指定するだけで、適切なメッセージが自動的に作成され、送信されます。

WSE 2.0 アダプタを使用し、オーケストレーション内で Web サービスを使用するときは、[Web 参照の追加] も不要です。代わりに、[生成した項目の追加] で [アダプタ メタデータの追加] を選択します。アダプタの追加ウィザードが表示されたら、登録済みアダプタの一覧から WSE を選択します。[次へ] をクリックすると、BizTalk Adapter for WSE Schema Generation (BizTalk Adapter for WSE スキーマ生成) ウィザードが表示されます。

BizTalk Adapter for WSE Schema Generation ウィザードでは、ダウンロードする WSDL 定義のアドレスを指定し、選択肢となるサービスと操作の一覧を取得します。この一覧から、サービスと一連のメソッドを選択します。次に、サービスと通信するために必要な WSE 2.0 ポリシー ファイルを構成します。ポリシー ファイルは、任意の URL (があれば) からダウンロードするか、図 9 に示すポリシーの構成ダイアログを使用して手動で構成します。

[Web 参照の追加] と同様に、サービスと通信するために必要な、スキーマとポートの種類を追加します。また、PolicyCache.xml という WSE 2.0 ポリシー ファイルが生成され、WSE 2.0 アダプタのインストール ディレクトリの Policy ディレクトリに保存されます (%PROGRAMFILES%\Microsoft BizTalk Server Adapter for WSE 2.0\Policy を参照してください)。ここは、BizTalk がすべての WSE 2.0 ポリシーを検索する場所です。WSE 2.0 アダプタをインストールするときは、この PolicyCache.xml グローバル ファイルを参照する <policy> 要素が含まれた BTSNTSvc.exe.config が更新されます。

WSE 2.0 アダプタには新しいメッセージ プロパティが多数導入され、それを使用すると WSE.SoapAction、WSE.SoapHeaders、WSE.AddressingHeaders、WSE.ReferralHeaders、WSE.SecurityHeaders、WSE.ClientCertificate など WSE 固有の情報にアクセスできます。詳細については、WSE 2.0 アダプタのドキュメントを参照してください。

要約すると、WSE 2.0 アダプタの主な新機能は、メッセージベースのセキュリティ ポリシーのサポートと、オーケストレーションを使用しないメッセージングのみのソリューションにおけるコンテンツベースのルーティングのサポートです。

BizTalk リソース
  • BizTalk Server 2006 の理解
  • BizTalk Web Services FAQ
  • BizTalk Server 2004 and Web Services
  • Implementing Service Contracts with BizTalk Server 2004
  • Walkthrough for BizTalk Adapter for WSE 2.0

WCF アダプタ

BizTalk Server 2006 R2 リリースに付属する Windows Communication Foundation アダプタは、特定の WS-* やトランスポートに依存しないメッセージングを一括して完全にサポートします。実は、BasicHttpBinding、WSHttpBinding、NetTcpBinding、NetNamedPipeBinding、NetMsmqBinding など、Windows Communication Foundation のビルトイン バインドの多くについて、個別のアダプタが用意されます。また、バインドを 100% ユーザーが構成できるカスタム Windows Communication Foundation アダプタもあります。このカスタム アダプタがあれば十分ではありますが、一般的なメッセージング シナリオを対象にした場合は、他のバインドの方が便利です。

カスタム アダプタは、現行の SOAP アダプタや WSE アダプタ以上の機能を発揮できます。セキュリティ、信頼できるメッセージング、トランザクションなどについての各種仕様を含んだ WS-* スタックを完全にサポートします。これらの仕様がサポートされていることで、セキュリティやルーティングが複雑なシナリオを扱うだけでなく、Web サービス トランザクションにメッセージ ボックスを含めることも可能です。

それだけでなく、ホスト、通信、および構成を簡素化できます。たとえば、Windows Communication Foundation アダプタを使用すると、受信場所を構成して BizTalk 管理コンソールで有効にするだけ (公開は不要) で、HTTP 以外のサービスも BTSNtSvc.exe 内でホストできます。

とは言え、最も目を引く機能は統合です。Windows Communication Foundation アダプタには、SOAP アダプタと WSE アダプタの機能が統合されているので、この 2 つのアダプタは不要になります。将来、さまざまなサービス構成を統合するときに必要になるのは、Windows Communication Foundation アダプタのみです。Windows Communication Foundation、およびその ASP.NET や WSE Web サービスとの相互運用性の詳細については、msdn2.microsoft.com/ms735119.aspx の Windows SDK を参照してください。

まとめ

BizTalk Server 2006 が、ビルトインの SOAP アダプタおよび WSE アダプタによって現行の Web サービス テクノロジをサポートすることは既に説明したとおりです (BizTalk の詳細については、「BizTalk リソース」を参照してください)。SOAP アダプタは BP 1.1 に準拠する通信を実現し、WSE アダプタは高度な WS-* サポートを提供します。しかし、特定のトランスポートに依存しない WS-* メッセージングは、BizTalk Server 2006 の、間もなくリリースされる Windows Communication Foundation アダプタによって初めて完全に実現されます。今回の内容は、Windows Communication Foundation アダプタの主要機能の紹介にすぎません。今後パブリック ベータのリリースが迫った時点で、詳しいコラムを提供することをお約束します。

ご意見やご質問は、

Aaron sstation@microsoft.com.

まで英語でお送りください。
Aaron Skonnardは、Microsoft .NET のトレーニング プロバイダである Pluralsight の共同設立者の 1 人です。Aaron は、Pluralsight が提供する Applied Web Services 2.0、Applied BizTalk Server 2006、および Introducing Windows Communication Foundation の各コースの著者です。Aaron はこれまで何年もの間、コースの開発、会議での講演、およびプロ開発者の指導に従事してきました。連絡先は pluralsight.com/aaron です。


Bugslayer: こちら GUI コントロール、トム少佐応答せよ

Bugslayer
こちら GUI コントロール、トム少佐応答せよ
John Robbins

コードのダウンロード : : Bugslayer2007_03.exe (199KB)
コンテンツ
Microsoft UI Automation
UISpy
より適切な UI Automation テスト
UIWindow クラス
UIMenuBar クラス
まとめ
ヒント

Windows 開発に携わった 14 年間で私が見た文句なしに最高のデモは、2003 年の Longhorn Professional Developer's Conference (PDC) で行われたものです。Windows® Presentation Foundation と Visual Studio® 2005 のベータ版についてのセッションも多くの聴衆を沸かせましたが、技術会議では見たことがなかったスタンディング オベーションを受けたのは、Active Accessibility® のプログラム マネージャだけでした。彼は、"Longhorn" (現在の Windows Vista™) のビルドのデモを行いました。これは、GUI アプリケーションの入力キューを分離できるようにしたものでした。そのデモは、フォアグラウンドでソリティアを実行していました。そして、その一方、バックグラウンドでは、Microsoft® .NET ベースのアプリケーションが、Active Accessibility から基になるオートメーション API を使用して、デバイス マネージャでハードウェア デバイスの追加と削除を行ったのです。彼は、デバイス マネージャ内をあちこちクリックしたりもしていたのですが、すべての入力は無視されました。これは、コントローラ プログラムがその入力キューを完全に制御していたからです。

Windows 開発における問題点は、以前の Bugslayer コラムで開発した Tester アプリケーションのように、マウスおよびキーボードの再生ユーティリティを備えた GUI アプリケーションを自動化しようとする場合に、タイミング悪くマウスに当たって動かしてしまうとオートメーションが中断される可能性があることでした。PDC デモの何にそんなに興奮したかというと、アプリケーションの GUI 部分の自動化を可能にするという夢が現実のものになったこと、そして再生が想定どおりに正確に動作することが保証されたことについてでした。何らかの Windows ベースのアプリケーションを作成している場合は、それがアプリケーション全体、インストール、または最新の AJAX バズワードへの対処のどれであっても、そのアプリケーションには GUI を持つ部分があります。アプリケーションの GUI 部分に対して一貫した正確なテストを実行できることは、優れたコードへの鍵となります。

このコラムに取り組んでいる間に、Windows Vista の製品版がリリースされました。悲しいことに、あれほど大きな賞賛を得た複数入力キュー機能は最終リリースまで行き着くことができませんでした。しかし、GUI オートメーションのかなりの部分が Active Accessibility に組み込まれました。また、新しい GUI オートメーション部分は、.NET Framework 3.0 を通じて利用できるようになったので、Windows Vista だけでなく、Windows XP および Windows Server® 2003 でも機能するようになりました。さらに、Active Accessibility コードは、Windows Presentation Foundation UI および HWND ベースのアプリケーションの両方でもシームレスに機能します。おそらく、オペレーティング システムの次期リリースで、分離入力キューの究極のゴールに到達することでしょう。しかし、新しい Microsoft UI Automation でも、アプリケーションの GUI 部分の自動化はかなり簡単になっており、今日からでも使用できます。

このコラムの目的は、UI Automation ツールおよび API を使用して GUI オートメーションを開始する方法を示すことです。API を直接使用するよりもずっと簡単に GUI テストができるように、ほとんどの Bugslayer コラムと同じく多くのコードを用意しました。既に msdn2.microsoft.com/ms747327.aspx のドキュメントは読んでいらっしゃるでしょうが、まだの場合は先にお読みください。そうでないと、これ以降のコラムの内容がわかりづらいと思います。なお、.NET Framework 3.0 のインストールと Windows Vista および .NET Framework 3.0 Software Development Kit も必要になります。

Microsoft UI Automation

Microsoft UI Automation のドキュメントを読むと、そのルーツは、まさに Active Accessibility にあることがわかります。説明のほとんどは、ウィンドウの特定の子コントロールすべての検索を軸として展開します。スクリーン リーダーなどの支援ツールではその情報を迅速に得る必要があるので、自分で 2 つの別々のコード セットを書かなくても HWND ベースのツリーと Windows Presentation Foundation アプリケーションのツリーの両方を簡単に取得できる API があるのは良いことです。

独自のプロバイダを記述する方法についてもわかりやすく説明されています。このプロバイダは、Microsoft UI Automation API を通じて一定の方法でカスタム コントロールにサービスを行います。独自に一意のカスタム コントロールを記述している場合は、支援技術を利用するユーザーにアプリケーションを使ってもらえるように、「UI Automation Providers Overview」(msdn2.microsoft.com/ms750446.aspx) を読んでおくべきです。プロバイダの作成に必要な作業はそれほど面倒なものではありませんが、このコラムで今回説明する自動テストの内容には含まれていません。

Microsoft UI Automation API で非常の興味深い部分の 1 つに、その優れたイベント サポートがあります。ジャーナル フックをインストールしてマシン上のキーボードおよびマウスの使用を記録することはできますが、フォーカスされているコントロール、およびウィンドウが開閉されたタイミングなどについての通知を取得するためのすっきりした API はありません。私の Tester アプリケーションのコードを見返せば、HWND ベースのアプリケーションでそのような通知を得るために Computer Based Training (CBT) フックとかなりの推測を用いていたのがわかります。新しいイベント システムで最も感銘を受けたのは、イベント通知を分離できることです。これで、単一のコントロールから、子コントロールを持つウィンドウ、そしてデスクトップ全体に至るまでのすべてを取得できます。イベント制御を使用してアプリケーションの使用を記録する例については、「Test Script Generator Sample」(msdn2.microsoft.com/ms771275.aspx) が大いに参考になります。

Microsoft UI Automation API 全体で重要なのは、すべてがコントロールの種類をベースにしていることです。AutomationElement クラスのインスタンスが各コントロールをラップするので、画面上のコントロールの種類とそれで実行可能な機能をすばやく特定できます。以前は、そのような詳細レベルを得るために市販のアプリケーションが使われていました。これまでにそのような高価な特注の自動テスト ツールを扱う必要があった場合は、無料ダウンロードにすべて含まれているありがたみがおわかりでしょう。

定義されているコントロールは 38 種類あり、ボタンからデータ グリッドまですべてをカバーしています (msdn2.microsoft.com/ms749005.aspx)。プロバイダ モデルが自動テストの観点から見て興味深いのは、これらのコントロールの種類が、それぞれがサポートしているプロパティおよび制御パターンを正確に示していることです。ご想像のとおり、プロパティは、その種類のコントロールで必須またはオプションである基本クラス AutomationElement のプロパティのサブセットです。たとえば、ボタン コントロールは、そのボタンで使用されるアクセラレータ キーを返す AcceleratorKeyProperty をサポートする必要があります。

制御パターンはより興味深いものです。「UI Automation Control Patterns Overview」(msdn2.microsoft.com/ms752362.aspx) では、"Control patterns provide a way to categorize and expose a control's functionality independent of the control type or the appearance of the control." (制御パターンは、コントロールの機能を分類し、それをコントロールの種類または外観とは切り離して公開する方法を提供します) と的確にまとめられています。このコラムの後半で説明するように、GUI の自動化では制御パターンを使用して多くの作業を行います。制御パターンのすばらしいところは、コントロールの特定のパターンを組み込めば、パターンによってそのアクションの実行作業が行われる点です。たとえば、ボタンを扱う場合、IInvokePattern インターフェイスが、ボタン クリックを行う Invoke メソッドを公開します。このため、Windows メッセージや SendInput API と格闘する必要はありません。

自動テストでの API の使用についてのドキュメントはあまり多くありませんが、サンプルを調べたり少し資料を読んだりすれば、それほど苦労しなくても何が必要かを理解できるはずです。UI Automation API の設計チームに対しては、その国際化作業について大いに讃えなければなりません。これまで自動化のためのツールおよび API では、メニュー項目を検索するために「ファイル」や「開く」などの文字列を埋め込む必要がありました。これは、自作の GUI オートメーション スクリプト内の国際化をすべて自分で行う必要があることを意味していました。これは面倒なだけでなくテスト スクリプトの再利用性も低下していました。それが今日では、アプリケーションの特定のコントロールにアクセスするときは、AutomationId プロパティを使用できるようになりました。この名前は一意であり、ロケールの設定にかかわらずどのマシンでも同じであることが保証されています。これでスクリプトを再利用できるようになっただけでなく、作成者と同じようにアプリケーションをテストできるという点で翻訳者にとっても大きな力になっています。

UI Automation API に欠けている UI テスト機能の 1 つがマウス サポートです。これについてはまったく用意されていません。ほとんどのアプリケーションの場合は、制御パターンが同等の機能を提供するので、それほど問題にはなりません。しかし、描画アプリケーションを使用している場合に、マウスでスマイリー フェイスを描く助けになるものは何もありません。この場合は、マウス サポートを提供する Tester のような何らかのツールを、UI Automation API を使用するプログラムと組み合わせて使用して、必要なマウス依存を実現できるところまでこぎつけることができます。

UISpy

皆さんの UI テストを大幅に簡素化するために私が書いたすばらしいコードを紹介する前に、テストの記述を支援するために多用されるであろうツール UISpy (図 1) について説明する必要があります。UISpy は Windows Vista および .NET Framework 3.0 Software Development Kit に含まれています。基本的に、UISpy は、デスクトップからずっと下の方まですべてのコントロールを確認して Microsoft Automation UI 形式で表示するためのツールです。したがって、特定のコントロールにはどんなプロパティがあるかをまず調べることができます。また、Microsoft Automation UI API を使用して、コントロールのパターンや特定のプロパティのクエリを実行できます。

図 1 UISpy ユーザー インターフェイス
図 1 UISpy ユーザー インターフェイス(画像を縮小するには、ここをクリックします)
図 1 UISpy ユーザー インターフェイス
図 1 UISpy ユーザー インターフェイス (画像を拡大するには、ここをクリックします)

ツール使用したり UISpy メニューをクリックしたりすれば、それぞれがどのように機能するかがわかります。しかし、私は今月のコラムに付けるコードを正しく機能させるために UISpy で長々と充実した時間を過ごしました。皆さんを待ち受ける厄介事の山を小さくするために、私がぶつかった難題について、2、3 お話しましょう。

UISpy のツリー コントロールには、特定のビュー ([Raw View]、[Control View]、または [Content View]) の階層が表示されます。[Raw View] には、特定のネイティブ プログラム構造に最も緊密に関連する階層が表示されますが、使用することはめったにないでしょう。[Control View] は、ユーザー インターフェイスを見たときにユーザーが認知するものにマップされている階層を示します。[Content View] は、[Control View] サブセットですが、ここにはユーザー インターフェイスに本当に情報を持つコントロールだけが表示されます。Microsoft Automation UI API を使用する場合は、これらの階層のいずれかを使用して、アプリケーションのコントロールにアクセスします。

UISpy では、[Control View] のツリー コントロールにバグがあるようで、ノードを右クリックしたときに選択内容が更新されないことがあります。ショートカット メニューは、ビューの更新、フォーカスの設定、または制御パターンへのアクセスで非常に重要ですが、そのようなメニュー オプションを、以前選択したノードに対して実行してしまっていることがあります。ショートカット メニューにアクセスする場合は、常に、まずマウスで項目を選択し、その後で右クリックしてください。間違ったコントロールの制御パターンをいったい何度見たか、とても思い出せません。

Microsoft Automation UI API に関するドキュメントには、複数の場所に、ルート要素、つまり Desktop Window を検索の基点として使用すると非常に時間がかかると記載されています。これについてはマニュアルを信じてください。UISpy を使用してアプリケーションを表示する場合は、アプリケーションの子ノードを直接選択して右クリックし、[Scope to Element] を選択します。これで、すべてのフォーカス追跡および強調表示が、使用しようとしているアプリケーションだけに分離されます。

UISpy の優れた機能の 1 つは、ホバーリング モードです。このモードでは、画面のコントロールにマウスを移動して Ctrl キーを押すと、開かれているすべてのツリー コントロール内のそのコントロールのノードにジャンプすることができます。ホバーリング モードは、メニュー項目を表示しているときに特に重要です。メニューを展開しないと子ノードが表示されないからです。問題は、UISpy が Ctrl キーの押下を常に認識するわけではないことです。それで、Ctrl キーを複数回押す癖を付けてください。

UISpy に関して最後に挙げる最悪の問題は、UISpy の障害によるものではありません。しかし、自分が所有していないユーザー インターフェイス部分を自動化する方法を調べるときには本当にイライラさせられます。その完璧な例として、Windows Vista の新しい共通の [ファイルを開く] ダイアログ ボックスがあります。ダイアログ ボックスを見ると、ほとんどのコントロールが、典型的な HWND ベースの編集ボックス、ボタン、リスト、コントロールなどのように見えます。しかし、見た目にだまされてはなりません。UISpy では、[開く] ボタンのように見えるものは、ボタン コントロールとして報告されることもありますが、ときにはペイン コントロールとして報告されることがあります。これは、ドキュメントによると、「ウィンドウまたはドキュメントより下位だが、個別のコントロールよりは上位のグループ レベルを表す」ものです。

自動化しようとしているユーザー インターフェイスを所有していない場合は、それを UISpy で何度も調査して、そのコントロールが本当は何であるかを正確に理解する必要があります。ホバーリング モードと完全なフォーカス追跡モード (UISpy ツリー ビューでフォーカスを持つコントロールにジャンプするモード) を切り替えて使用していくと、その謎のコントロールの正体が徐々にわかってくるはずです。

より適切な UI Automation テスト

UISpy の使い方もわかり、基本 API にも慣れてきたので、皆さんのお気に入りのアプリケーション、メモ帳を動かすのに Microsoft UI Automation API を使い始めました。私の計画は、自動テストを Visual Studio Team System のすばらしい単体テスト ツールと統合する方法を示すことができるように、API について学ぶことでした。NUnit (nunit.org) でも、本質的に同じことを実行できます。

ここでは、単純明快に Microsoft UI Automation API を使用して、以下を行うアプリケーションを書きました。

  • プロセスを確実に DPI 対応にする。
  • メモ帳を起動し、[ファイル] メニューの [開く] を実行する。
  • [ファイルを開く] ダイアログ ボックスで、プログラムのソース コードを開く。
  • [ヘルプ] メニューの [バージョン情報] を実行し、[OK] をクリックする。
  • [ファイル] メニューの [メモ帳の終了] を実行する。

このプログラムの一部を図 2 に示していますが、すべてのコードとコメントを読むことを強くお勧めします。そうすれば、Microsoft UI Automation API の使用方法についての有益なアイディアが得られるはずです。特にコア API の使用に焦点を当てるため、コードにはエラー処理を含めていないことに注意してください。

Figure 2 生の Microsoft UI Automation の使用 (Close Figure 2)
#region Using Statements
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Text;
using System.Windows.Automation;
using System.Windows.Automation.Provider;
using System.Threading;
using System.ComponentModel;
using System.IO;
using System.Globalization;
using System.Windows.Forms;
using System.Runtime.InteropServices;
#endregion

namespace RawUsageExample
{
    internal static class NativeMethods
    {
        // The Windows Vista DPI aware function.
        [DllImport ( "user32.dll" , EntryPoint = "SetProcessDPIAware" )]
        [return: MarshalAs ( UnmanagedType.Bool )]
        private static extern bool RealSetProcessDPIAware ( );

        internal static bool SetProcessDPIAware ( )
        {
            Boolean returnValue = false;
            try
            {
                returnValue = RealSetProcessDPIAware ( );
            }
            catch ( EntryPointNotFoundException )
            {
                // Not running on Windows Vista.
            }
            return ( returnValue );
        }
    }

    class Program
    {
        [STAThread]
        static void Main ( )
        {
            // Set this process to DPI aware if running on Windows Vista.
            NativeMethods.SetProcessDPIAware ( );

            // Start Notepad and connect up to it.
            // Wait for the process to get going enough to have a UI.
            Process proc = Process.Start ( "notepad" );
            Thread.Sleep ( 1000 );

            // Attach to the main window.
            AutomationElement rootElement =
                AutomationElement.FromHandle ( proc.MainWindowHandle );

            // Create the AndCondition to find the menuBar.
            AndCondition menuBarFind = new AndCondition (
                new PropertyCondition (
                    AutomationElement.ControlTypeProperty,
                    ControlType.MenuBar ) ,
                new PropertyCondition (
                    AutomationElement.AutomationIdProperty, "MenuBar" ) ,
                Automation.ControlViewCondition );

            // Find the menuBar, which is a child of the main window.
            AutomationElement menuBarElement =
                rootElement.FindFirst ( TreeScope.Children, menuBarFind );

            // Create the AndCondition to find the File menu.
            AndCondition fileMenuFind = new AndCondition (
                 new PropertyCondition (
                     AutomationElement.ControlTypeProperty ,
                     ControlType.MenuItem ) ,
                 new PropertyCondition (
                     AutomationElement.AutomationIdProperty , "Item 1" ) ,
                 Automation.ControlViewCondition );

            // Find the File menu.
            AutomationElement fileMenuElement =
                menuBarElement.FindFirst ( TreeScope.Children ,
                fileMenuFind );

            // Get the control pattern for ExpandCollapse and do the
            // Expand to get the children.
            ExpandCollapsePattern fileExpandPattern = fileMenuElement.
                GetCurrentPattern ( ExpandCollapsePattern.Pattern )
                    as ExpandCollapsePattern;
            fileExpandPattern.Expand ( );
            Thread.Sleep ( 1000 );

            // Create the AndCondition to find the Open menu.
            AndCondition openMenuFind = new AndCondition (
                 new PropertyCondition (
                     AutomationElement.ControlTypeProperty ,
                     ControlType.MenuItem ) ,
                 new PropertyCondition (
                     AutomationElement.AutomationIdProperty , "Item 2" ) ,
                     Automation.ControlViewCondition );

            // Find the Open menu.
            AutomationElement openMenuElement =
                fileMenuElement.FindFirst ( TreeScope.Descendants,
                    openMenuFind );

            // Get the InvokePattern off the openMenu.
            InvokePattern openInvokePattern =
                openMenuElement.GetCurrentPattern (
                    InvokePattern.Pattern ) as InvokePattern;

            // Invoke the menu to get the Open dialog.
            openInvokePattern.Invoke ( );
            Thread.Sleep ( 1000 );

            ...

図 2 のコードを読み終えたとき、皆さんの目はおそらくどんよりと曇っていることでしょう。それは、Microsoft UI Automation API が、大きな力を与えてくれるものである一方で、これまで開発された中でも最も多くの手間がかかる API の 1 つであるに違いないからです。図 2 のコードのほとんどは、[ファイル] メニューの [開く] を開くことだけに費やされています。Microsoft UI Automation API を直接使用することは、わずかに異なる 5 ～ 10 行のコードを何度も何度も (さらに何度も何度も!) 書かなければならないことを意味します。実際に、前に箇条書きにした 5 つの手順を実行するコードは、全部で 250 行になります。このような API の問題は、開発者が結局は "エディタの継承" (またの名を切り貼り) に頼って自分の単体テストを作成するしかないことで、結果的にコード内にあらゆる種類のバグが入り込むことになります。そして次には API の使用中止につながります。

API の設計者が成し遂げようとしたことは理解できますが、大きな失敗はこれをフラットな API にしたことです。操作できるのは、コントロールの種類が何であっても、ただ 1 つのクラス AutomationElement だけなのです。このため、[ファイル] メニューを探し、ExpandCollapsePattern を探し、実際の展開を実行し、GUI にメニューを表示する時間を与えるのに、コード 7 行を要します。ときに無限の柔軟性を持たせようとする場合は、実際の必要以上に使いにくい API を作成することになります。

2 度目に Microsoft UI Automation API について学ぼうとしたとき、私は、読者が自分で操作しているコントロールの正確な種類を認識できるように、38 種類のコントロールそれぞれを独自のクラスにカプセル化するという作業を行う必要がありました。さらに、生の API でのひたすら単調でつらい仕事をできるだけ隠すようにしましたが、高度な作業を行う必要がある場合に備えて、基になる API へのアクセスは残しています。図 3 のコードは、図 2 と同じプログラムを示していますが、Bugslayer.TestTools.GuiAutomation 名前空間コードを使用しています。ここでも、コードに焦点を当てるためにエラー チェックは含めていません。

Figure 3 Bugslayer.TestTools.GuiAutomation ラッパー (Close Figure 3)
#region Using Statements
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Text;
using System.Windows.Automation;
using System.Windows.Automation.Provider;
using System.Threading;
using System.ComponentModel;
using System.IO;
using System.Globalization;
using System.Windows.Forms;
using Bugslayer.TestTools.GuiAutomation;
#endregion

namespace UIUsageExample
{
    class Program
    {
        [STAThread]
        static void Main ( )
        {
            // Start Notepad and connect up to it.
            // Wait for the process to get going enough to have a UI.
            Process proc = Process.Start ( "notepad" );
            Thread.Sleep ( 1000 );
            UIWindow notepadWindow = new UIWindow ( proc );

            // Now we're on the main window so execute the
            // File Open command.
            UIMenuBar menuBar = notepadWindow.FindChild (
                "MenuBar" , ControlType.MenuBar ) as UIMenuBar ;

            // This wraps all the goo necessary to invoke the Open menu.
            menuBar.InvokeMenus ( "Item 1" , "Item 2" );

            ...

もうお気づきでしょうが、図 3 のコードは、図 2 よりもずっと短くなっています。実際、プログラムは全部でわずか 81 行、元のコードの 32 % です。また、Bugslayer.TestTools.GuiAutomation を使用するコードの方がずっと読みやすくわかりやすいはずです。賭けてもいいです。そして、もちろん、コードが使いやすければ、それを使用する可能性が高くなります。

UIWindow クラス

この時点で、今月の Bugslayer のコードをダウンロードしてじっくり眺めることができます。実装の詳細の一部を示しますので、調べてみてください。Visual Studio Team Developer Edition または Visual Studio Team Suite がある場合は、Bugslayer.TestTools.GuiAutomation\Bugslayer.TestTools.GuiAutomation.SLN ファイルを開いてコードと単体テストの両方を表示できます。Visual Studio Professional Edition だけがある場合は、UIUsageExample\UIUsageExample.SLN を開けば、プロジェクトおよび参照の欠如についてのエラーは発生しません。両方のソリューション ファイルには、すべてのコア コードを含む Bugslayer.TestTools.GuiAutomation.CSPROJ ファイルが含まれています。

これから調べるクラスは、Bugslayer.TestTools.GuiAutomation に存在するすべてのベースである UIWindow です。これは、AutomationElement クラスをラップし、RootElement プロパティとして公開されます。派生クラスの実際の機能のほとんどすべては、UIWindow で見つかります。たとえば、AutomationId、Name、IsEnabled など、AutomationElements で読者がアクセスするほとんどの共通プロパティはすべてそこにあります。

子コントロール検索メソッドも、UIWindow にあります。開発者が私の API をどのように使用するかを考えたとき、子コントロールの直接検索と、ときどき行われる祖先コントロールの検索の 2 つが主な用途だろうと推測しました。それで、検索に AutomationId を使用する FindChild/FindDescendent メソッドを多数用意しました。追加のオーバーロードでは、正確な ControlType も指定させるようにしています。これは、開発者が、想定どおりの正しいコントロールを検索していることを確認できるようにするためです。

AutomationId 値は、国際化の点では優れていますが、値が再使用されるので、子孫ツリーを検索する場合は十分な注意が必要です。たとえば、メニュー バーの [ファイル] メニューの値は "Item 1" ですが、[ファイル] メニューを展開すると、[開く] メニュー項目の値も "Item 1" になります。

AutomationId 値での検索以外に、FindChildByName メソッドおよび FindDescendentByName メソッドを使用したローカライズされた名前によるコントロールの検索もサポートしています。検索対象が何であれ、私の Find* メソッドは、AutomationElement.FindFirst メソッドのラッパーなので、特定の条件に最初に一致した項目だけを返します。この方法を選択したのは、GUI アプリケーションを動かす場合は、あらかじめどんなコントロールがユーザー インターフェイスにあるかがわかっているので、通常は、画面上にどんなコントロールがあるかわからないことがないからです。検証のために、あえて AutomationElement.FindAll メソッドを呼び出して一致するコントロールをすべて返す必要がある場合は、UIWindow.RootElement プロパティを使用して、必要なものをすべて検索し、並べ替えてください。

ドキュメントでコントロールの検索について読んでいれば、RawView、ControlView、および ContentView での検索について大きな議論があったことをご存知でしょう。UIWindow.Find* メソッドは、既定で、Automation.ControlViewCondition で検索を行います。特定の UIWindow の検索を変更する場合は、TreeSearchCondition を Automation.ContentViewCondition、Automation.ControlViewCondition、または Automation.RawViewCondition に設定します。

Find* メソッドについて最後に言いたいのは、UIWindow コードを読んでほしいということです。なぜなら、私がどのようにしてコードをいくつかの非常に小さいメソッドにリファクタしたか、そして Microsoft UI Automation API でのコントロールの検索についての考え方が示されているからです。

UIWindow クラスでたいへんな作業なのは、制御パターンの保護ラッパーを提供することです。38 種類の異なるコントロールは、多数の制御パターンを使用しますが、私は、パターンのアクセスおよび使用の詳細をすべて単純なラッパー プロパティおよびメソッドに埋め込もうと考えました。たとえば、メニュー項目コントロールの種類は、ExpandCollapsePattern を実装できます。私のコードには、protected static UIWindow.ImplementsExpandCollapse メソッドを呼び出す SupportsExpandCollapsePattern を持つ UIMenuItem クラスがあります。UIMenuItem クラスには、SupportsExpandCollapsePattern が true を返す場合に呼び出すことができる、残りの ExpandCollapsePattern、ExpandCollapseState、Expand、および Collapse のためのパブリック メソッドおよびプロパティがあります。

制御パターン コードをすべて UIWindow に実装することによって、(同様に ExpandCollapsePattern をサポートできる) UIComboBoxControl などの他のコントロールの種類は、そのパターンのパブリック メソッドを実装するだけで済むようになります。私が考えたのは、それらのクラスがサポートするメソッドとプロパティだけを公開して API をずっと簡単に使用できるようにすることです。

Bugslayer.TestTools.GuiAutomation を設計するときに直面した大きな課題は、UIWindow.Find* メソッドが正確な種類を返すようにする方法を見つけることでした。例として UIWindow.FindDescendant を見ると、UIWindow が返されます。私は、自分の実際の自動テスト コードが、メニュー項目を検索して UIMenuItem クラスを返すようにすることを目指しました。これは、UIWindow クラスに、派生クラスの演繹的知識を与えなければならなくなることを意味しました。この考えは、オブジェクト指向の意味では純粋とはいえませんでしたが、きちんと機能し、Bugslayer.TestTools.GuiAutomation をずっと使いやすくしてくれています。この改良を加えなかったら、読者は次のようなコードを書かなければならないところでした。

UIWindow rawMenuBarWindow = notepadWindow.FindChild (
    "MenuBar" , ConwwtrolType.MenuBar );
UIMenuBar menuBarWindow = new MenuBar ( rawMenuBarWindow.RootElement );

UIWindow メソッドは、新しい UIWindow を作成する必要がある場合は UIWindow.AllocateExactType メソッドを呼び出します。これは、唯一のパラメータとして AutomationElement を受け取ります。AutomationElement 内で、AllocateExactType は、ControlType プロパティを探して適切な派生クラスを割り当てます。正確なクラス型を割り当てることで、それを演算子として活用する次のようなコードを記述できます。

UIMenuBar menuBar = notepadWindow.FindChild (
    "MenuBar" ,  ControlType.MenuBar ) as UIMenuBar ;

コントロールの種類を Bugslayer.TestTools.GuiAutomation に追加する場合は、AllocateExactType で適切な割り当てを追加していることを確認する必要があります。API が使いやすくなるのなら、私はいつでもオブジェクト指向の純粋さを犠牲にできます。

テキスト入力に関して、やや興味深いポイントが 1 つあります。コントロールの種類についてのドキュメントを読めば、テキストの取得および設定に必要な TextPattern および ValuePattern をサポートするコントロールの種類がわずかしかないことがわかります。それらの特定のコントロールについて私が決意したことは、静的なテキスト制御をモデル化する UITextControl クラスを、TextPattern および ValuePattern をサポートするすべてのコントロールの基本クラスとして機能させることでした。そうすれば、機能が共有されますし、UIEditControl および UIToolTopControl がいずれも UITextControl から派生する階層を構築できます。また、UIDocumentControl が UIEditControl から派生します。これは、コードの再利用をサポートし、正しい ControlType が適切なクラスで使用されることを保証します。

Microsoft UI Automation API では、複数行の入力を許可するコントロールは、いずれも ControlType.Document と見なされます。ドキュメントの記載によると、ドキュメント コントロールが ValuePattern をサポートすることは想定されていません。つまり、これらのコントロールの値を設定する方法はないことを意味します (TextProvider は、ドキュメント コントロールからのデータを提供します)。これは当然です。というのは、テキストよりも複雑なデータをサポートする各プログラムは非常にモデル化が難しくなるからです。しかし、テキストをドキュメントに挿入するだけでも役に立つ場合もあるので、System.Windows.Forms の SendKeys クラスを、UIWindow にラップしました。また、スマートをクラスに挿入して、実際のキーボード入力を実行する前に、モデル化されているウィンドウにフォーカスが確実に設定されるようにしました。

UIMenuBar クラス

最後に説明する興味深いヘルパは、UIMenuBar クラスです。Microsoft UI Automation API クラスを使用する場合、通常のドロップダウン メニューの実行には、メニューの検索、その展開、適切な子の検索、そしてその呼び出しが含まれます。もう一度、図 2 を見ると、単に [ファイル] メニューの [開く] を実行するだけなのに、その作業がいかに面倒かがわかります。メニューの操作は非常に苦痛なので、UIMenuBar.InvokeMenus コマンドでは、1 回の呼び出しで可変長の文字列パラメータを受け取り、共通の操作を実行できるようにしました。最初の文字列は、メニュー バーの子である最初のメニューと見なされます。それ以降のパラメータは、最後を除き、展開されるメニューです。通常、最後のメニューが InvokePattern を持つメニューなので、UIMenuBar.InvokeMenus でそれを呼び出します。図 3 からわかるように、これによって、通常なら記述が必要なコードが確かに大幅に減っています。

まとめ

私が使いやすいと思うライブラリは Bugslayer.TestTools.GuiAutomation に収められています。自分のユーザー インターフェイス オートメーションと単体テストの結合するときに調査していただけることを期待します。コードのテストにかかわる他のものと同じように、それを製品のような成果物として扱う必要がありますが、コードの品質はどんどん向上するので見返りは非常に大きくなります。

Bugslayer.TestTools.GuiAutomation の使用方法について他のアイディアを探す場合は、Bugslayer.TestTools.GuiAutomation\Tests\Bugslayer.TestTools.GuiAutomation.Tests ディレクトリの単体テストを調べてみてください。それらのテストは、ライブラリをテストするために、そのあらゆる面を使用しています。コード カバレージの熱心な信奉者として、私は、既存の単体テストで (Windows XP で) コードの 90 パーセント以上を網羅したことを報告できることを誇りに思います。適切なバージョンの Visual Studio を所有している場合は、テストを実行してみてください。メモ帳の異なるインスタンスが 5、6 個同時に自動化されるのを見るのはわくわくします (テストはランダムな順序で実行されるので、インターリーブにできます)。

Bugslayer.TestTools.GuiAutomation の拡張方法について、いくつかアイディアを残しておきましょう。コードを見れば、すべてのコントロールの種類をモデル化しているわけではないことがわかります。私が確立した開発パターンに続いて、残りのコントロールの種類を追加してください。コントロールの種類を追加する場合は、適切な単体テストを追加することを忘れないでください。

Microsoft UI Automation API にマウス サポートが欠けていることは、多くのアプリケーションで問題になります。SendKeys 形式で文字列を受け取り、SendInput API を使用して処理を実行する PlayMouse メソッドを UIWindow に追加することを考えてください。SendInput 配列の構築と解析については、私が PlayKeys コマンドを Tester にどのように実装したかを見れば参考になります。

やる気に満ちあふれているという方は、Microsoft UI Automation API の優れたイベント サポートを使用して、ユーザーとアプリケーションとの対話を監視するレコーダを構築することもできます。その対話から、コンパイルとテストに使用できる C# または Visual Basic® ソース ファイルを作成できます。

上のいずれかを行う場合は、ぜひお知らせください。その成果を Bugslayer.TestTools.GuiAutomation に組み込めれば幸いです。

ヒント

ヒント 77 私は Team Foundation System が大好きですが、先日、作業項目をプロジェクト間で移動しなければならないという難題にぶつかりました。個々の項目を手作業で移動しなければならないとわかって、今にも泣きそうになったときに、Eric Lee が自作の Hemi ツールを持って助けに来てくれました。これは、2、3 回マウスをクリックするだけで作業項目をプロジェクト間で移動できるツールです。blogs.msdn.com/ericlee/archive/2006/11/20/work-item-moving-tool-is-back.aspx で Hemi の使用方法を確認して、このすばらしいフリー ツールをダウンロードしてみてください。

ご意見やご質問は、

John slayer@microsoft.com.

まで英語でお送りください。
John Robbinsは、.NET と Windows の両方を専門とするソフトウェアのコンサルティング、教育、開発を行う Wintellect 社の共同設立者です。最新の著書は『Debugging Microsoft .NET 2.0 Applications』(Microsoft Press 刊、2006 年) です。連絡先は www.wintellect.com です。


基礎: Windows Workflow Foundation の追跡サービス

基礎
Windows Workflow Foundation の追跡サービス
Matt Milner

コードのダウンロード : : Foundations2007_03.exe (178KB)
コンテンツ
追跡アーキテクチャ
追跡プロファイル
SQL 追跡サービス
追跡データを照会する
カスタム追跡サービス

Windows Workflow Foundation で最も強力な機能の 1 つが追跡です。この追跡により、ワークフロー内のイベント、アクティビティ プロパティ、およびカスタム データを監視できます。今回のコラムでは、追跡インフラストラクチャについて調べ、SQL Server™ ベースの組み込み追跡サービスを使用する方法と、さまざまな目的に合わせてカスタム追跡サービスを作成する方法を示します。説明の過程で、追跡している情報の使い方や、追跡を使用してアプリケーションの共通要件をいくつか満たす方法も示します。

多くのアプリケーションでは、プログラム ロジックや処理手順の実行を見極めることが求められます。これには、法規制の順守、実行マイルストーンに基づくタスク リストの管理など、さまざまな理由があります。Windows® Workflow Foundation には、アプリケーション別に異なる追跡システムを作成するのではなく、カスタム実装をオーバーレイできる柔軟なインフラストラクチャが用意されています。これにより開発モデルが簡単になるので、追跡対象のビジネス要件に集中することができます。

追跡アーキテクチャ

Windows Workflow Foundation でワークフローをホストしているときは、1 つのワークフロー ランタイムと一連のランタイム サービスの両方をホストしています。永続化サービスなど、多くのランタイム サービスでは、ランタイムにインスタンスを 1 つ登録すれば十分です。追跡サービスは例外で、さまざまなビジネス要件をサポートするために、ランタイムに複数の追跡サービスを追加できます。そのため、複数の異なる追跡サービスを 1 つのアプリケーションに適用できます。この場合、各追跡サービスは、それぞれ特定の実装の追跡を担当します。

Windows Workflow Foundation の追跡は、中心となるインターセプタに基づきます。このインターセプタが、アクティビティとワークフロー イベントを監視し、構成済みの各追跡サービスに適切なデータを送信します (このアーキテクチャを 図 1 に示します)。追跡インターセプタは、すべてのイベントに関する通知を受け取りますが、ほとんどの場合、使用するのはこれらのイベントの一部だけです。さらに、複数の追跡サービスを使用している場合、たいていは各サービスが情報の固有のサブセットを対象にします。そのため、追跡インターセプタは、構成済みの各追跡サービスに対し、ワークフローの種類ごとに追跡プロファイルを照会します。

図 1 追跡アーキテクチャ
図 1 追跡アーキテクチャ(画像を縮小するには、ここをクリックします)
図 1 追跡アーキテクチャ
図 1 追跡アーキテクチャ (画像を拡大するには、ここをクリックします)

追跡プロファイルは、追跡サービスで受け取りたいイベントの種類についての情報をインターセプタに提供します。こうしたイベントは、ワークフロー、アクティビティ、およびユーザーの 3 つのカテゴリに分類されます。

ワークフロー イベントを使うと、ワークフローが遷移する可能性のあるさまざまな状態や、イベントが発生した時刻を追跡できます。カスタム イベント引数を含むイベントの場合、WorkflowTerminated や WorkflowCompleted など、イベント引数自体が追跡され、これを後から照会できます。

アクティビティ イベントを使うと、アクティビティが特定の状態になった時刻と、その状態になったときのアクティビティ プロパティの特定の値を追跡できます。たとえば、電子メールを送信するアクティビティがあるとします。この場合、このアクティビティが Closed 状態になったときの To アドレス プロパティを追跡できます。

最後に、ユーザー イベントを使うと、ワークフローやアクティビティのコード内でデータを手動で追跡できます。

追跡プロファイル

各追跡サービスは、追跡インターセプタに追跡プロファイルを提供しなければなりません。サービスから、すべてのワークフローに同じプロファイルを提供することも、ワークフローの種類別に異なるプロファイルを提供することもできます。プロファイルの管理方法と作成方法は、サービスの作成者が決めます。

追跡プロファイルは、System.Workflow.Runtime.Tracking 名前空間にあるオブジェクト モデルを使用して作成するか、XML で直接作成します。通常はオブジェクト モデルを使用して作成しますが、これだけがプロファイルを表す方法ではなく、常に最適な方法というわけでもありません。そのため、追跡プロファイルは XML にシリアル化したり、XML からシリアル化解除したりできます。TrackingProfileSerializer クラスを使用して、TrackingProfile オブジェクトを XML テキストに変換でき、またその逆変換も可能です。追跡プロファイルの作成に役立つ例と、基本的な追跡プロファイルの編集に適したツールに、Windows SDK の Tracking Profile Designer サンプルがあります。このアプリケーションでは、ワークフローのデザイン画面を使用して、追跡プロファイルを視覚的に定義できます。このツールには、ユーザー追跡ポイントがサポートされないなど、いくつか制限事項がありますが、シリアル化された形式の追跡プロファイルも表示できます。また、多くのシナリオで、追跡プロファイルの開発と展開の速度を向上させることもできます。

基本的には、追跡プロファイルは追跡ポイントのコレクションです。各追跡ポイントは、追跡できるイベントの種類と相互に関連し、データ抽出のタイミングや、抽出対象のデータに関する情報をランタイムに提供します。たとえば、ワークフロー イベントを追跡することを示すには、WorkflowTrackPoint を追跡プロファイルに追加する必要があります。追跡ポイントは、1 つ以上の追跡場所を保持します。追跡場所では、実際にデータを追跡する場所とタイミングを定義します。たとえば、WorkflowTrackPoint では、MatchingLocation プロパティによって、追跡するさまざまなワークフロー イベントが識別されます。図 2 のコードに、作成した追跡プロファイルを示します。この追跡プロファイルには 1 つの WorkflowTrackPoint が含まれ、サービスからワークフローの例外、終了、中止などのエラー状態を追跡することを示しています。

Figure 2 ワークフロー追跡ポイント (Close Figure 2)
//create the profile object and set the version
TrackingProfile profile = new TrackingProfile();
profile.Version = new Version(1, 0, 0);

//define a tracking point
WorkflowTrackPoint wfTrack = new WorkflowTrackPoint();

//create a list of events we are interested in
List<TrackingWorkflowEvent> events = new List<TrackingWorkflowEvent>();
events.Add(TrackingWorkflowEvent.Aborted);
events.Add(TrackingWorkflowEvent.Terminated);
events.Add(TrackingWorkflowEvent.Exception);

//create a tracking location to indicate which events should be tracked
WorkflowTrackingLocation wfLocation =
    new WorkflowTrackingLocation(events);

//set the matching location on the track point
wfTrack.MatchingLocation = wfLocation;

//add the track point to the profile
profile.WorkflowTrackPoints.Add(wfTrack);

ActivityTrackPoint では、さらに充実した追跡がサポートされます。ワークフローの中心はアクティビティによって成り立っているため、これは当然と言えます。ActivityTrackPoint には、MatchingLocations コレクションと ExcludedLocations コレクションの両方が含まれており、データを収集するタイミングを厳密に制御できます。どちらのコレクションでも、照合する状態のセットを表す ActivityTrackingLocation を定義できます。

ActivityTrackingLocation クラスでは、追跡するアクティビティをその型に基づいて定義できます。また、派生アクティビティを照合するかどうかを示すこともできます。たとえば、すべてのアクティビティを照合する場合は、型 System.ComponentModel.Activity を使用し、MatchDerivedTypes を True に設定します。これにより、基本アクティビティ クラスとこのクラスから派生したすべてのアクティビティ、つまりすべてを照合することになります。その後、この情報を ActivityTrackPoint の MatchingLocations コレクションに追加してこれらのイベントを追跡することを示すか、ExcludedLocations コレクションに追加してこのデータを収集しないことを示します。

さらに細かく制御する場合は、データを追跡するために照合すべき状態を、プロパティがある値に等しいかどうかという形式で指定できます。たとえば、SendEmail アクティビティの Priority プロパティが High の場合のみ、電子メール メッセージを追跡することを選択するとします。この場合の例を、図 3 に示します。

Figure 3 ActivityTrackPoint (Close Figure 3)
ActivityTrackPoint actTrack = new ActivityTrackPoint();

ActivityTrackingLocation actMatch = new ActivityTrackingLocation(
    typeof(SendEmail), false, new ActivityExecutionStatus[] {
        ActivityExecutionStatus.Closed });

ActivityTrackingCondition condition =
    new ActivityTrackingCondition("Priority", "High");
actMatch.Conditions.Add(condition);

actTrack.MatchingLocations.Add(actMatch);

profile.ActivityTrackPoints.Add(actTrack);

アクティビティやワークフローから、プロパティ値という形式でデータを抽出することもできます。たとえば、ワークフロー内の照合場所に到達したときの SendEmail アクティビティの To プロパティと Subject プロパティを追跡することができます。この処理は、ActivityDataTrackingExtract または WorkflowDataTrackingExtract のアイテムを ActivityTrackPoint の Extracts コレクションに追加することによって行います。各 ActivityDataTrackingExtract では、抽出するメンバが識別され、追跡サービスに送信されます。次に示すように、To プロパティと Subject プロパティを追跡するように、データの抽出をアクティビティ追跡ポイントに追加します。

actTrack.Extracts.Add(new ActivityDataTrackingExtract("To"));
actTrack.Extracts.Add(new ActivityDataTrackingExtract("Subject"));

追跡の最終ポイントを識別する UserTrackPoints は、3 種類の中で最も柔軟なイベントです。ワークフローからのデータを追跡する必要がある場合がありますが、データをアクティビティのプロパティとして使用することはできません。また、ActivityTrackingLocation での単純な状態サポート以上のロジックを使用することが必要な場合もあります。このような場合、ActivityExecutionContext クラスまたは Activity クラスの TrackData メソッドを呼び出すことができます。このメソッドを呼び出すと、データ アイテムのオプションのキーや名前を添えて、データを追跡インターセプタに送信することができます。追跡サービスでこのデータを受け取る場合は、正確な照合場所を指定して UserTrackPoint を定義する必要があります。また、ワークフローとアクティビティのデータ抽出を、UserTrackPoint が一致したときに、ワークフローまたは現在のアクティビティのプロパティが追跡されるように定義することもできます。

SQL 追跡サービス

Windows Workflow Foundation には、ワークフローの種類に応じた追跡プロファイルを定義して SQL Server に配置できる、SQL Server 追跡サービスがあります。その後、この追跡サービスは、配置したプロファイルを実行時に使用して、追跡インターセプタを構成します。SQL Server 追跡サービスには、プロファイルのバージョン管理、プロファイルの更新通知、データのパーティション分割など、いくつか強力な機能があります。

シリアル化された追跡プロファイルをワークフローの種類情報とバージョン番号と共に渡して、UpdateTrackingProfile ストアド プロシージャを呼び出すことで、追跡プロファイルが SQL 追跡サービスに配置されます。このストアド プロシージャを使用して、ワークフローの種類固有の追跡プロファイルを挿入できますが、プロファイルを更新するたびに、一意のバージョン番号を使用しなければなりません。前述の TrackingProfileSerializer によって、プロファイルの XML テキストを取得できるので、このテキストをストアド プロシージャに渡すことができます。

ワークフローの種類に応じた追跡プロファイルを指定しないと、SQL 追跡サービスは UseDefaultProfile の設定を使用して、追跡インターセプタに既定のプロファイルを提供するかどうかを判断します。既定のプロファイルでは、ワークフローとすべてのアクティビティの全状態イベントを追跡します。また、送信されたすべてのユーザー追跡イベントを追跡するための照合場所も含みます。この既定の動作を希望しない場合は、SQL 追跡サービスを構成する際に、UseDefaultProfile プロパティを False に設定する必要があります。これにより、プロファイルが指定されたワークフローの種類のみが追跡されます。

プロファイル情報が最新かどうかを確認するために、追跡インターセプタは、プロファイルを既に受け取ったかどうかを、各追跡サービスに定期的に問い合わせます。追跡プロファイルの読み込みや作成を繰り返さないようにするために、追跡サービスは IProfileNotification インターフェイスを実装できます。このインターフェイスでは、ProfileUpdated と ProfileRemoved の 2 つのイベントを定義します。追跡サービスがこのインターフェイスを実装すると、追跡インターセプタは、これら 2 つのイベントのいずれかが発生した場合を除いて、プロファイル情報が更新されたかどうかを照会しません。SQL Server 追跡サービスはこのモデルをサポートするために、定期的 (既定では 60 秒ごと) にデータベースに照会し、ワークフローの種類に応じた追跡プロファイルが更新または削除されたかどうかを確認します。ProfileChangeCheckInterval プロパティにポーリング間隔を表す値をミリ秒単位で設定することで、サービスが更新の有無を照会する頻度を構成できます。

運用環境では追跡データがすぐに累積される可能性があるため、SQL 追跡サービスではデータのパーティション分割がサポートされます。データを日、週、月、または年ごとに分割するように構成できます。つまり、サービスは期間ごとに追跡テーブルを作成します。さらに、データを (ダウンタイム用にスケジュールを設定できるように) 要求時、または (ワークフロー完了時に) 自動的に移動するかどうかを制御できます。この設定を構成するには、SQL Server 追跡サービスの PartitionOnCompletion プロパティを true に設定し、各ワークフローの完了時にワークフローを適切な追跡パーティションに移動することを示します。データを手動でパーティション分割するには、追跡データベースの PartitionCompletedWorkflowInstances ストアド プロシージャを呼び出します。

追跡データを照会する

SQL 追跡サービスを使用してデータを収集すると、データは SQL Server に格納されます。つまり、従来のレポート ツールを使用して、データを表示したり分析を実行したりできます。ただし、シナリオによっては、アプリケーション内からワークフローのデータを照会したい場合もあります。SQL クエリを記述してデータベース内の正しいリレーションシップを判断する以外に、Windows Workflow Foundation には、ワークフロー インスタンスの追跡情報を検索できる、追跡クエリ プログラミング インターフェイスがあります。情報はクラスとして返されます。このクラスを使用して、追跡されたすべてのイベントや追跡プロファイルで指定されたデータにアクセスできます。

SqlTrackingQuery クラスでは、ワークフローのインスタンス ID を使用して、特定のワークフロー インスタンスの追跡データを読み込むか、パラメータの一意のセットを使用して、ワークフローの一覧に照会できます。特定のインスタンスを読み込む場合は、TryGetWorkflow メソッドを使用します。このメソッドは、要求が正常に行われたかどうかを示すブール値を返します。複数のワークフローを照会するには、GetWorkflows メソッドを使用します。このメソッドには、SqlTrackingQueryOptions インスタンスが必要です。

クエリ オプションを使用して、追跡データベース内のワークフローを検索するときに使用するパラメータを定義できます。最も単純なオプションには、日付の最小値や最大値、ワークフローの種類や状態などがあります。たとえば、状態が Completed で、今日から 1 週間前までの間に行われたワークフロー中で特定の種類に一致するすべてのワークフローを検索するようなクエリを構成できます。その結果、ワークフローの種類に関する目的の情報とその遷移状態を取得できます。

エラー状態を対象とする追跡プロファイルについては既に説明しました。図 4 に示すコードを使用すると、終了したすべてのワークフローのインスタンスをデータベースに照会し、例外の詳細を出力できます。追跡情報には、追跡したイベントの中で Terminated 状態のイベントのイベント引数として、例外の詳細が含まれることに注意してください。

Figure 4 Terminated 状態のワークフロー インスタンスを照会する (Close Figure 4)
SqlTrackingQueryOptions options = new SqlTrackingQueryOptions();
options.WorkflowStatus = WorkflowStatus.Terminated;

SqlTrackingQuery query = new SqlTrackingQuery(connectionString);
IList<SqlTrackingWorkflowInstance> workflows =
    query.GetWorkflows(options);

foreach (SqlTrackingWorkflowInstance workflow in workflows)
{
    foreach(WorkflowTrackingRecord workflowEvent in
        workflow.WorkflowEvents)
    {
        TrackingWorkflowTerminatedEventArgs args =
            workflowEvent.EventArgs as
                TrackingWorkflowTerminatedEventArgs;
        if (args != null)
        {
            Console.WriteLine(
                "workflow {0} terminated with exception: {1}",
            workflow.WorkflowInstanceId, args.Exception.Message);
        }
    }
}

また、抽出した値に基づいて検索を行うこともできます。たとえば、SendEmail アクティビティの To アドレスを抽出するように構成した追跡プロファイルがあるとします。この場合、To アドレスが特定の従業員やパートナーなどの特定の値に一致するワークフロー インスタンスを返すクエリを記述できます。SqlTrackingQueryOptions の TrackingDataItemValue アイテムを使用して、抽出した値に基づいて特定のアイテムに絞り込んだ検索を行うコード例を次に示します。

SqlTrackingQueryOptions options = new SqlTrackingQueryOptions();
TrackingDataItemValue item = new TrackingDataItemValue(
    "sendEmail1", "To", "highpriority@example.com");
options.TrackingDataItems.Add(item);

SqlTrackingQuery query = new SqlTrackingQuery(connectionString);
IList<SqlTrackingWorkflowInstance> workflows =
    query.GetWorkflows(options);

レポート処理などのニーズを満たすために SQL Server データベースに直接照会することにした場合、テーブルを直接使用するのではなく、テーブル内に定義されたビューを使用してください。ビューの使用は一般的な推奨方法ですが、ここでは、作成されたすべての追跡パーティションからのデータがビューに含まれることになるため、特に重要です。ビューを使用することで、これらすべてのテーブルのデータを自分でまとめなければならない負担をなくします。

カスタム追跡サービス

SQL Server データベースに追跡データを格納することは重要なユース ケースで、おそらく最も一般的に使用されます。しかし、追跡情報を使用するシナリオはこれだけではありません。このような用法は一般的な要件であるため、マイクロソフトはこの充実したサービスを Windows Workflow Foundation に含めることにしました。ただし、このように非常に幅広く使用されるサービスは、できる限り汎用的でなければなりません。つまり、これが適切ではない状況もかなりあります。このような場合に、カスタム追跡サービスを作成できます。

ワークフロー ソリューションには、カスタム追跡サービスを使用して満たすことができる多くの要件があります。ここでは、独自の追跡サービスの作成に関する基本事項を説明し、一般的な要件を満たすカスタム サービスの例をいくつか示します。ここで重要な点として、追跡とはワークフローを見極めることであったことを思い出してください。必要な結果が得られるように、追跡プロファイルを使用して追跡サービスを構成する方法を理解したら、後はカスタム サービスでその追跡情報をどのように使用または表示するかを決定するだけです。

カスタム追跡サービスを作成するには、2 つの作業が必要です。まず、サービスの追跡プロファイルの作成方法を決定します。プロファイルが静的で、すべての種類のワークフロー間で変化しない場合もあります。このような場合は、プロファイルをコードで作成し、プロファイルが要求されたら単にそのコードをランタイムに返すことができます。ただし、追跡サービスで複数のプロファイルやプロファイルのバージョンをサポートする必要がある場合は、その情報を格納して追跡サービスから取得する方法が必要です。

次に、サービスのランタイムの動作を提供する、追跡サービスと追跡チャネルの 2 つのクラスを実装する必要があります。追跡サービス クラスは、ワークフロー ランタイム サービスとして機能します。また、追跡サービスと対話するための、ホスト アプリケーションとランタイムの直接インターフェイスになります。このクラスのインスタンスを作成し、コードまたは構成を使用して、他のサービスとまったく同様にワークフロー ランタイムに追加します。

追跡サービス クラスには、主に 2 つの機能があります。1 つは追跡プロファイルを追跡インターセプタに提供する機能で、もう 1 つはリスナがデータを書き込める追跡チャネルを提供する機能です。追跡チャネルは開発者が実装するクラスで、追跡データの受け取りと処理を担当します。このクラスでは、追跡データを SQL Server や Windows イベント ログなどのストアに書き込むことができます。また、操作に関する Windows Management Instrumentation (WMI) イベントなどのイベントを発生させることもできます。さらに、データをメモリ内に保持するために追跡サービスに送信して戻し、ホスト アプリケーションで使用できるようにすることもできます。実際には他のオプションもあり、データを使用して何を実行するかは、ニーズや要件によって異なります。カスタム追跡サービスは、使用する追跡プロファイルや、受け取ったデータを処理する方法によって区別されます。

追跡チャネルを実装する場合、Send と InstanceCompletedOrTerminated という 2 つの単純なメソッドから始めるのが最も簡単です。追跡サービスでは、ワークフロー インスタンスごとに追跡チャネルを提供することが求められます。一般的には、要求ごとに追跡チャネルの一意のインスタンスを提供します。通常は、TrackingParameters をパラメータとして受け取るコンストラクタを用意します。この情報は、他のメソッドの実装で必要になる場合があります。TrackingParameters クラスは、追跡しているワークフロー インスタンスに関する情報 (インスタンス ID など)、ルート アクティビティ定義へのアクセス、および親アクティビティとワークフローの呼び出し (このワークフローが別のワークフローから呼び出された場合) に関する情報を含みます。

InstanceCompletedOrTerminated メソッドを使用すると、追跡サービスはワークフロー完了時に通知を受け取ることができ、ワークフローの追跡に関連するすべてのリソースをクリーンアップできるようになります。InstanceCompletedOrTerminated メソッドではパラメータを受け取らないため、通常、TrackingParameters から提供されたワークフロー インスタンス ID にアクセスする必要があります。

追跡チャネルの Send メソッドは、追跡プロファイルで要求された追跡レコードごとに呼び出されます。プロファイルと同様に、追跡レコードにもワークフロー、アクティビティ、およびユーザーの 3 つのカテゴリがあり、すべて TrackingRecord 基本クラスから派生します。Send メソッドでは、送信された追跡レコードの種類を調べてから、それに応じて処理を行うのは開発者の役割です。たとえば、追跡プロファイルがユーザー追跡イベントを対象としないことがわかっている場合は、追跡チャネルでユーザー追跡レコードを処理する必要はありません。

追跡サービス クラス自体は、追跡プロファイルの管理を担当します。追跡サービスの基本クラスにはプロファイルを取得するためのいくつかのメソッドがあり、ワークフローの種類とワークフロー インスタンスに基づいてプロファイルを管理できます。つまり、サービスから、すべてのワークフローに同じプロファイルが返される場合もあれば、ワークフロー インスタンスごとに一意のプロファイルが返される場合もあります。

GetProfile メソッドには、2 つのオーバーロードがあります。1 つはインスタンス ID、もう 1 つは種類とプロファイルのバージョン番号を受け取るオーバーロードです。また、TryGetProfile は型情報を受け取ります。これらのすべての場合に、カスタム追跡サービスの仕事は、種類またはインスタンスに適した追跡プロファイルを返すことです。

また、TryReloadProfile というメソッドもあります。このメソッドはランタイムから呼び出され、更新されたプロファイルが存在する場合はそれを取得します。追跡インターセプタは、プロファイルが最新であることを確認する必要があるため、更新されたプロファイルがあるかどうかを追跡サービスに定期的に問い合わせます。この要求が行われるたびに新しいプロファイルを作成するか、プロファイルを探すのではなく、カスタム追跡サービスに IProfileNotification インターフェイスを実装できます。IProfileNotifcation インターフェイスには、ProfileRemoved と ProfileUpdated の 2 つのイベントがあります。追跡サービスにこのインターフェイスを実装すると、追跡インターセプタは更新をポーリングしなくなります。代わりに、このインターフェイスのイベントを使用して、すべての更新の通知を待機します。

このコラムのコード サンプルには、サンプルの追跡サービスをいくつか含めています。現状のコード サンプルでは、追跡を使用してステート マシン ワークフローの現在状態を取得し、ホスト アプリケーションで使用できるようにします。このサービスの追跡プロファイルには、State アクティビティの Executing 状態のアクティビティ追跡ポイントが含まれます。追跡チャネルはアクティビティ追跡レコードを受け取ったら、追跡サービス クラスの辞書を現在状態の名前で更新します。この例は、すべてのワークフローに同じプロファイルを使用し、ホスト アプリケーションからクエリできるように、データをサービスに書き込むサービスを示しています。

コード サンプルに含まれるもう 1 つのサンプル追跡サービスは、Windows Workflow Foundation で開発者が直面する一般的な問題を解決することを目標としています。ホストから分離され実行されるというワークフローの性質のため、実行中のワークフローへの参照を取得し、プロパティ値を抽出する手順は簡単ではありません。これは、特に ASP.NET のシナリオで一般的な要件です。ManualWorkflowScheduler によってワークフロー内の手順の実行が完了したら、ユーザー インターフェイスをこのワークフローからのプロパティ値で更新することが必要な場合があります。

ワークフロー内の情報は CallExternalMethod アクティビティを使用して渡すことができますが、これが常に望ましい解決方法であるとは言えず、大規模プロジェクトでは実行するのが困難な場合があります。ワークフロー プロパティ追跡サービスのサンプルでは、追跡を使用してこの問題を解決します。この追跡サービスでは、ワークフローのさまざまなプロパティを反映するために、返されるプロファイルはワークフローの種類ごとに異なります。Reflection を使用して、ワークフロー データの追跡抽出のコレクションを構築し、このコレクションをアクティビティ追跡ポイントに追加します。追跡の場所は、各アクティビティの Closed イベントです。したがって、各アクティビティが終了した後に、ワークフロー インスタンスからプロパティ値が抽出され、追跡チャネルに送信されます。その後、チャネルはサービス内の辞書を更新し、これらの値をホスト アプリケーションで使用できるようにします。

以上の 2 つはカスタム追跡サービスのかなり単純な例ですが、どちらも一般的な開発者シナリオに対応しており、既存の追跡インフラストラクチャを利用した簡単で再利用できる実装を提供します。構築する基盤がなかったら、これらの課題はそれぞれはるかに困難になり、さらに多くのカスタム コードが必要になり、ソリューションに一貫性がなくなっていたでしょう。

おわかりのように、Windows Workflow Foundation の追跡サービスは非常に柔軟で、多種多様なビジネス要件を解決するのに役立ちます。追跡はワークフローを目に見えるようにするために重要な部分であり、カスタム追跡サービスはアプリケーションでその可視性を高めるためのツールです。

ご意見やご質問は、

こちら mmnet30@microsoft.com.

まで英語でお送りください。
Matt Milner は、.NET、Web サービス、Windows Workflow Foundation、Windows Communication Foundation、BizTalk Server などのマイクロソフト テクノロジを専門とする独立系ソフトウェア コンサルタントで、PluralSight のインストラクタでもあります。夫人の Kristen と 2 人の息子とミネソタ州に居を構えています。Matt の連絡先は、pluralsight.com/blogs/matt にあるブログ (英語) です。


同時実行の問題: CLR の非同期プログラミング モデルを実装する

同時実行の問題
CLR の非同期プログラミング モデルを実装する
Jeffrey Richter

コードのダウンロード : : ConcurrentAffairs2007_03.exe (157KB)
コンテンツ
APM の中核となる IAsyncResult
APM を実装する
テストとパフォーマンス
まとめ

I/O 操作には、通常、速度が遅く予測できないという特徴があります。アプリケーションが同期 I/O 操作を実行すると、基本的には、そのアプリケーションは実際の処理を行っているデバイスを制御できません。たとえば、アプリケーションが StreamRead メソッドを呼び出して FileStream または NetworkStream から複数のバイト列を読み取るとすると、そのメソッドから戻るまでにどの程度の時間がかかるかはわかりません。読み取るファイルがローカル ハード ドライブ上にあれば、Read メソッドからほぼ瞬時に戻るかもしれません。ファイルが格納されているリモート サーバーがオフラインであれば、Read メソッドは、タイムアウトが発生して例外がスローされるまで数分間待機するかもしれません。いずれにせよ、その間、同期要求を行っているスレッドは拘束されます。このスレッドが UI スレッドであれば、アプリケーションはフリーズし、ユーザー入力への応答を停止します。

同期 I/O の完了を待機するスレッドがブロックされると、スレッドがアイドル状態でも、実際の処理を行うことはできません。多くのアプリケーション開発者は、スケーラビリティを向上するために、多くのスレッドを作成します。残念ながら、各スレッドでは、カーネル オブジェクト、ユーザー モードおよびカーネル モードのスタック、コンテキスト切り替えの増加、スレッドのアタッチまたはデタッチの通知を含む DllMain メソッドの呼び出しなど、無視できないオーバーヘッドが発生します。その結果、実際にはスケーラビリティは "低下" します。

ユーザーへの応答性を維持し、スケーラビリティとスループット、および信頼性を向上するには、アプリケーションでは同期 I/O 操作を実行すべきではありません。代わりに、アプリケーションでは、共通言語ランタイム (CLR) の非同期プログラミング モデル (APM) を使用して、非同期 I/O 操作を実行します。CLR APM の使用方法に関する記事は多くあり、私の著書『プログラミング Microsoft .NET Framework 第2版』(日経 BP ソフトプレス、2006) の第 23 章もその 1 つです。ただし、これまで APM を実装するメソッドを備えたクラスを定義する方法について説明している記事は見たことがありません。そこで、今回のコラムではこのトピックに注目します。

開発者が APM 自体を実装するのには、基本的に 4 つの理由があります。第 1 は、ハードウェア (ハード ディスクのファイル システム、ネットワーク、シリアル ポート、パラレル ポートなど) と直接通信するクラスをビルドしている場合です。既に説明したようにデバイス I/O は予測できないため、アプリケーションは、ハードウェアとの通信時にその応答性、スケーラビリティ、および信頼性を維持するように、非同期 I/O 操作を実行する必要があります。

さいわい、Microsoft® .NET Framework には、多数のハードウェア デバイスと通信するクラスが既に含まれています。したがって、Framework クラス ライブラリ (FCL) でサポートされていないハードウェア デバイス (パラレル ポートなど) と通信するクラスを定義する場合を除けば、APM を自身で実装する必要はありません。ただし、FCL でサポートされていても、特定のサブ機能についてはサポートされていないデバイスがあります。このような場合に I/O 操作を実行するには、APM の実装が必要になることがあります。たとえば、FCL は、アプリケーションがディスクと通信できるようにする FileStream クラスを提供しますが、FileStream では、便宜的ロック (microsoft.com/msj/0100/win32/win320100.aspx (英語))、スパース ファイル ストリーム (microsoft.com/msj/1198/ntfs/ntfs.aspx (英語)) など、NTFS ファイル システムによって提供される優れた機能にアクセスできません。これらの機能を公開する Win32® API を呼び出すために P/Invoke ラッパーを記述する場合、そのラッパーで APM をサポートし、操作を非同期に実行できるようにします。

第 2 は、ハードウェアと直接通信する定義済みのクラスの上に、抽象層をビルドしている場合です。この例は、既に .NET Framework にいくつか存在しています。たとえば、データを Web サービス メソッドに送信しているとします。Web サービス クライアント プロキシ クラスには引数を受け取るメソッドがあり、この引数は複雑なデータ構造体のセットになる可能性があります。内部的には、こうしたメソッドで、このような複雑なデータ構造体をバイト列の配列にシリアル化します。その後、(既に非同期 I/O を使用してハードウェアと通信する機能を備えた) NetworkStream クラスを使用して、シリアル化したバイト配列をネットワーク経由で送信するようにします。もう 1 つの例は、データベースへのアクセス時に発生します。ADO.NET SqlCommand 型は、BeginExecuteNonQuery メソッド、BeginExecuteReader メソッドなどの BeginXxx 形式のメソッドを提供します。これらのメソッドでは、引数を解析し、データをネットワーク経由でデータベースに送信します。I/O が完了すると、対応する EndExecuteNonQuery メソッド、EndExecuteReader メソッドなどの EndXxx 形式のメソッドが呼び出されます。内部的には、こうした EndXxx メソッドで結果データを解析し、豊富なデータを含むオブジェクトを呼び出し元に返します。

第 3 は、クラスで実行時間が長くなる可能性のあるメソッドを提供する場合です。このような場合、BeginXxx メソッドと EndXxx メソッドを用意して、呼び出し元で APM のメリットを利用できるようにします。メソッドは、前の例では I/O に関連する操作を実行しましたが、この例ではコンピュータ処理中心の操作を実行します。操作がコンピュータ処理中心なので、こうした作業にスレッドを使用しなければなりません。つまり、クラスを使用するユーザーの都合に合うように、BeginXxx メソッドと EndXxx メソッドを定義します。

最後は、同期 I/O を実行する Win32 メソッドをクラスでラップする場合です。I/O を実行する Win32 メソッドは数多くありますが、残念ながら、Win32 はこうした I/O 操作を非同期に実行する方法は提供しません。たとえば、Win32 のレジストリやイベント ログの機能は、ローカルまたはリモートのレジストリやイベント ログと通信する場合があります。マイクロソフトでは、スレッドがこれらの Win32 関数をブロックしないようにする非同期バージョンの関数を作成することもできました。しかし、現時点では、このような非同期バージョンの Win32 関数は存在しません。この種のメソッドをマネージ コードでラップするときは、常に BeginXxx メソッドと EndXxx メソッドを用意します。その結果、アプリケーションの効率が低い場合でもマネージ コードで適切な処理を行うことができます。アプリケーションの効率が低いのは、Windows® が同期 I/O 操作を実行している間、メソッドでスレッドをブロックする必要があるためです。ただし、マイクロソフトが Win32 にこれらのメソッドの非同期バージョンを追加することがあれば、新しいメソッドを活用するようにラッパーを変更できます。それにより、クライアント コードをまったく変更する必要なく効率を高めることができます。

APM の中核となる IAsyncResult

CLR APM の中核となるのは IAsyncResult インターフェイスです。このインターフェイスの定義を図 1 に示します。

Figure 1 IAsyncResult (Close Figure 1)
public interface IAsyncResult {
   WaitHandle AsyncWaitHandle { get; } // For Wait-Until-Done technique
   Boolean    IsCompleted     { get; } // For Polling technique
   Object     AsyncState      { get; } // For Callback technique
   Boolean    CompletedSynchronously { get; } // Almost never used
}

いずれかの BeginXxx メソッドが呼び出される際に、そのメソッドでは内部的にオブジェクトを構築する必要があります。このオブジェクトでは、IAsyncResult とその 4 つの読み取り専用プロパティを実装します。このオブジェクトは、開始された非同期操作の状態を識別します。BeginXxx メソッドからアプリケーション コードに戻ると、アプリケーションではこれらのプロパティにクエリして、操作が完了したかどうかを判断できます。このオブジェクトは、完了した操作の状態も保持します。操作が正常に完了した場合は操作結果の値を保持し、操作がエラーで完了した場合は例外を保持します。アプリケーションではこの IAsyncResult オブジェクトを EndXxx メソッドに渡し、操作が完了するまで待機します (まだ完了していない場合)。EndXxx メソッドは、結果を返すか例外をスローして、操作結果またはエラーを呼び出し元に通知します。

図 2 では、IAsyncResult インターフェイスを実装する AsyncResultNoResult クラスを定義します。この単純なクラスは、戻り値のない非同期操作に使用できます (具体的には、操作は成功または失敗します)。Stream の BeginWrite メソッドと EndWrite メソッドがこの例です。ストリームへの非同期書き込み操作を開始すると、その結果は成功か失敗のいずれかになります (EndWrite メソッドのプロトタイプ宣言では void を返すことになっています)。

Figure 2 AsyncResultNoResult クラス (Close Figure 2)
internal class AsyncResultNoResult : IAsyncResult
{
   // Fields set at construction which never change while
   // operation is pending
   private readonly AsyncCallback m_AsyncCallback;
   private readonly Object m_AsyncState;

   // Fields set at construction which do change after
   // operation completes
   private const Int32 c_StatePending = 0;
   private const Int32 c_StateCompletedSynchronously = 1;
   private const Int32 c_StateCompletedAsynchronously = 2;
   private Int32 m_CompletedState = c_StatePending;

   // Field that may or may not get set depending on usage
   private ManualResetEvent m_AsyncWaitHandle;

   // Fields set when operation completes
   private Exception m_exception;

   public AsyncResultNoResult(AsyncCallback asyncCallback, Object state)
   {
      m_AsyncCallback = asyncCallback;
      m_AsyncState = state;
   }

   public void SetAsCompleted(
      Exception exception, Boolean completedSynchronously)
   {
      // Passing null for exception means no error occurred.
      // This is the common case
      m_exception = exception;

      // The m_CompletedState field MUST be set prior calling the callback
      Int32 prevState = Interlocked.Exchange(ref m_CompletedState,
         completedSynchronously ? c_StateCompletedSynchronously :
         c_StateCompletedAsynchronously);
      if (prevState != c_StatePending)
         throw new InvalidOperationException(
             "You can set a result only once");

      // If the event exists, set it
      if (m_AsyncWaitHandle != null) m_AsyncWaitHandle.Set();

      // If a callback method was set, call it
      if (m_AsyncCallback != null) m_AsyncCallback(this);
   }

   public void EndInvoke()
   {
      // This method assumes that only 1 thread calls EndInvoke
      // for this object
      if (!IsCompleted)
      {
         // If the operation isn't done, wait for it
         AsyncWaitHandle.WaitOne();
         AsyncWaitHandle.Close();
         m_AsyncWaitHandle = null;  // Allow early GC
      }

      // Operation is done: if an exception occured, throw it
      if (m_exception != null) throw m_exception;
   }

   #region Implementation of IAsyncResult
   public Object AsyncState { get { return m_AsyncState; } }

   public Boolean CompletedSynchronously {
      get { return Thread.VolatileRead(ref m_CompletedState) ==
                c_StateCompletedSynchronously; }
   }

   public WaitHandle AsyncWaitHandle
   {
      get
      {
         if (m_AsyncWaitHandle == null)
         {
            Boolean done = IsCompleted;
            ManualResetEvent mre = new ManualResetEvent(done);
            if (Interlocked.CompareExchange(ref m_AsyncWaitHandle,
               mre, null) != null)
            {
               // Another thread created this object's event; dispose
               // the event we just created
               mre.Close();
            }
            else
            {
               if (!done && IsCompleted)
               {
                  // If the operation wasn't done when we created
                  // the event but now it is done, set the event
                  m_AsyncWaitHandle.Set();
               }
            }
         }
         return m_AsyncWaitHandle;
      }
   }

   public Boolean IsCompleted {
      get { return Thread.VolatileRead(ref m_CompletedState) !=
                c_StatePending; }
   }
   #endregion
}

ご覧のように、AsyncResultNoResult クラスには、AsyncCallback 引数と Object 引数を受け取るコンストラクタがあり、このコンストラクタを使用してすべての非同期操作を開始します。コンストラクタでは、これらの引数をプライベート フィールドに保存するだけです。IAsyncResult の AsyncState プロパティは、Object フィールドを呼び出し元に返します。クラスでは、IAsyncResult の IsCompleted プロパティと CompletedSynchronously プロパティの実装に使用する m_CompletedState フィールドを定義します。また、IAsyncResult の AsyncWaitHandle プロパティを実装するための m_AsyncWaitHandle フィールドも定義します。最後に、m_exception フィールドを定義します。このフィールドは、操作完了時に設定されます。操作が正常に完了すると、フィールドに null (初期値と同じ) が設定されます。操作が失敗すると、フィールドに Exception から派生したオブジェクトが設定されます。このオブジェクトは失敗の原因を示します。

AsyncResultNoResult クラスを分析すると、m_AsyncWaitHandle フィールドに関するセクションを除いて、コード全体が非常に単純であることがわかります。このフィールドは ManualResetEvent オブジェクトへの参照で、非同期操作を開始するコードが AsyncWaitHandle プロパティにクエリするか、操作の実行が実際に完了する前にコードから EndInvoke メソッドを呼び出す場合にのみ必要です。最も一般的で推奨される APM の使用方法は、操作の完了時に自動的に呼び出される AsyncCallback メソッドを指定することです。この一般的な使用方法では、ManualResetEvent オブジェクトが必要ありません。そのため、AsyncResultNoResult オブジェクトを使用するコードが絶対的に必要としない限り、このオブジェクトの作成および使用を行わないようにしてきました。

このようにする理由は、カーネル オブジェクト (ManualResetEvent など) の作成と使用が、比較的負荷の高い作業であるためです。カーネル オブジェクトを使用する際のパフォーマンスに関する詳細については、2005 年 10 月の「同時実行の問題 (Concurrent Affairs) コラム」 (msdn.microsoft.com/msdnmag/issues/05/10/ConcurrentAffairs (英語)) を参照してください。

非同期操作の完了時に、コードの一部で AsyncResultNoResult の SetAsCompleted メソッドを呼び出す必要があります。このメソッドには、操作が正常に完了した場合には null を、操作が失敗した場合には Exception から派生したオブジェクトへの参照を渡します。コードでは、操作が同期と取って完了したか (ほとんどない)、非同期に完了したか (ほぼ常に) も示します。この情報は IAsyncResult の CompletedSynchronously プロパティから返されますが、ほとんどのアプリケーションでは実際には重要ではありません。

内部的には、SetAsCompleted は、例外を m_exception フィールドに保存し、m_completedSynchronously フィールドの状態を変更します。次に、手動リセット イベント オブジェクトが作成された場合は、それを設定します。最後に、AsyncResultNoResult オブジェクトの構築時に AsyncCallback メソッドが指定されていた場合は、このメソッドがコールバックされ、アプリケーション コードに非同期操作の完了を通知することで結果 (またはエラー) を処理できるようにします。

アプリケーション コードでは操作の結果を取得するため、いずれかの EndXxx メソッドを呼び出し、次にそのメソッドが AsyncResultNoResult の EndInvoke メソッドを呼び出して操作が成功したかどうかを判断します。操作の完了前に EndInvoke が呼び出されると、EndInvoke は次に手動リセット イベントを使用して、操作が完了するまで呼び出しスレッドを中断します。操作が完了すると、EndInvoke が返されるか、前述の SetAsCompleted が呼び出されたときに保存された例外がスローされます。

多くの非同期操作には戻り値があるので、これをサポートするクラスとして AsyncResult<TResult> (図 3 参照) も定義しました。このジェネリック クラスは AsyncResultNoResult から派生したクラスで、実際には、TResult 型の戻り値に対するサポートを追加するだけです。このサポートは、結果を保持するプライベート フィールド (m_result)、TResult 値を受け取る SetAsCompleted メソッドのオーバーロード、および新しい EndInvoke メソッドという形式で提供されます。新しい EndInvoke メソッドは操作の完了を待機して、操作が成功した場合には結果を返し、操作が失敗した場合には例外をスローします。

Figure 3 戻り値を返す AsyncResult (Close Figure 3)
internal class AsyncResult<TResult> : AsyncResultNoResult
{
   // Field set when operation completes
   private TResult m_result = default(TResult);

   public AsyncResult(AsyncCallback asyncCallback, Object state) :
      base(asyncCallback, state) { }

   public void SetAsCompleted(TResult result,
      Boolean completedSynchronously)
   {
      // Save the asynchronous operation's result
      m_result = result;

      // Tell the base class that the operation completed
      // sucessfully (no exception)
      base.SetAsCompleted(null, completedSynchronously);
   }

   new public TResult EndInvoke()
   {
      base.EndInvoke(); // Wait until operation has completed
      return m_result;  // Return the result (if above didn't throw)
   }
}

また、多くの BeginXxx メソッドは、AsyncCallback と Object の他にも引数を受け取ります。たとえば、Socket クラスには、IPAddress (アドレス) 引数と Int32 (ポート) 引数を受け取る BeginAccept メソッドがあります。追加の引数を受け取る BeginXxx メソッドを備えた AsyncResultNoResult クラスまたは AsyncResult<TResult> クラスを使用する場合は、これら 2 つの基本クラスのいずれか (EndXxx メソッドが void を返すかどうかによる) から派生する独自の型を定義します。クラスでは、追加のフィールドを定義し (各引数につき 1 つ)、クラスのコンストラクタでそのフィールドを設定します。その後、実際の処理を行うメソッドでは、適切なタイミングで、クラスのフィールドからこれらの引数の値を抽出できます。

APM を実装する

IAsyncResult インターフェイスを実装する型の定義方法について説明したので、次に AsyncResult<TResult> クラスと AsyncResultNoResult クラスの使用方法について説明します。このため、実行時間の長い同期 DoTask メソッドを提供し、操作完了時を示す DateTime インスタンスを返す、LongTask クラスを定義しました (図 4 参照)。

Figure 4 LongTask が非同期 I/O のシミュレーションを行う (Close Figure 4)
internal sealed class LongTask
{
   private Int32 m_ms;  // Milliseconds;

   public LongTask(Int32 seconds)
   {
      m_ms = seconds * 1000;
   }

   // Synchronous version of time-consuming method
   public DateTime DoTask()
   {
      Thread.Sleep(m_ms);  // Simulate time-consuming task
      return DateTime.Now; // Indicate when task completed
   }

   // Asynchronous version of time-consuming method (Begin part)
   public IAsyncResult BeginDoTask(AsyncCallback callback, Object state)
   {
      // Create IAsyncResult object identifying the
      // asynchronous operation
      AsyncResult<DateTime> ar = new AsyncResult<DateTime>(
         callback, state);

      // Use a thread pool thread to perform the operation
      ThreadPool.QueueUserWorkItem(DoTaskHelper, ar);

      return ar;  // Return the IAsyncResult to the caller
   }

   // Asynchronous version of time-consuming method (End part)
   public DateTime EndDoTask(IAsyncResult asyncResult)
   {
      // We know that the IAsyncResult is really an
      // AsyncResult<DateTime> object
      AsyncResult<DateTime> ar = (AsyncResult<DateTime>)asyncResult;

      // Wait for operation to complete, then return result or
      // throw exception
      return ar.EndInvoke();
   }

   // Asynchronous version of time-consuming method (private part
   // to set completion result/exception)
   private void DoTaskHelper(Object asyncResult)
   {
      // We know that it's really an AsyncResult<DateTime> object
      AsyncResult<DateTime> ar = (AsyncResult<DateTime>)asyncResult;
      try
      {
         // Perform the operation; if sucessful set the result
         DateTime dt = DoTask();
         ar.SetAsCompleted(dt, false);
      }
      catch (Exception e)
      {
         // If operation fails, set the exception
         ar.SetAsCompleted(e, false);
      }
   }
}

便宜上、CLR APM の説明に使用する BeginDoTask メソッドと EndDoTask メソッドも用意し、ユーザーが DoTask メソッドを非同期に実行できるようにします。ユーザーが BeginDoTask メソッドを呼び出すときに、AsyncResult<DateTime> オブジェクトを構築します。次に、スレッド プールのスレッドで、小さなヘルパ メソッドの DoTaskHelper を呼び出します。このヘルパ メソッドは、同期 DoTask メソッドへの呼び出しをラップします。

DoTaskHelper メソッドは、単純に、try ブロック内で同期バージョンのメソッドを呼び出します。DoTask メソッドが失敗 (例外をスロー) することなく完了したら、SetAsCompleted を呼び出して操作の戻り値を設定します。DoTask メソッドが例外をスローした場合、DoTaskHelper の catch ブロックでその例外をキャッチし、Exception から派生したオブジェクトへの参照を渡して SetAsCompleted を呼び出すことで操作が完了したことを通知します。

アプリケーション コードでは、LongTask の EndDoTask メソッドを呼び出し、操作の結果を取得します。すべての EndXxx メソッドには IAsyncResult が渡されます。内部的には、EndDoTask メソッドで、メソッドに渡された IAsyncResult オブジェクトが AsyncResult<DateTime> オブジェクトであることを認識し、それをキャストして EndInvoke を呼び出します。前述のように、AsyncResult<TResult> の EndInvoke メソッドは、操作が完了するまで待機し (必要な場合)、結果を返すか例外をスローして、非同期操作の結果を呼び出し元に通知します。

テストとパフォーマンス

FunctionalTest メソッド (図 5 参照) は、APM の実装を使用するコードの一部を示します。このメソッドでは、APM によってもたらされる 3 つの技法群、完了の待機、ポーリング、およびコールバック メソッドをテストします。コードを調べると、これまでにも目にしたことのある APM の他の使用方法と同じであることがわかります。もちろん、これが課題全体のポイントです。

Figure 5 LongTask を使用する (Close Figure 5)
private static void FunctionalTest()
{
  IAsyncResult ar;
  LongTask lt = new LongTask(5);

  // Prove that the Wait-until-done technique works
  ar = lt.BeginDoTask(null, null);
  Console.WriteLine("Task completed at: {0}", lt.EndDoTask(ar));

  // Prove that the Polling technique works
  ar = lt.BeginDoTask(null, null);
  while (!ar.IsCompleted)
  {
     Console.WriteLine("Not completed yet.");
     Thread.Sleep(1000);
  }
  Console.WriteLine("Task completed at: {0}", lt.EndDoTask(ar));

  // Prove that the Callback technique works
  lt.BeginDoTask(TaskCompleted, lt);
  Console.ReadLine();
}

private static void TaskCompleted(IAsyncResult ar)
{
  LongTask lt = (LongTask)ar.AsyncState;
  Console.WriteLine("Task completed at: {0}", lt.EndDoTask(ar));
  Console.WriteLine("All done, hit Enter to exit app.");
}

PerformanceTest メソッド (図 6 参照) は、IAsyncResult の実装のパフォーマンスと、デリゲートの BeginInvoke メソッドと EndInvoke メソッドの使用時に CLR によって提供される実装のパフォーマンスを比較します。IAsyncResult オブジェクトが作成されるたびに、アプリケーションで必要かどうかにかかわらず ManualResetEvent が常に作成されるため、IAsyncResult の実装の方が、FCL の現在の実装よりも高いパフォーマンスを達成します。

Figure 6 IAsyncResult のパフォーマンスをテストする (Close Figure 6)
private const Int32 c_iterations = 100 * 1000; // 100 thousand
private static Int32 s_numDone;
private delegate DateTime DoTaskDelegate();

private static void PerformanceTest()
{
   AutoResetEvent are = new AutoResetEvent(false);
   LongTask lt = new LongTask(0);

   Stopwatch sw;

   s_numDone = 0;
   sw = Stopwatch.StartNew();
   for (Int32 n = 0; n < c_iterations; n++)
   {
      lt.BeginDoTask(delegate(IAsyncResult ar)
      {
         if (Interlocked.Increment(ref s_numDone) == c_iterations)
            are.Set();
      }, null);
   }
   are.WaitOne();
   Console.WriteLine("AsyncResult Time: {0}", sw.Elapsed);
   s_numDone = 0;
   DoTaskDelegate doTaskDelegate = lt.DoTask;

   sw = Stopwatch.StartNew();
   for (Int32 n = 0; n < c_iterations; n++)
   {
      doTaskDelegate.BeginInvoke(delegate(IAsyncResult ar)
      {
         if (Interlocked.Increment(ref s_numDone) == c_iterations)
            are.Set();
      }, null);
   }
   are.WaitOne();
   Console.WriteLine("Delegate    Time: {0}", sw.Elapsed);
}

まとめ

APM のようなメカニズムを使用するときに、CLR 内部で何が起きているのかを理解するのは興味深いことです。ここに紹介した実装を調べれば、IAsyncResult オブジェクトのサイズ、その状態、およびその状態の管理方法を理解できます。これらを理解することで、自身のアプリケーションをより優れた方法で設計し、パフォーマンスを向上させることができます。

今回のコラムでは、スレッド プールのスレッドを使用して、コンピュータ処理中心の作業を実行する IAsyncResult の実装を使用しました。今後のコラムでは、I/O 関連の操作で IAsyncResult の実装を使用する方法について説明します。

ご意見やご質問は

Jeffrey mmsync@microsoft.com.

までお送りください。
Jeffrey Richter は、アーキテクチャのレビュー、トレーニング、およびコンサルティング サービスを提供する Wintellect 社 (www.Wintellect.com) の共同設立者です。彼の著書には『プログラミング Microsoft .NET Framework 第2版』(日経 BP ソフトプレス、2006) などがあります。Jeffrey は、MSDN Magazine の寄稿編集者で、1990 年からマイクロソフトのコンサルティングを担当しています。


{End Bracket}: Virtual Earth 3D コントロールを開発する

{End Bracket}
Virtual Earth 3D コントロールを開発する
Duncan Lawler

Virtual Earth 3D コントロールは、世界を 3 D 表示するためのツールとしてマイクロソフトが新しくリリースしたものです。Virtual Earth™ 3D は、スタンドアロン アプリケーションではなく、マネージ コントロールとして実装されます。つまり、このコントロールは Web サイトやスタンドアロン アプリケーションとシームレスに統合できます。Microsoft® .NET Framework のセキュリティ モデルにより、動的にプラグインを読み込んでデータを提供できるようになりましたが、部分的に信頼された環境では実行できません。

Virtual Earth 3D チームのプロジェクトで掲げた主要目的の 1 つには、開発者が独自のアプリケーションを構築できるようにするということがありました。ベータ版は Virtual Earth SDK で提供されている Jscript® API とシームレスに統合します。この SDK を使用して地図機能を Web サイトに統合すると、Web 開発者は、追加の作業を行うことなく、3D 表示を提供できるようになります。Web 開発に携わっていない開発者向けの SDK ドキュメントは現在準備中です。このドキュメントでは、独自の Windows® フォーム アプリケーションやネイティブ アプリケーションに直接コントロールを埋め込む方法を紹介します。また、独自のプラグイン拡張機能を記述および使用して、コントロールのモデルを変更したり、状況に応じて独自のデータを追加したりする方法のドキュメントとサンプルも提供する予定です。

Virtual Earth 3D コントロールを作成するにあたって、チームでは、12 インチよりも鮮明な解像度の航空写真に基づいて実際の建物を自動的に 3D テクスチャ モデルとして生成する手法を開発しました。このコントロールを使用すると、アプリケーション開発者は、臨場感のある詳細なレベルで実在する場所を表示する地図アプリケーションまたは視覚化アプリケーションを作成できます。

図 1 (画像を縮小するには、ここをクリックします)

図 1 (画像を拡大するには、ここをクリックします)

お察しのとおり、この解像度で世界を視覚化するには膨大な量のデータが必要になります。そのため、すべてのデータをユーザー PC のローカルにホストするのが不可能であることは、すぐに判明しました。データは大規模なサーバーにホストし、必要に応じてクライアントに転送する必要がありました。それでも、ファイルのサイズを縮小する必要がありました。まず、地形を描画する標準的な手法を使用して、画面の前面では詳細なデータを表示し、カメラから遠ざかるにつれて画質が粗くなる表面メッシュを作成しました。1 つの画面で表示されるデータ量には制限があるため、建物を精巧に再現するのは困難を極めました (Virtual Earth の自動処理によって生成される各表面テクスチャは一意です)。

パフォーマンスを向上させるために、まず、モデルの作成に関して複数の詳細レベルを作成しました。ソース データは、詳細な形状モデルと各建物の外観のテクスチャ マップを保持しています。まず、建物のソース テクスチャから単一テクスチャの地図を作成しました。次に、付近にある建物を取り込み、すべての建物と関連付けられている単一のテクスチャの地図を含む単一の合成メッシュを作成しました。縮小表示を使用した場合、小さな建物は表示されないので、小さな建物は削除しました。この処理を隣接する合成メッシュに対して繰り返し行い、より大きな合成メッシュを作り出しました。その結果、フレームが表示されたときには、詳細な合成メッシュではなく、縮小表示された合成メッシュが画面の背景に表示されるようになりました。テクスチャとモデルを折りたたむことで、Virtual Earth では、一連の呼び出しで 3D 画像を表示し、大幅なパフォーマンスの向上を実現しました。

次の問題は、このデータを転送するのに必要な帯域幅でした。指定のテクスチャ サイズで最適な画質を実現するために、Windows Imaging Components の新しい画像形式を使用して、転送のためにテクスチャを圧縮しました。

ユーザーは複数の都市にアクセスすることが多いため、コントロールでは、継続的に現在のビューを監視し、ユーザーが次にアクセスする都市を推測します。コントロールは非同期に現在表示されているか、近々表示される領域に対する優先度ベースの要求を発行します。また、コントロールでは、接続速度に基づいて要求する情報の詳細レベルを下げて、対話的なユーザー操作を実現できるようにしています。

多くのユーザーは自分が住んでいる都市を最も頻繁に表示します。そのため、キャッシュ機能を使用すると、2 回目以降のアクセスについては、パフォーマンスを効率的に向上させることができます。データがダウンロードされると、Virtual Earth では、システム メモリにテクスチャを格納し、テクスチャを画面上に表示します。ハードウェアでサポートされている場合、テクスチャは、ビデオ メモリで圧縮形式に変換されます。ただし、この変換処理では CPU を集中的に使用するため、チームではバックグランド スレッドで CPU の集中使用を抑制し、フレーム レートの低下を避ける必要がありました。

Virtual Earth の詳細情報と SDK については、Virtual Earth フォーラム (http://dev.live.com、英語) を参照してください。

Duncan Lawler は、Virtual Earth 3D チームの開発マネージャです。Duncan はマイクロソフトに 10 年間務めていて、その間ずっと Virtual Earth チームで MapPoint、Atlas、Streets & Trips、MSN Maps、および Pocket Streets の開発に携わってきました。連絡先は duncal@microsoft.com です。