この記事では、.NET Framework 4 で導入された Managed Extensibility Framework の概要について説明します。
MEF とは
Managed Extensibility Framework (MEF) は、軽量で拡張可能なアプリケーションを作成するためのライブラリです。 これにより、アプリケーション開発者は、構成を必要とせず、拡張機能を検出して使用できます。 また、拡張機能開発者はコードを簡単にカプセル化し、脆弱なハード依存関係を回避することもできます。 MEF を使用すると、アプリケーション内だけでなく、アプリケーション間でも拡張機能を再利用できます。
拡張性の問題
あなたは、拡張性のサポートを提供する必要がある大規模なアプリケーションのアーキテクトであるとします。 アプリケーションには、多数の小さなコンポーネントを含める必要があり、コンポーネントの作成と実行を担当します。
この問題に対する最も簡単な方法は、コンポーネントをソース コードとしてアプリケーションに含め、コードから直接呼び出す方法です。 これには、いくつかの明らかな欠点があります。 最も重要なのは、ソース コードを変更せずに新しいコンポーネントを追加できないことです。これは、Web アプリケーションなどでは許容できる制限ですが、クライアント アプリケーションでは動作しません。 同様に問題となるのは、コンポーネントのソース コードにアクセスできない可能性があります。コンポーネントはサード パーティによって開発される可能性があり、同じ理由でコンポーネントへのアクセスを許可できないからです。
もう少し高度なアプローチは、拡張ポイントまたはインターフェイスを提供して、アプリケーションとそのコンポーネント間の切り離しを可能にすることです。 このモデルでは、コンポーネントが実装できるインターフェイスと、アプリケーションとの対話を可能にする API を提供できます。 これにより、ソース コードへのアクセスが必要になるという問題は解決しますが、依然として独自の問題があります。
アプリケーションにはコンポーネントを単独で検出するための容量がないため、どのコンポーネントが使用可能であり、読み込むかを明示的に伝える必要があります。 これは通常、使用可能なコンポーネントを構成ファイルに明示的に登録することによって実現されます。 これは、コンポーネントが正しいことを保証することがメンテナンスの問題になることを意味します。特に、更新を行う必要がある開発者ではなく、エンド ユーザーである場合です。
さらに、コンポーネントは、アプリケーション自体の厳密に定義されたチャネルを介した場合を除き、相互に通信することはできません。 アプリケーション アーキテクトが特定の通信の必要性を予測していない場合、通常は不可能です。
最後に、コンポーネント開発者は、実装するインターフェイスが含まれているアセンブリに対する厳密な依存関係を受け入れる必要があります。 これにより、コンポーネントを複数のアプリケーションで使用することが困難になり、コンポーネントのテスト フレームワークを作成するときに問題が発生する可能性もあります。
MEF が提供するもの
MEF は、使用可能なコンポーネントの明示的な登録ではなく、 コンポジションを介して暗黙的に検出する方法を提供します。 パーツと呼ばれる MEF コンポーネントは、依存関係 (インポートと呼ばれます) と、使用できる機能 (エクスポートと呼ばれます) の両方を宣言によって指定します。 パーツが作成されると、MEF コンポジション エンジンは他のパーツから入手できるインポートを満たします。
この方法では、前のセクションで説明した問題を解決します。 MEF パーツは宣言によって機能を指定するため、実行時に検出できます。つまり、アプリケーションでは、ハードコーディングされた参照や脆弱な構成ファイルなしでパーツを使用できます。 MEF を使用すると、アプリケーションは、インスタンス化したりアセンブリを読み込んだりすることなく、メタデータによってパーツを検出して調べることができます。 そのため、拡張機能を読み込むタイミングと方法を慎重に指定する必要はありません。
指定されたエクスポートに加えて、パーツはインポートを指定できます。このインポートは他のパーツによって塗りつぶされます。 これにより、パーツ間の通信が可能になるだけでなく、簡単になり、コードの適切な要素化が可能になります。 たとえば、多くのコンポーネントに共通するサービスを個別のパーツに組み込み、簡単に変更または交換できます。
MEF モデルでは、特定のアプリケーション アセンブリに対するハード依存関係は必要ないため、アプリケーション間で拡張機能を再利用できます。 これにより、アプリケーションに依存しないテストハーネスを簡単に開発して拡張コンポーネントをテストできます。
MEF を使用して記述された拡張可能なアプリケーションは、拡張コンポーネントで入力できるインポートを宣言し、アプリケーション サービスを拡張機能に公開するためにエクスポートを宣言することもできます。 各拡張コンポーネントはエクスポートを宣言し、インポートを宣言することもできます。 これにより、拡張機能コンポーネント自体が自動的に拡張されます。
MEF が使用可能な場所
MEF は .NET Framework 4 の不可欠な部分であり、.NET Framework が使用されている場合はどこでも使用できます。 MEF は、Windows フォーム、WPF、その他のテクノロジを使用する場合でも、ASP.NET を使用するサーバー アプリケーションでも、クライアント アプリケーションで使用できます。
MEF と MAF
以前のバージョンの .NET Framework では、アプリケーションが拡張機能を分離および管理できるように設計されたマネージド アドイン フレームワーク (MAF) が導入されました。 MAF の焦点は MEF よりも若干高く、拡張機能の分離とアセンブリの読み込みとアンロードに重点を置いていますが、MEF の焦点は検出可能性、拡張性、移植性です。 2 つのフレームワークはスムーズに相互運用でき、1 つのアプリケーションで両方を利用できます。
SimpleCalculator: アプリケーションの例
MEF でできることを確認する最も簡単な方法は、単純な MEF アプリケーションを構築することです。 この例では、SimpleCalculator という名前の非常に単純な電卓を作成します。 SimpleCalculator の目的は、"5+3" または "6-2" の形式で基本的な算術コマンドを受け入れ、正しい回答を返すコンソール アプリケーションを作成することです。 MEF を使用すると、アプリケーション コードを変更せずに新しい演算子を追加できます。
この例の完全なコードをダウンロードするには、 SimpleCalculator サンプル (Visual Basic) を参照してください。
注
SimpleCalculator の目的は、MEF の概念と構文を示すのではなく、必ずしもその使用のための現実的なシナリオを提供することです。 MEF の機能から最もメリットを得られるアプリケーションの多くは、SimpleCalculator よりも複雑です。 より広範な例については、GitHub の Managed Extensibility Framework を参照してください。
開始するには、Visual Studio で新しいコンソール アプリケーション プロジェクトを作成し、
SimpleCalculator
名前を付けます。MEF が存在する
System.ComponentModel.Composition
アセンブリへの参照を追加します。Module1.vbまたはProgram.csを開き、
System.ComponentModel.Composition
とSystem.ComponentModel.Composition.Hosting
のImports
またはusing
ディレクティブを追加します。 これら 2 つの名前空間には、拡張可能なアプリケーションを開発するために必要な MEF 型が含まれています。Visual Basic を使用している場合は、
Module1
モジュールを宣言する行にPublic
キーワードを追加します。
コンポジション コンテナーとカタログ
MEF コンポジション モデルの中核となるのがコン ポジション コンテナーです。このコンテナーには、使用可能なすべてのパーツが含まれ、合成が実行されます。 コンポジションは、インポートとエクスポートのマッチングです。 最も一般的な種類のコンポジション コンテナーは CompositionContainerであり、これを SimpleCalculator に使用します。
Visual Basic を使用している場合は、Module1.vbに Program
という名前のパブリック クラスを追加します。
Module1.vbまたはProgram.csの Program
クラスに次 の 行 を追加します。
Dim _container As CompositionContainer
private CompositionContainer _container;
使用可能なパーツを検出するために、コンポジション コンテナーはカタログを使用 します。 カタログは、あるソースから使用可能なパーツを検出するオブジェクトです。 MEF には、指定された型、アセンブリ、またはディレクトリからパーツを検出するためのカタログが用意されています。 アプリケーション開発者は、Web サービスなどの他のソースからパーツを検出する新しいカタログを簡単に作成できます。
Program
クラスに次のコンストラクターを追加します。
Public Sub New()
' An aggregate catalog that combines multiple catalogs.
Dim catalog = New AggregateCatalog()
' Adds all the parts found in the same assembly as the Program class.
catalog.Catalogs.Add(New AssemblyCatalog(GetType(Program).Assembly))
' Create the CompositionContainer with the parts in the catalog.
_container = New CompositionContainer(catalog)
' Fill the imports of this object.
Try
_container.ComposeParts(Me)
Catch ex As CompositionException
Console.WriteLine(ex.ToString)
End Try
End Sub
private Program()
{
try
{
// An aggregate catalog that combines multiple catalogs.
var catalog = new AggregateCatalog();
// Adds all the parts found in the same assembly as the Program class.
catalog.Catalogs.Add(new AssemblyCatalog(typeof(Program).Assembly));
// Create the CompositionContainer with the parts in the catalog.
_container = new CompositionContainer(catalog);
_container.ComposeParts(this);
}
catch (CompositionException compositionException)
{
Console.WriteLine(compositionException.ToString());
}
}
ComposePartsの呼び出しは、コンポジション コンテナーに対して、特定のパーツ セット (この場合は Program
の現在のインスタンス) を構成するように指示します。 ただし、この時点では、 Program
に入力するインポートがないため、何も起こりません。
属性を使用したインポートとエクスポート
まず、電卓をインポート Program
。 これにより、 Program
に入るコンソールの入力や出力などのユーザー インターフェイスの問題を電卓のロジックから分離できます。
以下のコードを Program
クラスに追加します。
<Import(GetType(ICalculator))>
Public Property calculator As ICalculator
[Import(typeof(ICalculator))]
public ICalculator calculator;
calculator
オブジェクトの宣言は珍しいことではありませんが、ImportAttribute属性で修飾されていることに注意してください。 この属性は、インポートとして何かを宣言します。つまり、オブジェクトが構成されると、コンポジション エンジンによって塗りつぶされます。
すべてのインポートには コントラクトがあり、どのエクスポートと一致するかを決定します。 コントラクトは、明示的に指定された文字列にすることも、特定の型から MEF によって自動的に生成することもできます。この場合、インターフェイス ICalculator
。 一致するコントラクトで宣言されたエクスポートは、このインポートを満たします。
calculator
オブジェクトの型は実際にはICalculator
されていますが、これは必須ではありません。 コントラクトは、インポートするオブジェクトの型から独立しています。 (この場合は、 typeof(ICalculator)
を除外できます。MEF では、明示的に指定しない限り、インポートの種類に基づいてコントラクトが自動的に想定されます)。
この非常に単純なインターフェイスをモジュールまたは SimpleCalculator
名前空間に追加します。
Public Interface ICalculator
Function Calculate(input As String) As String
End Interface
public interface ICalculator
{
string Calculate(string input);
}
ICalculator
を定義したら、それを実装するクラスが必要になります。 モジュールまたは名前空間に次のクラス SimpleCalculator
追加します。
<Export(GetType(ICalculator))>
Public Class MySimpleCalculator
Implements ICalculator
End Class
[Export(typeof(ICalculator))]
class MySimpleCalculator : ICalculator
{
}
Program
のインポートと一致するエクスポートを次に示します。 エクスポートがインポートと一致するためには、エクスポートに同じコントラクトが必要です。
typeof(MySimpleCalculator)
に基づいてコントラクトの下でエクスポートすると不一致が発生し、インポートは満たされません。コントラクトは正確に一致する必要があります。
コンポジション コンテナーには、このアセンブリで使用可能なすべてのパーツが設定されるため、 MySimpleCalculator
パーツが使用可能になります。
Program
のコンストラクターがProgram
オブジェクトに対してコンポジションを実行すると、そのインポートには MySimpleCalculator
オブジェクトが格納され、その目的のために作成されます。
ユーザー インターフェイス レイヤー (Program
) は、他の情報を知る必要はありません。 そのため、 Main
メソッドの残りのユーザー インターフェイス ロジックを入力できます。
Main
メソッドに次のコードを追加します。
Sub Main()
' Composition is performed in the constructor.
Dim p As New Program()
Dim s As String
Console.WriteLine("Enter Command:")
While (True)
s = Console.ReadLine()
Console.WriteLine(p.calculator.Calculate(s))
End While
End Sub
static void Main(string[] args)
{
// Composition is performed in the constructor.
var p = new Program();
Console.WriteLine("Enter Command:");
while (true)
{
string s = Console.ReadLine();
Console.WriteLine(p.calculator.Calculate(s));
}
}
このコードは、単に入力行を読み取り、結果にICalculator
のCalculate
関数を呼び出します。この関数はコンソールに書き戻されます。 これは、 Program
で必要なすべてのコードです。 残りの作業はすべてパートで行われます。
Imports 属性と ImportMany 属性
SimpleCalculator を拡張可能にするには、操作の一覧をインポートする必要があります。 通常の ImportAttribute 属性は、1 つだけの ExportAttributeで塗りつぶされます。 複数のコンポジション エンジンを使用できる場合は、エラーが発生します。 任意の数のエクスポートで入力できるインポートを作成するには、 ImportManyAttribute 属性を使用できます。
MySimpleCalculator
クラスに次の operations プロパティを追加します。
<ImportMany()>
Public Property operations As IEnumerable(Of Lazy(Of IOperation, IOperationData))
[ImportMany]
IEnumerable<Lazy<IOperation, IOperationData>> operations;
Lazy<T,TMetadata> は、エクスポートへの間接参照を保持するために MEF によって提供される型です。 ここでは、エクスポートされたオブジェクト自体に加えて、 エクスポートメタデータ、またはエクスポートされたオブジェクトを記述する情報も取得します。 各 Lazy<T,TMetadata> には、実際の操作を表す IOperation
オブジェクトと、そのメタデータを表す IOperationData
オブジェクトが含まれています。
次の単純なインターフェイスをモジュールまたは SimpleCalculator
名前空間に追加します。
Public Interface IOperation
Function Operate(left As Integer, right As Integer) As Integer
End Interface
Public Interface IOperationData
ReadOnly Property Symbol As Char
End Interface
public interface IOperation
{
int Operate(int left, int right);
}
public interface IOperationData
{
char Symbol { get; }
}
この場合、各操作のメタデータは、+、-、*など、その操作を表すシンボルです。 追加操作を使用できるようにするには、モジュールまたは名前空間に次のクラス SimpleCalculator
追加します。
<Export(GetType(IOperation))>
<ExportMetadata("Symbol", "+"c)>
Public Class Add
Implements IOperation
Public Function Operate(left As Integer, right As Integer) As Integer Implements IOperation.Operate
Return left + right
End Function
End Class
[Export(typeof(IOperation))]
[ExportMetadata("Symbol", '+')]
class Add: IOperation
{
public int Operate(int left, int right)
{
return left + right;
}
}
ExportAttribute属性は、前と同じように機能します。
ExportMetadataAttribute属性は、名前と値のペアの形式でメタデータをエクスポートにアタッチします。
Add
クラスはIOperation
を実装しますが、IOperationData
を実装するクラスは明示的に定義されていません。 代わりに、指定されたメタデータの名前に基づくプロパティを使用して、MEF によってクラスが暗黙的に作成されます。 (これは、MEF のメタデータにアクセスするいくつかの方法の 1 つです)。
MEF でのコンポジションは 再帰的です。
Program
オブジェクトを明示的に作成しました。このオブジェクトは、MySimpleCalculator
型であることが判明したICalculator
をインポートしました。
MySimpleCalculator
次に、IOperation
オブジェクトのコレクションをインポートします。そのインポートは、Program
のインポートと同時に、MySimpleCalculator
の作成時に入力されます。
Add
クラスが追加のインポートを宣言した場合は、そのインポートも入力する必要があります。 インポートが未入力の場合、合成エラーが発生します。 (ただし、インポートを省略可能として宣言したり、既定値を割り当てたりすることはできます)。
電卓ロジック
これらの部分を配置すると、残っているのは電卓ロジック自体のみです。
Calculate
メソッドを実装するために、MySimpleCalculator
クラスに次のコードを追加します。
Public Function Calculate(input As String) As String Implements ICalculator.Calculate
Dim left, right As Integer
Dim operation As Char
' Finds the operator.
Dim fn = FindFirstNonDigit(input)
If fn < 0 Then
Return "Could not parse command."
End If
operation = input(fn)
Try
' Separate out the operands.
left = Integer.Parse(input.Substring(0, fn))
right = Integer.Parse(input.Substring(fn + 1))
Catch ex As Exception
Return "Could not parse command."
End Try
For Each i As Lazy(Of IOperation, IOperationData) In operations
If i.Metadata.symbol = operation Then
Return i.Value.Operate(left, right).ToString()
End If
Next
Return "Operation not found!"
End Function
public String Calculate(string input)
{
int left;
int right;
char operation;
// Finds the operator.
int fn = FindFirstNonDigit(input);
if (fn < 0) return "Could not parse command.";
try
{
// Separate out the operands.
left = int.Parse(input.Substring(0, fn));
right = int.Parse(input.Substring(fn + 1));
}
catch
{
return "Could not parse command.";
}
operation = input[fn];
foreach (Lazy<IOperation, IOperationData> i in operations)
{
if (i.Metadata.Symbol.Equals(operation))
{
return i.Value.Operate(left, right).ToString();
}
}
return "Operation Not Found!";
}
最初の手順では、入力文字列を左右のオペランドと演算子文字に解析します。
foreach
ループでは、operations
コレクションのすべてのメンバーが検査されます。 これらのオブジェクトは Lazy<T,TMetadata>型であり、メタデータ値とエクスポートされたオブジェクトには、それぞれ Metadata プロパティと Value プロパティを使用してアクセスできます。 この場合、IOperationData
オブジェクトのSymbol
プロパティが一致していることが検出された場合、計算ツールはIOperation
オブジェクトのOperate
メソッドを呼び出して結果を返します。
電卓を完了するには、文字列内の最初の数字以外の文字の位置を返すヘルパー メソッドも必要です。
MySimpleCalculator
クラスに次のヘルパー メソッドを追加します。
Private Function FindFirstNonDigit(s As String) As Integer
For i = 0 To s.Length - 1
If Not Char.IsDigit(s(i)) Then Return i
Next
Return -1
End Function
private int FindFirstNonDigit(string s)
{
for (int i = 0; i < s.Length; i++)
{
if (!char.IsDigit(s[i])) return i;
}
return -1;
}
これで、プロジェクトをコンパイルして実行できるようになります。 Visual Basic で、Module1
に Public
キーワードを追加したことを確認します。 コンソール ウィンドウで、"5+3" などの追加操作を入力すると、計算ツールによって結果が返されます。 その他の演算子では、"Operation Not Found!" メッセージが表示されます。
新しいクラスを使用して SimpleCalculator を拡張する
電卓が動作するようになったので、新しい操作を追加するのは簡単です。 モジュールまたは名前空間に次のクラス SimpleCalculator
追加します。
<Export(GetType(IOperation))>
<ExportMetadata("Symbol", "-"c)>
Public Class Subtract
Implements IOperation
Public Function Operate(left As Integer, right As Integer) As Integer Implements IOperation.Operate
Return left - right
End Function
End Class
[Export(typeof(IOperation))]
[ExportMetadata("Symbol", '-')]
class Subtract : IOperation
{
public int Operate(int left, int right)
{
return left - right;
}
}
プロジェクトをコンパイルして実行します。 減算演算 ("5-3" など) を入力します。 電卓で減算と加算がサポートされるようになりました。
新しいアセンブリを使用して SimpleCalculator を拡張する
ソース コードへのクラスの追加は簡単ですが、MEF では、アプリケーション独自のソースの外部でパーツを探す機能が提供されます。 これを示すには、simpleCalculator を変更して、 DirectoryCatalogを追加して、ディレクトリだけでなく、パーツ用の独自のアセンブリも検索する必要があります。
simpleCalculator プロジェクトに Extensions
という名前の新しいディレクトリを追加します。 ソリューション レベルではなく、プロジェクト レベルで追加してください。 次に、 ExtendedOperations
という名前の新しいクラス ライブラリ プロジェクトをソリューションに追加します。 新しいプロジェクトは、別のアセンブリにコンパイルされます。
ExtendedOperations プロジェクトのプロジェクト プロパティ デザイナーを開き、[コンパイル] タブまたは [ビルド] タブをクリックします。SimpleCalculator プロジェクト ディレクトリ (..\SimpleCalculator\Extensions\)。
Module1.vbまたはProgram.csで、Program
コンストラクターに次の行を追加します。
catalog.Catalogs.Add(
New DirectoryCatalog(
"C:\SimpleCalculator\SimpleCalculator\Extensions"))
catalog.Catalogs.Add(
new DirectoryCatalog(
"C:\\SimpleCalculator\\SimpleCalculator\\Extensions"));
例のパスを Extensions ディレクトリへのパスに置き換えます。 (この絶対パスはデバッグのみを目的としています。運用アプリケーションでは、相対パスを使用します)。これで、 DirectoryCatalog は Extensions ディレクトリ内のアセンブリ内にあるパーツをコンポジション コンテナーに追加します。
ExtendedOperations
プロジェクトで、SimpleCalculator
とSystem.ComponentModel.Composition
への参照を追加します。
ExtendedOperations
クラス ファイルで、System.ComponentModel.Composition
のImports
またはusing
ディレクティブを追加します。 Visual Basic では、SimpleCalculator
のImports
ステートメントも追加します。 次に、次のクラスを ExtendedOperations
クラス ファイルに追加します。
<Export(GetType(SimpleCalculator.IOperation))>
<ExportMetadata("Symbol", "%"c)>
Public Class Modulo
Implements IOperation
Public Function Operate(left As Integer, right As Integer) As Integer Implements IOperation.Operate
Return left Mod right
End Function
End Class
[Export(typeof(SimpleCalculator.IOperation))]
[ExportMetadata("Symbol", '%')]
public class Mod : SimpleCalculator.IOperation
{
public int Operate(int left, int right)
{
return left % right;
}
}
コントラクトが一致するためには、 ExportAttribute 属性の型が ImportAttributeと同じである必要があることに注意してください。
プロジェクトをコンパイルして実行します。 新しい Mod (%) 演算子をテストします。
結論
このトピックでは、MEF の基本的な概念について説明しました。
パーツ、カタログ、およびコンポジション コンテナー
パーツとコンポジション コンテナーは、MEF アプリケーションの基本的な構成要素です。 パーツは、値をインポートまたはエクスポートする任意のオブジェクトであり、それ自体を含めるまでです。 カタログは、特定のソースからのパーツのコレクションを提供します。 コンポジション コンテナーは、カタログによって提供されるパーツを使用して、エクスポートへのインポートのバインドであるコンポジションを実行します。
インポートとエクスポート
インポートとエクスポートは、コンポーネントが通信する方法です。 インポートでは、コンポーネントは特定の値またはオブジェクトの必要性を指定し、エクスポートでは値の可用性を指定します。 各インポートは、コントラクトを使用してエクスポートの一覧と照合されます。
次のステップ
この例の完全なコードをダウンロードするには、 SimpleCalculator サンプル (Visual Basic) を参照してください。
詳細とコード例については、「 Managed Extensibility Framework」を参照してください。 MEF 型の一覧については、 System.ComponentModel.Composition 名前空間を参照してください。
.NET