MEF Teil 11 – Neuerungen unter .NET 4.5

Mit dem .NET Framework 4.5 wurden auch bei dem Managed Extensibility Framework (MEF) einige Neuerungen eingeführt. So werden jetzt offene generische Typen unterstützt und es gibt die Möglichkeit einer API-basierten Konfiguration. Dadurch wird eine lose Koppelung noch einfacher erreicht. Diese und alle neuen Erweiterungen erklärt der folgende Post.

Die folgenden fünf Neuerungen sind aus meiner Sicht von besonderer Bedeutung:

  • Unterstützung offener generischer Parts
  • Lebensdauer der Imports durch die Klasse ExportFactory kontrollieren
  • API-basierte Konfiguration (Klasse RegistrationBuilder)
  • Instanziierung der Exports lässt sich durch Scopes begrenzen
  • verbesserte Fehlerdiagnose

Genaugenommen gibt es noch zwei weitere Punkte, die in diesem Post aber nicht behandelt werden:

  • MEF mit ASP.NET MVC
  • leichtgewichtiges Programmiermodell

Unterstützung offener generischer Parts

Generische Klassen ermöglichen die Anwendung einer Implementierung für verschiedene Datentypen. Erst der Anwender der Klasse definiert den genauen Datentyp, mit der die generische Klasse eingesetzt werden soll. Für eine bestimmte Funktionalität ist es somit nicht mehr notwendig, jedem Datentyp eine eigene Implementierung zu spendieren. Die bekanntesten generischen Klassen sind sicherlich List<T> oder Dictionary<TKey, TValue>. Die Anwendung offener generischer Parts ist im MEF denkbar einfach. Als Beispiel dient ein Programm, bei der ein Export zur Laufzeit geladen, instanziiert und dem Import zugewiesen wird.

public interface ICarContract<T>
{
    string DoSomething(T foo);
}

[Export(typeof(ICarContract<>))]
public class CarPart<T> : ICarContract<T>
{
    public string DoSomething(T foo)
    {
        return String.Format("Value: {0}  Type: {1}",
                             foo.ToString(), foo.GetType().Name);
    }
}

Der Export implementiert die Schnittstelle ICarContract<T>, die die generische Methode DoSomething(T foo) enthält. Am Import steht ein geschlossener generischer Typ (z.B. ICarContract<T>), während beim Export ein offener generischer Typ (z.B. ICarContract<>) jetzt möglich ist.

[Import]
private ICarContract<int> carInt = null;

[Import]
private ICarContract<string> carString = null;

Da die Klasse CarPart<T> die Schnittstelle ICarContract<T> implementiert, ist der Export zu den beiden Imports kompatibel. MEF legt von der Klasse zwei Objekte an; einmal mit dem Datentyp string und das zweite mal mit int. Dadurch kann der Export mit unterschiedlichen Datentypen eingesetzt werden, muss aber nur einmal implementiert werden. Das Composen der Parts und deren Zugriff erfolgt in bekannter Art und Weise:

var catalog = new DirectoryCatalog(".");
var container = new CompositionContainer(catalog);
container.ComposeParts(this);

Console.WriteLine(carInt.DoSomething(10));
Console.WriteLine(carString.DoSomething("Hello"));

Beispiel 1 (Visual Studio 2012) auf GitHub

Alternativ dazu kann auf die Variablen, die mit dem Attribut [Import] dekoriert werden, verzichtet werden. Die Methode GetExportedValue() sucht nach dem passenden Export. Der Parameter der Methode definiert dabei den Contract, den der Export erfüllen muss. Dadurch entfällt auch der Aufruf der Methode ComposeParts().

var catalog = new DirectoryCatalog(".");
var container = new CompositionContainer(catalog);

ICarContract<int> carInt = container.GetExportedValue<ICarContract<int>>();
ICarContract<string> carString = container.GetExportedValue<ICarContract<string>>();

Console.WriteLine(carInt.DoSomething(10));
Console.WriteLine(carString.DoSomething("Hello"));

Lebensdauer der Imports kontrollieren

