February 2010

Volume 25 Number 02

Managed Extensibility Framework - Managed Extensibility Framework による .NET 4 で構成可能なアプリケーションの構築

Glenn Block | February 2010

まもなくリリースされる Microsoft .NET Framework 4 では、新たにアプリケーションの開発が大幅に容易になる魅力的なテクノロジが登場します。アプリケーションを設計する際にメンテナンスや機能拡張が容易になるようにと頭を悩ませたことのある方は、ぜひお読みください。

Managed Extensibility Framework (MEF) は .NET Framework 4 および Silverlight 4 に付属する新しいライブラリで、システムを配置後にそのシステムをサードパーティ製品で拡張できる、構成可能なシステムの設計を簡単にします。MEF を使用すると、アプリケーション開発者やフレームワーク作成者の手で、またはサードパーティ製のエクステンダーを使用して、新しい機能を段階的に導入できるため、アプリケーションの可能性が広がります。

開発の背景

数年前、マイクロソフト社内の多くのグループが、動的に検出、再利用、および構成できる再利用可能なコンポーネントからアプリケーションを組み立てるという課題に取り組んでいました。

  • Visual Studio 2010 では、拡張可能なコード エディターが新しく開発されました。このエディターでは、中核機能だけでなく、サードパーティ製機能もすべて、実行時に検出されるバイナリとして配置されます。このエディタの主要要件 の 1 つが、起動時間を短縮し、メモリ使用量を削減するために、拡張機能の遅延読み込みをサポートすることでした。
  • "Oslo" では、Intellipad という、MEF を操作するための新しい拡張可能なテキスト エディターが導入されました。Intellipad のプラグインは IronPython で作成されます。
  • Acropolis では、複合アプリケーション構築用フレームワークが提供されました。Acropolis のランタイムは、実行時にアプリケーション コンポーネントの "パーツ"を検出し、これらのパーツを疎結合したサービスを提供します。Acropolis でのコンポーネント作成の中心となるのは XAML です。

このような課題は、マイクロソフトに限られたものではありませんでした。多くのユーザーが、長年に渡って独自の機能拡張ソリューションを実装しています。したがって、今がまさに、プラットフォームを一歩進めて、マイクロソフトとユーザーの双方に役立つ汎用性の高いソリューションを提供するタイミングであると言えます。

新しいソリューションは必要だったのか

MEF は、この課題に対する最初のソリューションではありません。これまでにも、多数のソリューションが提案されています。プラットフォームの境界を超える活動はたくさんあり、EJB、CORBA、OSGi が実装する Eclipse、Spring といった Java プラットフォームを中心とした活動もあります。マイクロソフトのプラットフォームでも、コンポーネント モデルと System.Addin が .NET Framework 自体に含まれています。また、SharpDevelop の SODA アーキテクチャや、制御の反転コンテナー (Castle Windsor、StructureMap、patterns & practices の Unity など) を含め、オープン ソースのソリューションもいくつかあります。

このような既存のソリューションがあっても新しいソリューションを開発するのは、現状のソリューションはいずれも、サードパーティによる汎用性のある機能拡張には適していないと考えたためです。現状のソリューションは、複雑すぎて汎用性がないか、ホストの開発者にも拡張機能の開発者にも求められる負担が大きすぎます。MEF は、こうしたソリューションから学んだことをすべて集大成し、問題点に対処しようとする試みです。

では、MEF の中核となる概念について説明しましょう (図 1 参照)。

Managed Extensibility Framework の中核となる概念

図 1 Managed Extensibility Framework の中核となる概念

概念

MEF の中心となるのは、次の基本概念です。

構成可能なパーツ (以下、単に "パーツ" と呼びます): パーツは、他のパーツにサービスを提供し、他のパーツから提供されるサービスを利用します。MEF のパーツは、アプリケーション内外の任意の場所から取得できます。MEF の考え方では、取得する場所によるパーツの違いはありません。

エクスポート: エクスポートとは、パーツから提供するサービスです。パーツがエクスポートを提供する場合、パーツから "エクスポートする" と表現します。たとえば、パーツからロガーをエクスポートすることも、Visual Studio であればディター拡張機能をエクスポートすることもできます。パーツで複数のエクスポートを提供することもできますが、大半のパーツは単一のエクスポートを提供します。

インポート: インポートは、パーツが利用するサービスです。パーツでインポートを使用する場合、パーツに "インポートする" と表現します。パーツでは、ロガーなどのサービスを 1 つインポートすることも、エディター拡張機能など複数のサービスをインポートすることもできます。

コントラクト: コントラクトは、エクスポートまたはインポートの識別子です。エクスポート側のパーツでは、パーツが提供する文字列形式のコントラクトを指定し、インポート側のパーツでは、そのパーツに必要なコントラクトを指定します。MEF では、コントラクト名がエクスポート対象やインポート対象の型から派生されるため、ほとんどの場合はコントラクトを考慮する必要がありません。

構成: 各パーツは MEF によって組み立てられます。つまり、インスタンスが作成され、インポート側のパーツとエクスポート側のパーツが調整されます。

プログラミング モデル: MEF の外見

開発者は、プログラミング モデルを通じて MEF を使用します。プログラミング モデルは、コンポーネントを MEF パーツとして宣言する手段を提供します。MEF では、既定で属性プログラミング モデルが提供されます。このコラムでは、この属性プログラミング モデルを中心に説明します。このモデルは MEF で利用できる多数のプログラミング モデルの 1 つにすぎません。MEF のコア API は、属性にまったく依存しません。

