MEF(Managed Extensibility Framework)はちょっと変わったDIコンテナ

この前の勉強会で聞かれたので調べてみたら、これ「System.AddIn」のやつとは全然関係ないのね。

でも、アドインとかの機構を実現させるためのフレームワークというのには違いが無いみたい。どう住み分けるんだろう?

で、軽く触ってみたんだけど、これって要はDIコンテナなのね。他のDIコンテナよりも少しだけスコープが広いみたいな。

以下のURLに簡単な使い方が載っているので見てみると、

基本はプロパティに対してインジェクションする「セッターインジェクション」で*1、その設定はImport属性とExport属性で行う。

Export属性でクラスをマークするとDIコンテナに登録できる。逆にImport属性でプロパティをマークするとオブジェクトをDIコンテナからインジェクションできる。このマッピングには型と名前(自由に定義できる)が使える。

// DIコンテナに型で登録
[Export(typeof(HogeService))]
// 名前で登録
// [Export("hogeService")]
public class HogeService {
}

public class HogeClient {
    // DIコンテナから型で取り出し
    [Import]
    // 名前で取り出し
    // [Import("hogeService")]
    public HogeService {
        get;
        set;
    }
}

サンプルではExport側とImport側でアセンブリを分けてアドインの機構を作っていたけど、別に分けなくてインジェクションだけしていれば「Unity Application Block」なんかと一見変わらない感じ。

ただ、このフレームワークが他と違うのが、Exportできるのがクラスだけじゃなくプロパティやメソッドもその対象になるという事とアドインアセンブリが追加・削除されたというイベントをハンドリングする事ができるという点。

この辺が他のDIコンテナとは一線を画する。まぁ設計思想が違うってことですな。

このフレームワークでおもしろいと思うところはImport設定とExport設定を読み込むところ(カタログと呼ぶ)がカスタム属性だけに限定されていないこと。

デフォルトで用意されているカタログは、

  • AggregateCatalog
  • AssemblyCatalog
  • DirectoryCatalog
  • TypeCatalog

の四種類。といってもこいつらは全部カスタム属性から設定を読み込んでいるんだけどね。読み込むアセンブリの指定方法が違うというだけで。

でも、実際にはカスタム属性に限定されないので、ちょっとアプリケーション構成ファイルのAppSettingsセクションの内容をExportするカタログを作ってみた。

AppSettingsPartCatalog.cs

class AppSettingsPartCatalog : ComposablePartCatalog {
    public override IQueryable<ComposablePartDefinition> Parts {
        get {
            var array = new[] {
                new AppSettingsPartDefinition()
            };
            return array.AsQueryable<ComposablePartDefinition>();
        }
    }
}

class AppSettingsPart : ComposablePart {
    private AppSettingsPartDefinition partDef;

    internal AppSettingsPart(AppSettingsPartDefinition partDef) {
        this.partDef = partDef;
    }

    public override IEnumerable<ExportDefinition> ExportDefinitions {
        get { return partDef.ExportDefinitions; }
    }

    public override object GetExportedObject(ExportDefinition definition) {
        // 本当はImportするプロパティの型に合わせて変換したいけど、Import側を調べる方法がわからん。
        return int.Parse(definition.Metadata["Value"].ToString());
    }

    public override IEnumerable<ImportDefinition> ImportDefinitions {
        get { return partDef.ImportDefinitions; }
    }

    public override void SetImport(ImportDefinition definition, IEnumerable<Export> exports) {
    }
}

class AppSettingsPartDefinition : ComposablePartDefinition {
    public override ComposablePart CreatePart() { return new AppSettingsPart(this); }

    public override IEnumerable<ExportDefinition> ExportDefinitions {
        get {
            var appSettings = ConfigurationManager.AppSettings;

            foreach(var key in appSettings.AllKeys) {
                yield return new ExportDefinition(key, new Dictionary<string, object> {
                    { "Value", appSettings[key] }
                });
            }
        }
    }

    public override IEnumerable<ImportDefinition> ImportDefinitions {
        get { yield break; }
    }
}

独自のカタログを定義するには「ComposablePartCatalog」クラスを継承する。

アプリケーション構成ファイルはこんな感じ

App.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <appSettings>
        <add key="app.form.left" value="500" />
        <add key="app.form.top" value="500" />
        <add key="app.form.width" value="100" />
        <add key="app.form.height" value="200" />
    </appSettings>
</configuration>

で、インポート側のフォーム

MainForm.cs
public partial class MainForm : Form {
    [Import("app.form.left", AllowDefault = true)]
    public int FormLeft {
        set {
            this.Left = value;
        }
    }
    [Import("app.form.top", AllowDefault = true)]
    public int FormTop {
        set {
            this.Top = value;
        }
    }
    [Import("app.form.width", AllowDefault = true)]
    public int FormWidth {
        set {
            this.Width = value;
        }
    }
    [Import("app.form.height", AllowDefault = true)]
    public int FormHeight {
        set {
            this.Height = value;
        }
    }

    public MainForm() {
        InitializeComponent();

        var catalog = new AppSettingsPartCatalog();
        var container = new CompositionContainer(catalog);
        var batch = new CompositionBatch();
        batch.AddPart(this);

        container.Compose(batch);
    }
}

と、まぁ意味があるかどうかはわからないけど、こんな事ができますよという例。

まだプレビュー4だからか機能が足らない感じだけど、今後に期待したい。

*1:もちろん他のインジェクションもサポートされている、コンストラクタインジェクションとか