Bisher gab es unter MEF nur wenige Möglichkeiten, die Lebensdauer der Parts gezielt zu beeinflussen. So konnten zwar mit der Methode CompositionContainer.GetExport<T>() Parts erzeugt und mit CompositionContainer.ReleaseExport() wieder freigegeben werden, aber dieses war nur über die Klasse Lazy<T> möglich. Die Methode ReleaseExport() steht auch nur zur Verfügung, wenn die Erstellungsrichtlinien auf NonShared gesetzt wurden.

var catalog = new DirectoryCatalog(".");
var container = new CompositionContainer(catalog);

Lazy<ICarContract> carLazy = container.GetExport<ICarContract>();

carLazy.Value.DoSomething("Hello");

container.ReleaseExport(carLazy);

Weitere Informationen hierzu gibt es unter MEF Teil 2 – Metadaten und Erstellungsrichtlinien und MEF Teil 3 – Lifecycle beeinflussen und überwachen.

Jetzt bietet MEF mit der Klasse ExportFactory eine weitere Möglichkeit an. Hierbei wird ExportFactory mit dem gewünschten Typ importiert.

[Import]
private ExportFactory<ICarContract> carFactory = null;

Über dieses Objekt können jetzt mit der Methode CreateExport() einzelne Exports manuell erzeugt werden. Zurückgeliefert wird eine Instanz der Klasse ExportLifetimeContext<T>. Ähnlich wie bei Lazy<T> erfolgt der Zugriff auf das eigentliche Objekt über die Eigenschaft Value.

ExportLifetimeContext<ICarContract> car = carFactory.CreateExport();
car.Value.DoSomething("Hello");

Anders als bei der Klasse Lazy<T> wird durch die Methode CreateExport() das Objekt unmittelbar instanziiert.

Soll ein Export wieder freigegeben werden, so kann dieses über die Methode Dispose() erfolgen, da ExportLifetimeContext die Schnittstelle IDisposable implementiert. Hat der Export ebenfalls die Schnittstelle implementiert, so wird dadurch auch dessen Dispose() aufgerufen.

car.Dispose();

Das folgende Beispiel erzeugt über die Klasse ExportFactory zwei Objekte eines Exports, ruft deren Methode auf und zerstört die Objekte wieder.

[Import]
private ExportFactory<ICarContract> carFactory = null;

static void Main(string[] args)
{
    new Program().Run();
}
void Run()
{
    var catalog = new DirectoryCatalog(".");
    var container = new CompositionContainer(catalog);

    container.ComposeParts(this);

    ExportLifetimeContext<ICarContract> carA = carFactory.CreateExport();
    ExportLifetimeContext<ICarContract> carB = carFactory.CreateExport();

    carA.Value.DoSomething("carA");
    carB.Value.DoSomething("carB");

    carA.Dispose();
    carB.Dispose();
}

Beispiel 2 (Visual Studio 2012) auf GitHub

Mit ExportFactory kann jederzeit ein neues Objekt erzeugt und bei Bedarf auch wieder freigegeben werden. Dieses ist alleine mit ReleaseExport() und Lazy<T> nicht möglich.

API-basierte Konfiguration

Bisher bediente sich MEF eines deklarativen Programmiermodells. Hierzu wurden die Klassen mit den Attributen Export oder Import dekoriert. Zusätzliche Attribute legten weitere Eigenschaften, wie z.B. die Metadaten oder Erstellungsrichtlinien fest.

Mit .NET 4.5 bietet MEF ein weiteres Programmiermodell an, das parallel zu den bestehenden eingesetzt werden kann. Hierbei werden über eine API die Konventionen zentral festgelegt. Grob vereinfacht geschieht dieses in zwei Schritten:

  • Festlegen der Datentypen, die für die Parts herangezogen werden
  • Konfigurieren der Parts

Datentypen der zu berücksichtigenden Parts festlegen

Von zentraler Bedeutung ist hierbei die Klasse RegistrationBuilder im Namespace System.ComponentModel.Composition.Registration. Diese ist zu finden in den Assemblies System.ComponentModel.Composition.Registration.dll und System.Reflection.Context.dll.

ClassDiagram01