属性プログラミング モデルについて

属性プログラミング モデルでは、パーツ (別名、属性付きパーツ) を、System.ComponentModel.Composition 名前空間にある一連の .NET 属性を使って定義します。ここからは、このモデルを使用した Windows Presentation Foundation (WPF) の拡張可能な販売注文管理アプリケーションの構築方法を説明します。このアプリケーションでは、ユーザーが bin フォルダーにバイナリを配置するだけで、そのユーザーの環境にカスタマイズされた新しいビューを追加できます。ここでは、MEF を使用してこれを実装する方法について説明します。また、その過程で設計を段階的に向上しながら、MEF の機能や属性プログラミング モデルの効果について詳しく説明します。

クラスをエクスポートする

この注文管理アプリケーションでは、新しいビューをプラグインとして追加できます。MEF に何かをエクスポートするには、次のように Export 属性を使用します。

[Export]
public partial class SalesOrderView : UserControl
{
public SalesOrderView()
  {
InitializeComponent();
  }
}

上記のパーツは、SalesOrderView コントラクトをエクスポートします。既定では、Export 属性にはコントラクトとしてメンバー (このコードではクラス) の具象型が使用されます。また、パラメーターを属性のコンストラクターに渡して、明示的にコントラクトを指定することもできます。

プロパティとフィールド経由でインポートする

属性付きパーツでは、プロパティやフィールドに Import 属性を使用して、そのパーツに必要なサービスを表現できます。サンプル アプリケーションは、他のパーツからビューへのアクセスに使用できる ViewFactory パーツをエクスポートします。この ViewFactory では、プロパティのインポートを使用して SalesOrderView をインポートします。プロパティをインポートするには、次のようにプロパティを Import 属性で修飾するだけです。

[Export]
public class ViewFactory
{
  [Import]
  public SalesOrderView OrderView { get; set; }
}

コンストラクター経由でインポートする

パーツでは、次のように ImportingConstructor 属性を使用して、コンストラクター (一般的には、コンストラクターによる挿入) を使用してインポートすることもできます。インポート用コンストラクターを使用すると、MEF はすべてのパラメーターをインポートと想定するため、Import 属性が必要ありません。

[Export]
public class ViewFactory
{
  [ImportingConstructor]
  public ViewFactory(SalesOrderView salesOrderView)
{
}
}

一般に、プロパティとコンストラクターのどちらをインポートに使用するかは好みの問題ですが、場合によってはプロパティ経由のインポートの使用が適していることがあります。特に、今回の WPF アプリケーションの例のように、MEF によってインスタンスが作成されないパーツがある場合は、プロパティが適しています。コンストラクターのパラメーターでは、再構成もサポートされません。

構成

SalesOrderView と ViewFactory を用意したら、構成を開始できるようになります。MEF パーツの検出や作成は自動的には行われません。このためには、構成を実行するブートストラップ コードを記述する必要があります。ブートストラップ コードの記述先としてよく利用されるのは、アプリケーションのエントリ ポイント、この例では App クラスです。

MEF のブートストラップを行うには、次のように数手順必要です。

  • コンテナーでの作成が必要なコントラクトのインポートを追加する
  • MEF がパーツの検出に使用するカタログを作成する
  • パーツのインスタンスを組み立てるコンテナーを作成する
  • インポートを含むインスタンスを渡してコンテナーの Composeparts メソッドを呼び出し、パーツを組み立てる

ご覧のように、この例では、まず ViewFactory インポートを App クラスに追加します。次に、bin フォルダーを参照する DirectoryCatalog を作成し、このカタログを使用するコンテナーを作成します。最後に Composeparts メソッドを呼び出し、App クラスのインスタンスを組み立てて、ViewFactory インポートを完成します。