Die drei Methoden ForType<T>(), ForTypesDerivedFrom<T>() und ForTypesMatching(Predicate<Type> filter) ermöglichen das Festlegen der gewünschten Datentypen.

ForType<T>()

Ein konkreter Datentyp wird selektiert.

RegistrationBuilder builder = new RegistrationBuilder();
PartBuilder partBuilder = builder.ForType<CarMercedes>();
ForTypesDerivedFrom<T>()

Selektiert alle Datentypen, die von der angegebenen Klasse abgeleitet sind. Es kann sich hierbei auch um eine Schnittstelle handeln. Die Basisklasse selber wird nicht berücksichtigt, steht also später als Export nicht zur Verfügung.

RegistrationBuilder builder = new RegistrationBuilder();
PartBuilder partBuilder = builder.ForTypesDerivedFrom<ICarContract>();
ForTypesMatching(Predicate<Type> filter)

Diese Methode ermöglicht das Selektieren der Datentypen nach selbst definierbaren Kriterien. Als Parameter wird ein Lambda-Ausdruck übergeben, in dem die zu erfüllende Bedingung hinterlegt ist. Jeder sichtbare Datentyp wird durch den Lambda-Ausdruck geprüft. Liefert der Lambda-Ausdruck true zurück, so wird später der jeweilige Datentyp berücksichtigt.

RegistrationBuilder builder = new RegistrationBuilder();
PartBuilder partBuilder = builder.ForTypesMatching(t => t.Name.StartsWith("Car"));

Es wird immer der konkrete Datentyp in Betracht gezogen. Basisklassen oder Schnittstellen werden hierbei nicht berücksichtigt.

Parts konfigurieren

Jede der drei zuvor gezeigten Methoden liefert ein Objekt der Klasse PartBuilder zurück. Dieses wird nun verwendet, um die einzelnen Parts zu konfigurieren. ClassDiagram02

Das Konfigurieren der Parts kann wiederum in zwei Schritte aufgeteilt werden:

  • Exportieren von Klassen, Schnittstellen oder Eigenschaften, optional mit Metadaten
  • Importieren von Eigenschaften, Konstruktoren oder Konstruktorparametern

Bei den folgenden Beispielen habe ich die Attribute, die durch die API-Aufrufe ersetzt werden, mit angegeben (aber auskommentiert).

Exportieren

Der einfachste Fall ist das Exportieren der unterlagerten Klasse. Die Methode PartBuilder.Export() ersetzt hierbei das Attribut [Export].

// [Export]
public class CarBMW
{
   // ...
}

RegistrationBuilder builder = new RegistrationBuilder();
PartBuilder partBuilder = builder.ForType<CarBMW>();
partBuilder.Export();

// oder alternativ

RegistrationBuilder builder = new RegistrationBuilder();
PartBuilder partBuilder = builder.ForType<CarBMW>().Export();

Implementiert der Export eine bestimmte Schnittstelle, so kann diese für den Export verwendet werden.

// [Export(typeof(ICarContract))]
public class CarBMW : ICarContract
{
   // ...
}

RegistrationBuilder builder = new RegistrationBuilder();
PartBuilder partBuilder = builder.ForType<CarBMW>()
                                 .Export<ICarContract>();

Es können auch mehrere Schnittstellen exportiert werden.

// [Export(typeof(ICarContract))]
// [Export(typeof(ICarFoo))]
public class CarBMW : ICarContract, ICarFoo
{
    // ...
}

RegistrationBuilder builder = new RegistrationBuilder();
PartBuilder partBuilder = builder.ForType<CarBMW>()
                                 .Export<ICarContract>()
                                 .Export<ICarFoo>();

Die Methode ExportInterfaces() exportiert alle implementierten Schnittstellen.

// [Export(typeof(ICarContract))]
// [Export(typeof(ICarFoo))]
public class CarBMW : ICarContract, ICarFoo
{
    // ...
}

RegistrationBuilder builder = new RegistrationBuilder();
PartBuilder partBuilder = builder.ForType<CarBMW>()
                                 .ExportInterfaces();

Durch ein Lambda-Ausdruck kann auf einfache Art ein Filter definiert werden. Für jede Schnittstelle wird der Lambda-Ausdruck aufgerufen. Die Variable t ist ein Objekt der Klasse Type. Wird ein true zurückgeliefert, so wird die entsprechende Schnittstelle exportiert.

PartBuilder partBuilder = builder.ForType<CarBMW>()
                   .ExportInterfaces(t => t.Name.EndsWith("Foo"));

Auf gleiche Weise lassen sich auch Eigenschaften exportieren. Als Parameter wird an den Lambda-Ausdruck ein Objekt der Klasse PropertyInfo übergeben.

public class CarBMW
{
   // [Export]
   public string CarName { get; set; }

   // ...
}

RegistrationBuilder builder = new RegistrationBuilder();
PartBuilder partBuilder = builder.ForType<CarBMW>()
                .ExportProperties(pi => pi.PropertyType == typeof(string));

Metadaten lassen sich ebenfalls hinzufügen. Hier muss unterschieden werden zwischen “Export Metadata”, die über die Methode ExportBilder.AddMetadata() hinzugefügt werden (diese entsprechen dem Attribut [ExportMetadata]) und den “Part Metadata”, die per PartBuilder.AddMetadata() erzeugt werden.

Das folgende Beispiel legt zwei ”Export Metadata” an. Hierzu besitzt die Methode Export() die notwendige Überladung. In dem Lambda-Ausdruck ist die Variable eb vom Typ ExportBuilder.

// [ExportMetadata("Name", "MyCar")]
// [ExportMetadata("Price", (uint)1000)]
// [Export(typeof(ICarContract))]
public class CarBMW : ICarContract
{
    // ...
}

RegistrationBuilder builder = new RegistrationBuilder();
PartBuilder partBuilder = builder.ForType<CarBMW>()
     .Export<ICarContract> (eb =>
           {
               eb.AddMetadata("Name", "MyCar");
               eb.AddMetadata("Price", (uint)1000);
           });

Vergleichbare Überladungen bieten auch die Methoden ExportProperties() und ExportInterfaces() an.

Der zweite Parameter von AddMetadata() kann als Lambda-Ausdruck angegeben werden. Dadurch lassen sich recht kompakt die einzelnen Werte der Metadaten zur Laufzeit berechnen.

PartBuilder partBuilder = builder.ForType<CarBMW>()
     .Export<ICarContract>(eb =>
     {
        eb.AddMetadata("Name",
                 t =>
                 {
                    return (t == typeof(CarBMW)) ? "myBMW" : "Mercedes";
                 });
        eb.AddMetadata("Price",
                 t =>
                 {
                    return (t == typeof(CarBMW)) ? (uint)1000 : (uint)1500;
                 });
     });

Erstellungsrichtinien können bei den Exports mit der Methode SetCreationPolicy() festgelegt werden.

// [Export(typeof(ICarContract))]
// [PartCreationPolicy(CreationPolicy.NonShared)]
public class CarMercedes : ICarContract
{
   // ...
}

PartBuilder partBuilder = builder.ForType<CarMercedes>()
                   .Export<ICarContract>()
                   .SetCreationPolicy(CreationPolicy.NonShared);
Importieren

Neben den Exporten können auch die Importe festgelegt werden. Importe beziehen sich auf Eigenschaften oder auf Parameter von Konstruktoren (Constructor-Injection).

Beispiel (zum Vergleich habe ich die entsprechenden Attribute mit angegeben, aber auskommentiert):

public interface ICarContract
{
     string DoSomething();
}

// [Export(typeof(ICarContract))]
public class CarBMW : ICarContract
{
     public string DoSomething()
     {
         return "CarBMW";
     }
}

// [Export(typeof(ICarContract))]
public class CarMercedes : ICarContract
{
     public string DoSomething()
     {
         return "CarMercedes";
     }
}

// [Export]
public class CarHost
{
     // [ImportMany]
     public ICarContract[] CarParts { set; get; }
}

Zuerst die notwendigen Aufrufe für die Exporte:

RegistrationBuilder builder = new RegistrationBuilder();

builder.ForTypesDerivedFrom<ICarContract>()
     .Export<ICarContract>();