public partial class App : Application
{
  [Import]
public ViewFactory ViewFactory { get; set; }

public App()
  {
this.Startup += new StartupEventHandler(App_Startup);
  }

void App_Startup(object sender, StartupEventArgs e)
  {
var catalog = new DirectoryCatalog(@".\");
var container = new CompositionContainer(catalog);
container.Composeparts(this);
  }
}

コンテナーは、構成中に ViewFactory を作成し、そのパーツの SalesOrderView インポートを設定します。その結果、SalesOrderView パーツが作成されます。最後に、Application クラスの ViewFactory インポートが完成します。このようにして、オブジェクト グラフ全体を組み立てる命令コードを手作業で記述するのではなく、MEF によって宣言型の情報に基づきオブジェクト グラフが組み立てられます。

MEF 非対応の項目をプロパティ経由で MEF にエクスポートする

MEF を既存のアプリケーションや他のフレームワークと統合するときに、インポート側のパーツで、関連クラスの MEF 非対応インスタンス (パーツではないインスタンス) を使用したい場合がよくあります。たとえば、System.String などのシールされたフレームワーク型、Application.Current などのアプリケーション単位のシングルトン、Log4Net から取得したロガー インスタンスなどのファクトリから取得したインスタンスなどが考えられます。

これをサポートするため、MEF ではプロパティをエクスポートできます。プロパティのエクスポートを使用するには、Export 属性で修飾したプロパティを設定した仲介パーツを作成します。このパーツのプロパティは事実上はファクトリで、MEF 非対応の値を取得するのに必要なあらゆるカスタム ロジックを実行します。以下のサンプル コードは、App などの他のパーツから静的なアクセサー メソッドを使用してロガーにアクセスするのではなく、Loggerpart で Log4Net ロガーをエクスポートし、他のパーツでインポートできるようにする方法を示しています。

public class Loggerpart
{
  [Export]
public ILog Logger
  {
get { return LogManager.GetLogger("Logger"); }
  }
}

プロパティのエクスポートはスイスのアーミー ナイフのように豊富な機能を備えているため、MEF と他のフレームワークがうまく連携できます。MEF を既存のアプリケーションに統合したり、レガシ システムにアクセスしたりする際に、プロパティのエクスポートは非常に有効です。

インターフェイスを使って実装を分離する

先ほどの SalesOrderView のサンプルでは、ViewFactory と SalesOrderView の間に密結合された関係が形成されます。ファクトリでは具象型の SalesOrderView を想定するため、拡張オプションやファクトリ自体のテスト容易性が制限されます。MEF では、次のようにインターフェイスをコントラクトとして使用することで、エクスポート側のパーツの実装とインポートとを分離できます。

public interface ISalesOrderView{}

[Export(typeof(ISalesOrderView))]
public partial class SalesOrderView : UserControl, ISalesOrderView
{
   ...
}

[Export]
public class ViewFactory
{
  [Import]
ISalesOrderView OrderView{ get; set; }
}

上記のコードでは、ISalesOrderView を実装して明示的にエクスポートするように SalesOrderView を変更しています。また、ISalesOrderView をインポートするように、インポート側のパーツのファクトリも変更しています。MEF ではプロパティ型 (ISalesOrderView) から型を派生できるため、インポート側のパーツで型を明示的に指定する必要がないことがわかります。

ここで、ViewFactory でも IViewFactory のようなインターフェイスを実装すべきかどうかという疑問が生じますが、ViewFactory 側での実装は必須ではありません。ただし、モック目的には意味がある場合があります。このサンプルでは、ViewFactory が他人に置き換えられることは想定せず、テストが容易になるようにアプリケーションを設計しているため、省略してもかまいません。また、パーツが複数のコントラクトの下でインポートされるよう、1 つのパーツに複数のエクスポートを設定できます。たとえば、次のように SalesOrderView に追加のエクスポート属性を設定して、SalesOrderView を UserControl と ISalesOrderView の両方でエクスポートできます。

[Export (typeof(ISalesOrderView))]
[Export (typeof(UserControl))]
public partial class SalesOrderView : UserControl, ISalesOrderView
{
   ...
}

コントラクト アセンブリ

コントラクトの作成に着手するときに、作成したコントラクトをサードパーティ製製品に配置する手段が必要になります。よく行われるのは、エクステンダーが実装するコントラクト用インターフェイスを含むコントラクト アセンブリを用意する方法です。コントラクト アセンブリは、パーツから参照する SDK の形式になります。一般的なパターンとしては、アプリケーション名の末尾に .Contracts を付加した名前 (SalesOrderManager.Contracts など) をコントラクト アセンブリに付けます。

同じコントラクトの複数のエクスポートをインポートする

現在、ViewFactory からインポートしているのは 1 つのビューだけです。各ビューのメンバー (プロパティ パラメーター) をハードコードする手法は、頻繁には変更されない、ごく少数の型が定義済みのビューに適しています。しかし、このような手法では、新しいビューを追加するたびにファクトリを再コンパイルする必要があります。

さまざまな型のビューが想定される場合、MEF にはより適切な手法が用意されています。この手法では、特定のビューのインターフェイスを使用する代わりに、すべてのビューからエクスポートする IView というジェネリック インターフェイスを作成できます。その結果、ファクトリには使用可能なすべての IViews のコレクションがインポートされます。属性モデルでコレクションをインポートするには、次のように ImportMany 属性を使用します。

[Export]
public class ViewFactory
{
  [ImportMany]
IEnumerable<IView> Views { get; set; }
}

[Export(typeof(IView))]
public partial class SalesOrderView : UserControl, IView
{
}
//in a contract assembly
public interface IView{}

上記のコードからわかるように、ViewFactory は特定のビューではなく IView のインスタンスのコレクションをインポートするようになります。SalesOrderView では、ISalesOrderView の代わりに IView を実装してエクスポートしています。このリファクタリングによって、ViewFactory で任意の組み合わせのビューをサポートできるようになります。

MEF では、ObservableCollection<T>、List<T> などの具象コレクションや、既定のコンストラクターを用意したカスタム コレクションを使用したインポートもサポートされます。

パーツの作成ポリシーを制御する

既定では、コンテナー内のすべてのパーツ インスタンスはシングルトンになるため、そのパーツ インスタンスをコンテナー内にインポートするすべてのパーツで共有されます。このため、インポート側のすべてのパーツで SalesOrderView と ViewFactory の同じインスタンスが取得されます。この処理は、他のコンポーネントが依存している静的メンバーを取得する代わりになるため、多くの場合に適しています。しかし、たとえば同時に複数の SalesOrderView インスタンスを画面に表示できるようにする場合など、インポート側のパーツごとに専用のインスタンスを取得しなければならない場合もあります。

MEF でのパーツ作成ポリシーは、CreationPolicy.Shared、CreationPolicy.NonShared、CreationPolicy.Any のいずれかの値に設定できます。パーツの作成ポリシーを指定するには、そのパーツを次のように partCreationPolicy 属性で修飾します。

[partCreationPolicy(CreationPolicy.NonShared)]
[Export(typeof(ISalesOrderView))]
public partial class SalesOrderView : UserControl, ISalesOrdderView
{
public SalesOrderView()
  {
  }
}

インポートに RequiredCreationPolicy プロパティを設定すると、インポート側のパーツでも PartCreationPolicy 属性を指定できます。

メタデータを使ってエクスポートを区別する

この時点では、ViewFactory で任意の組み合わせのビューを処理できますが、ビューどうしを区別する方法がありません。1 つの解決策としては、ビューが提供する ViewType というメンバーを IView に追加して、このプロパティに照らしてフィルター処理を実行できます。もう 1 つの解決策は、MEF のエクスポート メタデータの機能を使用して、ビューにその ViewType で注釈をつけます。メタデータを使用すると、必要なときまでビューのインスタンス作成を遅延できるため、リソースを節約してパフォーマンスを向上できるというメリットもあります。

エクスポート メタデータを定義する

エクスポートのメタデータを定義するには、ExportMetadata 属性を使用します。次のコードでは、コントラクトとして IView マーカー インターフェイスをエクスポートするように SalesOrderView を変更しています。さらに、"ViewType" というメタデータも追加しているため、同じコントラクトを共有する他のビューと区別できます。

[ExportMetadata("ViewType", "SalesOrder")]
[Export(typeof(IView)]
public partial class SalesOrderView : UserControl, IView
{
}

ExportMetadata には、文字列型のキーと型オブジェクトの値という 2 つのパラメーターを受け取ります。前の例のような便利な省略構文を使用すると、コンパイルに影響するため、問題が発生するおそれがあります。省略構文の代わりに、次のようにキーの定数と値の列挙を指定できます。

[ExportMetadata(ViewMetadata.ViewType, ViewTypes.SalesOrder)]
[Export(typeof(IView)]
public partial class SalesOrderView : UserControl, IView
{
  ...
}
//in a contract assembly
public enum ViewTypes {SalesOrderView}

public class ViewMetadata
{
public const string ViewType = "ViewType";
}

ExportMetadata 属性を使用すると柔軟性が高まりますが、次のようにいくつか注意点もあります。

  • メタデータ キーは、IDE で検出されません。パーツの作成者は、エクスポートに有効なメタデータ キーと型を把握しておく必要があります。
  • コンパイラではメタデータが正しいかどうか検証されません。
  • ExportMetadata を使用すると、意図が不明瞭になり、余分なコードが増加します。

このような問題に対処するために、MEF にはカスタム エクスポートという解決策が用意されています。

カスタム エクスポートの属性

MEF では、独自のメタデータを含むカスタム エクスポートを作成できます。カスタム エクスポートを作成するには、派生した ExportAttribute を作成します。この属性もメタデータを指定します。次のようにカスタム エクスポートを使用して、ViewType 用メタデータを含む ExportView 属性を作成できます。

[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
public class ExportViewAttribute : ExportAttribute {
public ExportViewAttribute()
:base(typeof(IView))
  {}

public ViewTypes ViewType { get; set; }
}

ExportViewAttribute では、Export の基本コンストラクターを呼び出して、IView をエクスポートするよう指定します。ExportViewAttribute は MetadataAttribute で修飾され、属性によってメタデータを提供することが指定されています。この属性は、すべてのパブリック プロパティを照合し、プロパティ名をキーに使用してエクスポートの関連メタデータを作成するよう MEF に指示します。この場合、メタデータは ViewType だけです。

ExportView 属性に関する最後の重要な点は、AttributeUsage 属性で修飾されていることです。これにより、属性がクラスでのみ有効なこと、および ExportView の属性は 1 つだけ存在できることが指定されます。

一般に、AllowMultiple は false に設定します。true に設定すると、インポート側のパーツには単一値ではなく値の配列が渡されます。同じメンバーに、同じコントラクトで異なるメタデータが設定された複数のエクスポートが存在する場合は、AllowMultiple を true のままにします。

新しい ExportViewAttribute を SalesOrderView に適用すると、次のようになります。

[ExportView(ViewType = ViewTypes.SalesOrder)]  
public partial class SalesOrderView : UserControl, IView
{
}

ご覧のように、カスタム エクスポートを使用すると、特定のエクスポートに適切なメタデータを提供できます。また、余分なコードを削減でき、IntelliSense から検出されやすく、ドメイン固有なので意図がわかりやすくなります。

これでビューのメタデータを定義したので、ViewFactory からインポートできます。

遅延エクスポートをインポートしてメタデータにアクセスする

メタデータにアクセスできるよう、MEF では System.Lazy<T> という .NET Framework 4 の新しい API を使用します。この API では、Lazy の value プロパティにアクセスするまでインスタンスの作成を遅延できます。MEF では、Lazy<T> が Lazy<T,TMetadata> でさらに拡張されているため、基になるエクスポートのインスタンスを作成しなくてもエクスポート メタデータにアクセスできます。

TMetadata は、メタデータ ビューの型です。メタデータ ビューは、エクスポートされるメタデータのキーに対応する、読み取り専用のプロパティを定義するインターフェイスです。metadata プロパティにアクセスすると、MEF では TMetadata が動的に実装され、エクスポートから提供されたメタデータを基に値が設定されます。

Lazy<T,TMetadata> を使用してインポートするように Views プロパティを変更すると、ViewFactory は次のようになります。

[Export]
public class ViewFactory
{
  [ImportMany]
IEnumerable<Lazy<IView, IViewMetadata>> Views { get; set; }
}

public interface IViewMetadata
{
ViewTypes ViewType {get;}
}

メタデータを使用した遅延エクスポートのコレクションがインポートされたら、LINQ を使用してこのセットに照らしてフィルター処理できます。以下のコード スニペットでは、指定した型のビューをすべて取得する GetViews メソッドを ViewFactory に実装しています。このメソッドでは、フィルターに一致するビューに限定して実際のビュー インスタンスを作成するために、Value プロパティにアクセスしているのがわかります。

[Export]
public class ViewFactory
{
  [ImportMany]
IEnumerable<Lazy<IView, IViewMetadata>> Views { get; set; }

public IEnumerable<View> GetViews(ViewTypesviewType) {
return Views.Where(v=>v.Metadata.ViewType.Equals(viewType)).Select(v=>v.Value);
  }
}

このように変更すると、"ファクトリが MEF によって組み立てられる時点" で使用可能なすべてのビューが、ViewFactory で検出されるようになります。この初期構成後に新しい実装がコンテナーやカタログに追加されると、ViewFactory は既に組み立て済みのため、追加された実装は ViewFactory では検出されません。それだけでなく、実際には、MEF で CompositionException がスローされ、ビューがカタログに追加されません。つまり、再構成が有効にならない限り追加されません。

再構成

再構成は MEF の機能の 1 つで、新しく一致するエクスポートがシステムに追加されたときに、パーツのインポートが自動的に更新されるようにします。リモート サーバーからパーツをダウンロードする場合などに、再構成が有効です。SalesOrderManager は、起動時にいくつかオプションのビューのダウンロードを開始するように変更できます。ビューはダウンロードされると、ビュー ファクトリに追加されます。ViewFactory を再構成可能にするには、次のように Views プロパティの ImportMany 属性で AllowRecomposition プロパティを true に設定します。

[Export]
public class ViewFactory
{
[ImportMany(AllowRecomposition=true)]
IEnumerable<Lazy<IView, IViewMetadata>> Views { get; set; }

public IEnumerable<View>GetViews(ViewTypesviewType) {
return Views.Where(v=>v.Metadata.ViewType.Equals(viewType)).Select(v=>v.Value);
  }
}

再構成が実行されると、Views コレクションは、更新された一連のビューを含む新しいコレクションに即座に置き換えられます。

再構成が有効な場合、アプリケーションでサーバーから追加のアセンブリをダウンロードして、コンテナーに追加できます。MEF のカタログを使用して、この処理を実行できます。MEF では数種類のカタログを使用できますが、再構成可能なのはそのうちの 2 つです。既に説明した DirectoryCatalog は、Refresh メソッドを呼び出すと再構成が行われるカタログの 1 つです。もう 1 つの再構成可能なカタログは AggregateCatalog で、これはカタログのカタログです。Catalogs コレクション プロパティを使用して AggregateCatalog にカタログを追加すると、再構成が開始されます。以下のコードで最後に使用しているのは、AssemblyCatalog です。このカタログでは、アセンブリを受け取り、そのアセンブリに基づいてカタログを構築します。図 2 に、これらのカタログを組み合わせて動的ダウンロードに使用する方法を示すサンプルを示します。

図 2 MEF カタログを使用した動的ダウンロード

void App_Startup(object sender, StartupEventArgs e)
{
var catalog = new AggregateCatalog();
catalog.Catalogs.Add(newDirectoryCatalog((@"\.")));
var container = new CompositionContainer(catalog);
container.Composeparts(this);
base.MainWindow = MainWindow;
this.DownloadAssemblies(catalog);
}

private void DownloadAssemblies(AggregateCatalog catalog)
{
//asynchronously downloads assemblies and calls AddAssemblies
}

private void AddAssemblies(Assembly[] assemblies, AggregateCatalog catalog)
{
var assemblyCatalogs = new AggregateCatalog();
foreach(Assembly assembly in assemblies)
assemblyCatalogs.Catalogs.Add(new AssemblyCatalog(assembly));
catalog.Catalogs.Add(assemblyCatalogs);
}

図 2 のコンテナーは、AggregateCatalog を使って作成しています。次に、bin フォルダーのローカル パーツを取得するために、このコンテナーに DirectoryCatalog を追加します。こうして集約したカタログを DownloadAssemblies メソッドに渡すと、このメソッドによって非同期にアセンブリがダウンロードされてから AddAssemblies メソッドが呼び出されます。AddAssemblies メソッドでは、新しい AggregateCatalog を作成し、ダウンロードしたアセンブリごとにこのカタログに AssemblyCatalog を追加します。続いて AddAssemblies メソッドでは、アセンブリを含む AggregateCatalog を追加し、アプリケーション本体のカタログに集約します。このようにしてカタログを追加するのは、再構成を繰り返さず一度に実行するためです。アセンブリのカタログを直接追加すると、再構成が繰り返し実行されます。

再構成が実行されると、コレクションが即座に更新されます。コレクション プロパティの型によって、実際の更新処理は異なります。プロパティが IEnumerable<T> 型の場合は、新しいインスタンスに置き換えられます。List<T> または ICollection から継承された具象コレクションの場合は、MEF によって項目ごとに Clear と Add が呼び出されます。どちらの場合でも、再構成を使用する場合は、スレッド セーフにすることを検討する必要があります。再構成は、カタログの追加だけでなく、削除にも関連します。カタログがコンテナーから削除する場合は、カタログのパーツも削除することになります。

安定した構成、拒否、および診断

場合によっては、カタログに存在しないパーツのインポートが指定されていることがあります。このような場合、MEF は依存関係が失われているパーツ (またはそのパーツに依存しているパーツ) が検出されないようにします。このような処理が行われる理由は、そのようなパーツが作成されたら間違いなく発生することになるランタイム エラーを防ぎ、システムを安定させるためです。

次のコードでは、ロガーのインスタンスが存在していないのに ILogger をインポートするよう、SalesOrderView を変更しています。

[ExportView(ViewType = ViewTypes.SalesOrder)]  
public partial class SalesOrderView : UserControl, IView
{
[Import]
public ILogger Logger { get; set; }
}

使用できる ILogger エクスポートがないため、SalesOrderView のエクスポートはコンテナーに表示されません。これによって例外がスローされることはなく、SalesOrderView が無視されるだけです。ViewFactory の Views コレクションをチェックすれば、空になっています。

次のように、1 つのインポートに対して使用可能なエクスポートが複数あると、そのようなパーツが拒否されます。この場合、その 1 つのエクスポートをインポートするパーツだけが拒否されます。

[ExportView(ViewType = ViewTypes.SalesOrder)]  
public partial class SalesOrderView : UserControl, IView
{
[Import]
public ILogger Logger { get; set; }
}
 [Export(typeof(ILogger))]  
public partial class Logger1 : ILogger
{
}
 [Export(typeof(ILogger))]  
public partial class Logger2 : ILogger
{
}

上記の例の SalesOrderView には複数の ILogger が実装されていますが、インポートされる実装は 1 つだけのため、このパーツは拒否されます。MEF には、エクスポートが複数存在する場合に既定のエクスポートを許可する機能が用意されています。詳細については、codebetter.com/blogs/glenn.block/archive/2009/05/14/customizing-container-behavior-part-2-of-n-defaults.aspx (英語) を参照してください。

「どうして MEF では SalesOrderView が作成されず、例外をスローしないのだろう」と疑問に思う方もいらっしゃるでしょう。オープンで拡張可能なシステムでは、パーツが不足していたり、構成上インポートが何重にも入れ子になっていたりするため、MEF が例外をスローすると、アプリケーションでその例外をハンドルするのも、コンテキストを取得して必要な処理を特定するのも非常に困難になります。例外が適切にハンドルされないと、アプリケーションが無効状態になり、使用できなくなります。MEF ではそのパーツを拒否することで、アプリケーションの安定性を確保します。安定した構成の詳細については、blogs.msdn.com/gblock/archive/2009/08/02/stable-composition-in-mef-preview-6.aspx (英語) を参照してください。

拒否を診断する

拒否は非常に優れた機能ですが、特に依存グラフ全体が拒否されている場合など診断が難しくなることもあります。このファイルの最初のコード例では、ViewFactory から直接 SalesOrderView をインポートしています。MainWindow で ViewFactory がインポートされる場合に、SalesOrderView が拒否されるとすると、ViewFactory と MainWindow も拒否されることになります。実際には MainWindow と ViewFactory は存在しているとわかっているため、このような事態に遭遇したら皆さんは困惑されるでしょう。拒否の原因は依存関係が不足していることにあります。

MEF はこうした困惑を残しません。この問題の診断に役立つよう、MEF にはトレース機能が備わっています。IDE では、出力ウィンドウですべての拒否メッセージがトレースされますが、任意の有効なトレース リスナーでもトレースできます。たとえば、アプリケーションから MainWindow をインポートしようとすると、図 3 のようなトレース メッセージが出力されます。

図 3 MEF のトレース メッセージ

System.ComponentModel.Composition Warning: 1 : The ComposablepartDefinition 'Mef_MSDN_Article.SalesOrderView' has been rejected. The composition remains unchanged. The changes were rejected because of the following error(s): The composition produced a single composition error. The root cause is provided below. Review the CompositionException.Errors property for more detailed information.

1) No valid exports were found that match the constraint '((exportDefinition.ContractName == "Mef_MSDN_Article.ILogger") AndAlso (exportDefini-tion.Metadata.ContainsKey("ExportTypeIdentity") AndAlso "Mef_MSDN_Article.ILogger".Equals(exportDefinition.Metadata.get_Item("ExportTypeIdentity"))))', invalid exports may have been rejected.

Resulting in: Cannot set import 'Mef_MSDN_Article.SalesOrderView.Logger (ContractName="Mef_MSDN_Article.ILogger")' on part 'Mef_MSDN_Article.SalesOrderView'.
Element: Mef_MSDN_Article.SalesOrderView.logger (ContractName="Mef_MSDN_Article.ILogger") -->Mef_MSDN_Article.SalesOrderView -->TypeCatalog (Types='Mef_MSDN_Article.MainWindow, Mef_MSDN_Article.SalesOrderView, ...').

トレースの出力には、問題の根本原因が示されます。つまり、SalesOrderView には ILogger が必要ですが、これを検出できません。さらに、SalesOrderView が拒否されたことでファクトリも拒否され、最終的に MainWindow も拒否されたことがわかります。

デバッガーでパーツを調べる

一歩進んで、カタログ内で実際に使用可能なパーツを調べることができます。これについてはホストに関するセクションで説明します。図 4 のウォッチ ウィンドウでは、使用可能なパーツ (緑の円で囲んだ要素) と必要な ILogger インポート (青い円で囲んだ要素) を確認できます。

ウォッチ ウィンドウに表示される使用可能なパーツと必要な ILogger

図 4 ウォッチ ウィンドウに表示される使用可能なパーツと必要な ILogger

コマンド ラインで拒否を診断する

MEF の目標の 1 つは、静的分析をサポートし、構成をランタイム環境外で分析できるようにすることでした。このようなツールは、まだ Visual Studio ではサポートされていませんが、Nicholas Blumhardt が便利なコマンド ライン ツールの MEFX.exe (mef.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=33536、英語) を作成しました。MEFX ではアセンブリを分析し、拒否されることになるパーツとその原因を特定します。

コマンド ラインから MEFX.exe を実行すると、多数のオプションが表示されます。このオプションで、特定のインポートやエクスポート、使用可能なすべてのパーツなどを一覧することができます。たとえば、MEFX を使用してパーツの一覧を表示するには次のように入力します。

C:\mefx>mefx.exe /dir:C:\SalesOrderManagement\bin\debug /parts 
SalesOrderManagement.SalesOrderView
SalesOrderManagement.ViewFactory
SalesOrderManagement.MainWindow

この機能はパーツのインベントリを作成するのに役立ちますが、MEFX では、ここでの目的である拒否の追跡も実行できます (図 5 参照)。

図 5 MEFX.exe での拒否の追跡

C:\mefx>mefx.exe /dir:C:\SalesOrderManagement\bin\debug /rejected /verbose 

[part] SalesOrderManagement.SalesOrderView from: DirectoryCatalog (Path="C:\SalesOrderManagement\bin\debug")
  [Primary Rejection]
  [Export] SalesOrderManagement.SalesOrderView (ContractName="SalesOrderManagement.IView")
  [Export] SalesOrderManagement.SalesOrderView (ContractName="SalesOrderManagement.IView")
  [Import] SalesOrderManagement.SalesOrderView.logger (ContractName="SalesOrderManagement.ILogger")
    [Exception] System.ComponentModel.Composition.ImportCardinalityMismatchException: No valid exports were found that match the constraint '((exportDefinition.ContractName == "SalesOrderManagement.ILogger") AndAlso (exportDefinition.Metadata.ContainsKey("ExportTypeIdentity") AndAlso "SalesOrderManagement.ILogger".Equals(exportDefinition.Metadata.get_Item("ExportTypeIdentity"))))', invalid exports may have been rejected.
at System.ComponentModel.Composition.Hosting.ExportProvider.GetExports(ImportDefinition definition, AtomicCompositionatomicComposition)
at System.ComponentModel.Composition.Hosting.ExportProvider.GetExports(ImportDefinition definition)
at Microsoft.ComponentModel.Composition.Diagnostics.CompositionInfo.AnalyzeImportDefinition(ExportProvider host, IEnumerable`1 availableparts, ImportDefinition id)

図 5 の出力を詳しく分析すると、問題の根本原因は ILogger を検出できないことだとわかります。ご覧のように、多数のパーツが存在する大規模なシステムでは、MEFX は非常に役立つツールです。MEFX の詳細については、blogs.msdn.com/nblumhardt/archive/2009/08/28/analyze-mef-assemblies-from-the-command-line.aspx (英語) を参照してください。

IronRuby でのパーツの例

図 6 IronRuby でのパーツの例

まとめると、属性モデルには次のような複数のメリットがあります。

  • パーツでエクスポートとインポートを宣言するための統一手法が用意される
  • 事前登録を必要としないで、システムで使用可能なパーツを動的に検出できる
  • 静的分析が可能で、MEFX などのツールで前もってエラーを特定できる

ここからは、MEF のアーキテクチャを簡単に紹介し、アーキテクチャによって可能になる機能について説明します。大まかに考えると、MEF のアーキテクチャは、プログラミング モデル、ホスト、およびプリミティブの 3 つのレイヤーに分けられます。

プログラミング モデルについて再考する

属性モデルは、検出手段に属性を使用するプリミティブの実装の 1 つにすぎません。プリミティブは、属性が設定されていないパーツや、動的言語ランタイム (DLR) などで静的に型指定されないパーツも表現できます。図 6 に、IOperation をエクスポートする IronRuby パーツを示します。パーツの宣言に、属性モデルではなく IronRuby 本来の構文が使用されていることに注目してください。これは、DLR で属性がサポートされないためです。

MEF には IronRuby プログラミング モデルは付属していませんが、今後、動的言語のサポートが追加される可能性は高いと言えます。

Ruby プログラミング モデルを構築する試みの詳細については、ブログ シリーズ (blogs.msdn.com/nblumhardt/archive/tags/Ruby/default.aspx、英語) を参照してください。

ホスト: 構成の実行場所

プログラミング モデルは、パーツ、インポート、およびエクスポートを定義します。実際にインスタンスとオブジェクト グラフを作成するために、MEF には System.ComponentModel.Composition.Hosting 名前空間に主に存在するホスト API が付属しています。ホスト レイヤーでは、高度な柔軟性、構成可能性、および拡張性が提供されます。このレイヤーで、MEF 処理の大部分が実行され、MEF の検出が開始されます。パーツを作成するだけの開発者の多くは、この名前空間をまったく扱いません。しかし、ホストを開発する開発者は、前述のコードのように、構成のブートストラップを行うためにホスト API を使用することになります。

カタログは、パーツの定義 (ComposablepartDefinition) を提供します。この定義には使用可能なエクスポートとインポートを記述します。カタログは、MEF の主要検出単位です。MEF の System.ComponentModel.Composition 名前空間にはいくつかのカタログが用意され、その一部は今回の記事でも取り上げました。たとえば、DirectoryCatalog はディレクトリをスキャンし、AssemblyCatalog はアセンブリをスキャンし、TypeCatalog は特定の型のセットをスキャンします。これらのカタログは、どれも属性プロブラミング モデル固有のカタログです。ただし、AggregateCatalog はプログラミング モデルに依存しません。カタログは ComposablepartCatalog から継承され、MEF での拡張ポイントにもなります。カスタム カタログには、まったく新しいプログラミング モデルの提供から、既存のカタログのラップやフィルター処理に至るまで、いくつもの用途があります。

図 7 は、フィルター処理されるカタログの例を示しています。このカタログでは、内部カタログをフィルター処理する述語を受け取って、その内部カタログからパーツを返します。

図 7 フィルター処理されるカタログ

public class FilteredCatalog : ComposablepartCatalog, 
{
private readonly composablepartcatalog _inner;
private readonly IQueryable<ComposablepartDefinition> _partsQuery;

public FilteredCatalog(ComposablepartCatalog inner,
Expression<Func<ComposablepartDefinition, bool>> expression)
  {
      _inner = inner;
    _partsQuery = inner.parts.Where(expression);
  }

public override IQueryable<ComposablepartDefinition> parts
  {
get
      {
return _partsQuery;
      }
  }
}

CompositionContainer の役割は、構成、つまり、パーツを作成して、作成したパーツのインポートを設定することです。インポートを設定する際、使用可能なエクスポートのプールから該当するインポートを取得します。こうしたエクスポートにもインポートが含まれていれば、コンテナーは、まず、それらのインポートも設定します。このようにして、コンテナーでは要求時にオブジェクト グラフ全体が組み立てられます。エクスポートのプールのソースには主にカタログが使用されますが、コンテナーに既存のパーツ インスタンスを直接追加して構成することもできます。ほとんどの場合、パーツはカタログからインポートされますが、コンテナーに手動でエントリ ポイント クラスを追加して、カタログからインポートしたパーツと組み合わせることが慣例となっています。

また、スコープのシナリオをサポートするために、コンテナーを階層状の入れ子にできます。子コンテナーは既定で親コンテナーに照会しますが、次のように子パーツを含む独自のカタログを子コンテナーで用意し、子パーツを子コンテナー内に作成することもできます。

var catalog = new DirectoryCatalog(@".\");
var childCatalog = new DirectoryCatalog(@".\Child\";
var rootContainer = new CompositionContainer(rootCatalog));
var childContainer = new CompositionContainer(childCatalog, 
rootContainer);

上記のコードでは、childContainer は rootContainer の子コンテナーとして配置されます。rootContainer と childContainer の両方から独自のカタログが提供されます。コンテナーを使用してアプリケーション内で MEF をホストする方法の詳細については、codebetter.com/blogs/glenn.block/archive/2010/01/15/hosting-mef-within-your-applications.aspx (英語) を参照してください。

プリミティブ: パーツとプログラミング モデルが作成される場所

System.ComponentModel.Composition.Primitives 名前空間にあるプリミティブは、MEF で最も低いレベルにあります。プリミティブは、いわば MEF の量子宇宙であり、非常に重要な拡張ポイントです。今回の記事では、属性プログラミング モデルを取り上げました。しかし、MEF のコンテナーはまったく属性の制約を受けません。コンテナーが制約を受けるのはプリミティブです。プリミティブはパーツの抽象表現を定義します。こうした抽象表現には、ComposablepartDefinition、ImportDefinition、ExportDefinition などの定義や、実際のインスタンスを表す Composablepart や Export などがあります。

プリミティブは、それだけで 1 つのトピックにできるほど複雑なので、今後の記事で紹介するつもりです。それまでの間、プリミティブの詳細については、blogs.msdn.com/dsplaisted/archive/2009/06/08/a-crash-course-on-the-mef-primitives.aspx (英語) を参照してください。

Silverlight 4 やその他のプラットフォームにおける MEF

MEF は、Silverlight 4 にも付属しています。今回の記事で説明したすべての内容は、拡張可能なリッチ インターネット アプリケーションの開発にも関連します。Silverlight では、さらに開発が進められ、MEF でのアプリケーション作成を簡略化するための追加の API が導入されています。これらの機能強化は、最終的に .NET Framework にも組み込まれる予定です。

Silverlight 4 の MEF の詳細については、次のブログ記事を参照してください。codebetter.com/blogs/glenn.block/archive/2009/11/29/mef-has-landed-in-silverlight-4-we-come-in-the-name-of-extensibility.aspx (英語)

今回の記事では、MEF で実行できる処理の概要を説明しただけです。MEF は強力かつ堅牢で、柔軟なツールです。手持ちのツールに MEF を加えれば、アプリケーションの可能性が大きく広がります。皆さんが MEF で実現した機能を目にするのを楽しみにしています。

Glenn Block は、.NET Framework 4 の新しい Managed Extensibility Framework (MEF) の PM です。MEF の前は、Prism およびその他のクライアント ガイダンスを担当する patterns & practices の製品プランナーでした。Block は卓越した知識を持ち、ALT.NET などのカンファレンスやグループでその専門知識を広めることに時間を費やしています。 彼のブログは codebetter.com/blogs/glenn.block (英語) です。

この記事のレビューに協力してくれた技術スタッフの Ward Bell、Nicholas Blumhardt、Krzysztof Cwalina、Andreas Håkansson、Krzysztof Kozmic、Phil Langeberg、Amanda Launcher、Jesse Liberty、Roger Pence、Clemens Szypierski、Mike Taulty、Micrea Trofin、および Hamilton Verissimo に心より感謝いたします。