builder.ForType<CarHost>()
     .Export();

var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly(), builder);
var container = new CompositionContainer(catalog);

var carHost = container.GetExportedValue<CarHost>();

Der Aufruf von container.GetExportedValue<CarHost>() liefert zwar das exportierte Objekt der Klasse CarHost zurück, die Eigenschaft CarParts ist aber weiterhin null.

Damit sich dieses ändert, muss die Eigenschaft CarParts der Klasse CarHost importiert werden. Die notwendigen Methoden für das Importieren sind mit denen für das Exportieren vergleichbar.

builder.ForType<CarHost>()
       .Export()
       .ImportProperties(pi => pi.Name == "CarParts");

Somit werden jetzt beide Klassen (CarBMW und CarMercedes) in die Eigenschaft CarParts der Klasse CarHost importiert.

RegistrationBuilder builder = new RegistrationBuilder();

builder.ForTypesDerivedFrom<ICarContract>()
       .Export<ICarContract>();

builder.ForType<CarHost>()
       .Export()
       .ImportProperties(pi => pi.Name == "CarParts");

var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly(), builder);
var container = new CompositionContainer(catalog);

var carHost = container.GetExportedValue<CarHost>();

foreach (ICarContract carParts in carHost.CarParts)
     Console.WriteLine(carParts.DoSomething());

An die Methode ImportProperties() kann ein zweiter Parameter, vom Typ Action<PropertyInfo, ImportBuilder>, übergeben werden. Über diesen Delegate können weitere Einstellungen für den Import vorgenommen werden.

// [ImportMany(typeof(CarHost),
//             AllowRecomposition = true)]

builder.ForType<CarHost>()
       .Export()
       .ImportProperties(pi => pi.Name == "CarParts",
                        (pi, ib) => ib.AllowRecomposition());

Oder auch:

// [ImportMany(typeof(CarHost),
//             RequiredCreationPolicy = CreationPolicy.NonShared)]

builder.ForType<CarHost>()
       .Export()
       .ImportProperties(pi => pi.Name == "CarParts",
          (pi, ib) => ib.RequiredCreationPolicy(CreationPolicy.NonShared));

Mehrere Optionen können auch miteinander kombiniert werden:

// [ImportMany(typeof(CarHost),
//             AllowRecomposition = true),
//             RequiredCreationPolicy = CreationPolicy.NonShared)]

builder.ForType<CarHost>()
     .Export()
     .ImportProperties(pi => pi.Name == "CarParts",
                   (pi, ib) =>
                   {
                       ib.AllowRecomposition();
                       ib.RequiredCreationPolicy(CreationPolicy.NonShared);
                   });

Wurde eine Eigenschaft mit Hilfe der Klasse PartBuilder importiert, so ist dieses gleichzusetzen mit dem Attribut [ImportMany]. Wird ein Verhalten gewünscht, das mit dem Attribut [Import] vergleichbar ist, so muss dieses explizit angegeben werden.

// [Export]
public class CarHost
{
   // [Import]
   public ICarContract[] CarParts { set; get; }
}

builder.ForType<CarHost>()
       .Export()
       .ImportProperties(pi => pi.Name == "CarParts",
                        (pi, ib) => ib.AsMany(false));

Ein wichtiger Punkt ist das Importieren von Parametern über Konstruktoren (Constructor-Injection).

Die Klasse Host wird für das Beispiel mit einem Konstruktor erweitert. Der Parameter des Konstruktors soll als Import dienen.

[Export]
public class CarHost
{
   private ICarContract[] carParts = null;

   [ImportingConstructor]
   public CarHost([ImportMany]ICarContract[] carParts)
   {
      this.carParts = carParts;
   }
}

Soll auf die Attribute verzichtet werden, so muss mit der Methode PartBuilder.SelectConstructor() der gewünschte Konstruktor angegeben werden.

builder.ForType<CarHost>()
       .Export()
       .SelectConstructor(ctors => ctors.First(ci => ci.GetParameters().Length == 1),
                         (pi, ib) => ib.AsMany(true));

Auch hierbei helfen Delegates beim Aussuchen des gewünschten Konstruktors. Ich habe hier als Auswahlkriterium die Anzahl der Parameter benutzt. Wird kein Filter definiert, so wird der Konstruktor mit den meisten Parametern berücksichtigt.

Wichtig bei diesem Beispiel ist der Aufruf von AsMany(true). Anders als bei Imports auf Eigenschaften, wird bei Imports auf Konstruktor-Parameter diese Eigenschaft implizit auf false gesetzt.

Das Beispiel 3 (Visual Studio 2012) auf GitHub wendet die meisten, oben gezeigten API-Aufrufe an. Dabei kommen Metadaten, Erstellungsrichtlinien und Constructor-Injections zum Einsatz.

Anmerkungen:

Es ist durchaus erlaubt, mehrere Regeln zu definieren. Sollten Überschneidungen auftreten, so werden diese additiv behandelt.

Der deklarative Ansatz lässt sich mit den API-Aufrufen kombinieren. Hierbei haben die Attribute immer Vorrang. Ein sinnvoller Einsatz ist z.B. das Attribut [PartNotDiscoverable] auf Klassenebene. Hierdurch wird das Exportieren verhindert, auch dann, wenn per Registration-Builder eigentlich die Klasse berücksichtigt werden müsste. Das kann z.B. beim Debuggen hilfreich sein.

Instanziierung der Exports durch Scopes begrenzen

Bei komplexen Anwendungen können die Kompensationen schnell umfangreiche Ausmaße annehmen. Sollen die einzelnen Objekte unterschiedliche Life-cycles besitzen, so kann dieses mit Hilfe sogenannter Scopes realisiert werden. Abgebildet werden diese durch Objekte der Klasse CompositionScopeDefinition. Mehrere Objekte lassen sich zu einer Hierarchie zusammenstellen. Hierzu dient der zweite Parameter des Konstruktors. Während der erste Parameter den ComposablePartCatalog entgegen nimmt, enthält der zweite Parameter eine Auflistung von weiteren Scope Definitionen.

var managerCatalog = new TypeCatalog(typeof(CarManager));
var partCatalog = new TypeCatalog(typeof(CarHost),
                                  typeof(CarMercedes),
                                  typeof(CarBMW),
                                  typeof(CarGarage));

var scope = new CompositionScopeDefinition(
             managerCatalog,
             new[] { new CompositionScopeDefinition(partCatalog, null) });

Da die Klasse CompositionScopeDefinition von ComposablePartCatalog abgeleitet wurde, kann diese zur Erzeugung eines Containers genutzt werden.

var container = new CompositionContainer(scope);

Als Beispiel soll das folgende Objektmodell erstellt werden. Der CarManager liegt in einem eigenen Scope. Über diesen können beliebige Objektbäume, bestehend aus dem CarHost-Objekt, den eigentlichen Car-Objekten und einem CarGarage-Objekt, erstellt werden.

Diagram01

Zuerst werden die einzelnen Klassen mit den notwendigen Attributen erstellt:

public interface ICarContract
{
    string DoSomething();
}

[Export(typeof(ICarContract))]
public class CarBMW : ICarContract
{
    [Import]
    public CarGarage CarGarage { get; set; }

    public string DoSomething()
    {
        // ...
    }
}

[Export(typeof(ICarContract))]
public class CarMercedes : ICarContract
{
    [Import]
    public CarGarage CarGarage { get; set; }
    public string DoSomething()
    {
        // ...
    }
}

[Export]
public class CarGarage
{
     // ...
}

[Export]
public class CarHost
{
    [ImportMany]
    public ICarContract[] CarParts { get; set; }
}

[Export]
public class CarManager
{
    [Import]
    private ExportFactory<CarHost> carHostFactory = null;
    public ExportLifetimeContext<CarHost> CreateCarHost()
    {
        ExportLifetimeContext<CarHost> carHostContext = carHostFactory.CreateExport();
        return carHostContext;
     }
}

Nach dem Anlegen der Cataloges, Scopes und dem Container wird das CarManager-Objekt abgefragt (Zeile 11).

var managerCatalog = new TypeCatalog(typeof(CarManager));
var partCatalog = new TypeCatalog(typeof(CarHost),
                                  typeof(CarMercedes),
                                  typeof(CarBMW),
                                  typeof(CarGarage));
var scope = new CompositionScopeDefinition(
               managerCatalog,
               new[] { new CompositionScopeDefinition(partCatalog, null) });
var container = new CompositionContainer(scope);

var carManager = container.GetExportedValue<CarManager>();

ExportLifetimeContext<CarHost> carHostContextA = carManager.CreateCarHost();
ExportLifetimeContext<CarHost> carHostContextB = carManager.CreateCarHost();

foreach (ICarContract carParts in carHostContextA.Value.CarParts)
    Console.WriteLine(carParts.DoSomething());
carHostContextA.Dispose();

foreach (ICarContract carParts in carHostContextB.Value.CarParts)
    Console.WriteLine(carParts.DoSomething());
carHostContextB.Dispose();

Über den CarManager werden zwei unabhängige CarHost-Objekte erzeugt. MEF hat den gewünschten Objektbaum aus CarHost, CarBMW, CarMercedes und CarGarage angelegt. Da die Objektbäume mit der Klasse ExportFactory erzeugt wurden, können diese durch die Methode Dispose() auch jederzeit und unabhängig voneinander gelöscht werden.

In dem beigefügten Beispielprogramm lasse ich von den einzelnen Objekten den Hash-Code ausgeben. Dadurch kann sehr gut beobachtet werden, wie viele Objekte von jeder Klasse angelegt werden.

Beispiel 4 (VisualStudio 2012) auf GitHub

verbesserte Fehlerdiagnose

Wer mit MEF bisher produktiv gearbeitet hat, wird sich sicherlich schon über die wenig aussagekräftigen Meldungen der Exceptions geärgert haben. Das Problem wurde scheinbar erkannt, denn mit dem MEF von .NET 4.5 sind die Texte der Exceptions deutlich hilfreicher. Hier ein Beispiel (zu einem Import wurden keine entsprechenden Exports gefunden):

Picture01

Auch sollte man immer die entsprechenden Exceptions wie CompositionException, CompositionContractMismatchException oder ChangeRejectedException benutzen. Diese enthalten weitere, hilfreiche Informationen.

Neu hinzugekommen ist mit .NET 4.5 die Aufzählung CompositionOptions, die an den Konstruktur der Klasse CompositionContainer übergeben werden kann.

Default keine besonderen Optionen werden benutzt.
DisableSilentRejection sämtliche Kompensationsprobleme erzeugen einen Fehler.
IsThreadSafe Der Composition-Container bzw. Export-Provider sollte thread safe sein.
ExportCompositionService Der Composition-Container bzw. Export-Provider ist ein Export Composition Service.

Anmerkung: Es existieren leider sehr wenige Informationen zu den Optionen IsThreadSafe und ExportCompositionService. Über weitere Hinweise wäre ich sehr dankbar.

var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
var container = new CompositionContainer(catalog,
                                         CompositionOptions.DisableSilentRejection);

Da die Aufzählung mit dem Attribut FlagsAttribute dekoriert wurde, ist das bitweise kombinieren mehrerer Werte erlaubt (aber nicht immer sinnvoll).

Author: Stefan Henneken

I’m Stefan Henneken, a software developer based in Germany. This blog is just a collection of various articles I want to share, mostly related to Software Development.

5 thoughts on “MEF Teil 11 – Neuerungen unter .NET 4.5”

  1. Vielen vielen dank! Das beste MEF-Tutorial ever.

    Ich habe bitte eine Frage. Ich benutze Prism Library mit MEF. Das heißt, ich habe schon einen Instanz von CompositionContainer und ich möchte zu diesem meine CompositionScopeDefinition hinzufügen. In deinem Beispiel Code übergibst du direkt CompositionScopeDefinition an den Konstruktor. Hier ein Link wie jemand das gelöst hat. Hat leider bei mir nicht funktioniert.

    http://stackoverflow.com/questions/16943121/defining-scope-in-mef-with-compositionscopedefinition

  2. Erstmal Vielen Dank, dieses Tut hat mir bei MEF sehr weiter geholfen. 🙂

    Ich habe eine Frage, wie kann ich folgendes lösen?`

    public class ContentBase : UserControl, IContentBase, IPartImportsSatisfiedNotification{

    [Export] // oder Import???
    public ViewModelBase ViewModel { get; set; }

    public void OnImportsSatisfied() {
    DataContext = ViewModel;
    }
    }

    [Content(“/Home”)] //ViewAttribute:ExportAttribute
    public partial class Home : ContentBase{
    public Home() {
    InitializeComponent();
    }
    }

    [ViewModel(“/Home”)]
    public class HomeViewModel : ViewModelBase {
    public string _home;
    public string HomeName {
    get { return _home; }
    set {
    _home = value;
    OnPropertyChanged(nameof(HomeName));
    }
    }
    }

    ViewModel soll nun das richtige ViewModel (in dem Fall HomeViewModel) erhalten, wann und wo setze ich am besten das ViewModel

    Ich habe eine Klasse
    [Export]
    public class AppContentLoader {

    [ImportMany]
    private Lazy[] Contents { get; set; }

    [ImportMany]
    private Lazy[] ViewModels = null;

    public object LoadContent(Uri uri) {

    var content = (from c in Contents
    where c.Metadata.ContentUri == uri.OriginalString
    select c.Value).FirstOrDefault();

    var vm = (from v in ViewModels
    where v.Metadata.ContentUri==uri.OriginalString
    select v.Value).FirstOrDefault();

    return content ?? base.LoadContent(uri);
    }
    }

    Bin für jeden Tipp dankbar.

  3. Prima Tutorial, hat mir sehr weiter geholfen.
    Eine Frage, wie kann ich folgendes lösen:
    in meiner Basisklasse “ContentBase” habe ich ein Property
    public ViewModelBase ViewModel { get; set; }

    Wie kann ich nun diesem Property mit dem richtigen abgeleiteten ViewModel zuweisen

    public class ContentBase : UserControl, IContent, IPartImportsSatisfiedNotification {

    [Export]
    public ViewModelBase ViewModel { get; set; }

    public void OnImportsSatisfied() {
    DataContext = ViewModel;
    }
    }

    [Content(“/Home”)]
    public partial class Home : ContentBase{
    public Home() {
    InitializeComponent();
    }
    }

    [ViewModel(“/Home”)]
    public class HomeViewModel : ViewModelBase {
    }

    [Export]
    public class AppContentLoader {

    [ImportMany]
    private Lazy[] Contents { get; set; }

    [ImportMany]
    private Lazy[] ViewModels = null;

    public object LoadContent(Uri uri) {

    //uri.OriginalString = “/Home”
    var content = (from c in Contents
    where c.Metadata.ContentUri == uri.OriginalString
    select c.Value).FirstOrDefault();

    var vm = (from v in ViewModels
    where v.Metadata.ContentUri==uri.OriginalString
    select v.Value).FirstOrDefault();

    return content ?? base.LoadContent(uri);
    }
    }

    Wo und wann setze ich am besten das richtige ViewModel?

    Bin für jeden Tipp dankbar. 🙂

  4. Ich kann mich den Feedbacks nur anschließen. Sehr ausfrühlich und verständlich. Richtig klasse. Daum hoch. Kannst du mir sagen ob sich seitdem du den letzten Teil geschrieben hast, noch was an Neuerungen dazu kommen ist. Und ist MEF immer noch Stand der Technik mit zu arbeiten. Wir arbeiten mit .Net Framework 4.8.

    1. Danke für das positive Feedback. Bei MEF muss man mittlerweile unterscheiden zwischen den MEF für das .NET Framework und MEF für .NET Core bzw. .NET Standard. Bei MEF für das .NET Framework hat sich in den letzten drei Jahren eigentlich nichts mehr getan. Bei Microsoft liegt der Fokus auch hier auf .NET Core und .NET Standard. Der folgende Artikel gibt hier einen recht guten Überblick:
      https://blog.softwarepotential.com/porting-to-net-standard-2-0-part-2-porting-mef-1-0-to-mef-2-0-on-net-core

Leave a comment