Anwendungen auf mehrere Komponenten zu verteilen ist mittlerweile ein notwendiger Standard geworden. Jeder Entwickler, der solch eine Infrastruktur zur dynamischen Erweiterung entworfen hat, weiß dass die Implementierung sehr aufwendig werden kann. Das Managed Extensibility Framework (MEF) verspricht Abhilfe. Eine kurze Einführung soll dieses verdeutlichen.
kurzer Rückblick
Seit dem .NET Framework 3.5 gibt es das Managed AddIn Framework (MAF), das hier dem Entwickler die Arbeit vereinfachen sollte. Zu finden ist das MAF im Namespace System.AddIn. Bei dem MAF werden Hauptanwendung und Erweiterungen sehr gut voneinander getrennt. Beide laufen in eigenständige Application Domains. Meine ersten Erfahrungen mit dem MAF waren allerdings etwas enttäuschend. Die Lernkurve bei MAF ist sehr flach. Auch ist es um MAF etwas ruhig geworden. Schaut man sich den Team-Blog von Microsoft an, so sind die letzten Einträge von 2008. Tools für Visual Studio, die die Implementierung vereinfachen sollen, wirken noch nicht ganz ausgereift und werden scheinbar auch nicht weiterentwickelt.
das Managed Extensibility Framework
Als mit dem .NET Framework 4 das Managed Extensibility Framework (MEF) angekündigt wurde, habe ich meine Aktivitäten zu MAF erst mal zur Seite gelegt. Zur Erklärung soll Schritt für Schritt eine kleine Konsolen-Anwendung erstellt werden, die dynamisch mit Komponenten erweitert werden kann. Jede Komponente ist eine einzelne DLL, in der bestimmte Funktionen hinterlegt sind.
In einer realen Applikation könnten das z.B. bestimmte Berechnungen sein. Von Kunde zu Kunde können aber genau diese Berechnungen unterschiedlich ausfallen. Damit nicht für jede Berechnungsvariante das gesamte Programm kompiliert werden muss, bietet es sich an, die
Berechnungen auszulagern. Das Basisprogramm bleibt bei allen Kunden gleich, nur die Komponenten mit den Berechnungen werden einzeln angepasst. Vorstellbar sind auch Reportfunktionen, die dem Hauptprogramm separat zur Verfügung gestellt werden. Somit lassen sich spezielle Reports erstellen, ohne das gesamte Programm anzupassen.
ein einfaches Beispiel mit MEF
Unser Beispiel soll alle vorhandenen DLLs im aktuellen Verzeichnis nach einem bestimmten Interface durchsuchen. Besitzt eine DLL das geforderte Interface, so soll diese geladen und mit der Hauptanwendung verbunden werden. Anschließend wird von jeder Komponente die erwartete Methode aufgerufen. Die Methode besitzt als Parameter nur einen String. Dieser String wird erweitert und als Rückgabeparameter an die Hauptanwendung zurückgegeben. Konkret geht es in dem Beispiel um Automarken. Jede Komponente repräsentiert eine Marke. Die Hauptanwendung, der Host, lädt alle gefundenen Komponenten und ruft über das gemeinsame Interface die Methode auf.
Als erstes wird das gemeinsame Interface festgelegt, das alle Komponenten implementieren müssen:
using System; namespace CarHost { public interface ICarContract { string StartEngine(string name); } }
Die Schnittstelle wird in einem separaten Projekt gespeichert, so dass es als einzelne DLL allen anderen Komponenten zur Verfügung steht. Host und AddIns sehen jeweils nur den Contract (und besitzen auch nur eine Referenz auf diese).
Als nächstes folgt die erste Komponente, die von der Hauptanwendung dynamisch hinzugeladen werden soll. Die Klasse Mercedes implementiert das Interface ICarContract. Damit das Interface genutzt werden kann, muss eine Referenz auf die DLL mit dem Interface dem Projekt hinzugefügt werden. Desweiteren wird die Klasse mit dem Attribut Export dekoriert. Damit dieses Attribut genutzt werden kann, muss dem Projekt eine Referenz auf System.ComponentModel.Composition hinzugefügt werden. Das Attribut besitzt mehrere Konstruktoren. Einer davon erwartet als Parameter einen Type. Der Type bezeichnet einen Contract. Dadurch wird die Klasse, die exportiert werden soll, einem eindeutigen Contract zugeordnet.
using System; using System.ComponentModel.Composition; namespace CarMercedes { [Export(typeof(CarHost.ICarContract))] public class Mercedes : CarHost.ICarContract { public string StartEngine(string name) { return String.Format("{0} starts the Mercedes.", name); } } }
Analog dazu die Klasse BMW. Sie implementiert ebenfalls ICarContract und enthält auch das Export-Attribut.
using System; using System.ComponentModel.Composition; namespace CarBMW { [Export(typeof(CarHost.ICarContract))] public class BMW : CarHost.ICarContract { public string StartEngine(string name) { return String.Format("{0} starts the BMW.", name); } } }
Kommen wir nun zu der Hauptanwendung, dem Host. Die Aufgabe vom Host besteht darin, im aktuellen Projektverzeichnis nach Komponenten zu suchen, die das Interface ICarContract implementiert haben. Diese Komponenten sollen geladen und dem Host zugänglich gemacht werden.
Erst mal muss dem Projekt eine Referenz auf System.ComponentModel.Composition und ICarContract hinzugefügt werden. In dem Array cars sollen alle geladenen Komponenten aufgelistet werden. Hierzu wird das Array mit dem Attribut ImportMany dekoriert. Als Parameter wird wieder der gemeinsame Contract übergeben.
using System; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; namespace CarHost { class Program { [ImportMany(typeof(ICarContract))] private ICarContract[] cars = null; static void Main(string[] args) { new Program().Run(); } void Run() { var catalog = new DirectoryCatalog("."); var container = new CompositionContainer(catalog); container.ComposeParts(this); foreach (ICarContract contract in cars) Console.WriteLine(contract.StartEngine("Sebastian")); } } }
Das Laden und Binden geschieht in der Methode Run(). Von zentraler Bedeutung sind bei MEF die Klassen ComposablePartCatalog und CompositionContainer. Der Catalog kontrolliert das Laden der Komponenten, während der CompositionContainer die Instanzen erzeugt und diese an die entsprechenden Variablen bindet. Von ComposablePartCatalog gibt es mehrere Ableitungen; eine davon ist DirectoryCatalog. Diese Klasse durchsucht ein Verzeichnis und lädt alle Komponenten, die mit Import/ImportMany oder Export dekoriert sind. Diese Elemente werden auch als Composable Part bezeichnet. Der Container bekommt über den Konstruktor den Catalog zugewiesen. Mit der Methode ComposeParts() wird das Instanzieren und Binden gestartet. Anschließend sind in dem Array die Referenzen auf die gefundenen Komponenten enthalten. MEF sorgt dafür, dass nur jene geladen und gebunden werden, die auch den Contract erfüllen.
Bei der Ausführung wird das erwartete Ergebnis geliefert. Es ist auch gut zu erkennen, aus welchen Komponenten unser Testprojekt besteht. Zentraler Mittelpunkt stellt unser Host da; CarHost.exe. Des weiteren haben wir die Datei CarContract.dll mit dem gemeinsamen Contract, unserem Interface. BMW.dll und Mercedes.dll sind die Komponenten, die dynamisch zur Laufzeit geladen und gebunden werden.
Beispiel 1 (Visual Studio 2010) auf GitHub
Dieses einfache Beispiel zeigt schon deutlich, dass mit drei Zeilen Code und zwei Attributen ein Programm realisiert werden kann, das in der Lage ist, Komponenten dynamisch nachzuladen. Eine eigene Implementierung ‘per Hand’ wäre sicherlich um ein vielfaches umfangreicher geworden. Auch ist gut zu beobachten, das kein new-Operator vorhanden ist, der die jeweiligen Instanzen der Komponenten anlegt. Es besteht eine ‘lose Koppelung’ zwischen beiden Programmteilen. Das OO-Pattern Open/Closed und Design by Contract werden somit ebenfalls erfüllt.
Export/Import von Methoden
Ein Composable Part muss nicht unbedingt eine Klasse sein. Möglich sind auch Methoden, Eigenschaften und Felder. Das folgende Beispiel bindet Methoden zueinander. Hierzu wird die Methode mit dem Attribut Export dekoriert. Der Contract wurde hierbei mit einem String definiert. Da später die Methode über ein Delegate aufgerufen wird, kann das Interface ICarContract entfallen.
using System; using System.ComponentModel.Composition; namespace CarBMW { public class BMW { [Export("CarContract")] public string StartEngine(string name) { return String.Format("{0} starts the BMW.", name); } } }
Im Hauptprogramm wird ein Array vom Delegate Func<T, TResult> angelegt. Das Delegate ist im Namespace System definiert. Über das Delegate kann jetzt die jeweilige Methode aus den hinzugeladenen Komponenten aufgerufen werden.
using System; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; namespace CarHost { class Program { [ImportMany("CarContract")] private Func<string, string>[] startCars = null; static void Main(string[] args) { new Program().Run(); } void Run() { var catalog = new DirectoryCatalog("."); var container = new CompositionContainer(catalog); container.ComposeParts(this); foreach (Func<string, string> start in startCars) Console.WriteLine(start("Sebastian")); } } }
Alles andere entspricht dem ersten Beispiel.
Beispiel 2 (Visual Studio 2010) auf GitHub
Importtypen
expliziter Vertragsname
Bei dem folgenden Beispiel wird bei den Attributen Import und Export der Vertragsname direkt angegeben. Unter bestimmten Umständen ist es durchaus sinnvoll, den Vertragsnamen direkt anzugeben. Das kann der Fall sein, wenn eine Klasse mehrere Werte exportiert, die gemeinsam den gleichen Typ verwenden.
public class ClassA { [Import("MajorRevision")] public int MajorRevision; } public class ClassB { [Export("MajorRevision")] public int MajorRevision = 2; [Export("MinorRevision")] public int MinorRevision = 9; }
Auch wenn der Vertragsname direkt angegeben wird, so muss der Vertragstyp zwischen Import und Export exakt übereinstimmen. Wäre die Variable MajorRevision in ClassB vom Typ byte, so würde das Binding zwischen Import und Export nicht zustande kommen.
dynamische Importe
Mit Hilfe eines dynamischen Imports kann das Binden flexibler gestaltet werden. In diesem Fall muss der genaue Datentyp beim Import nicht angegeben werden. Stattdessen wird das Schlüsselwort dynamic angegeben.
[ImportMany("MyCarContract")] private dynamic[] cars = null;
In diesem Fall sollte auch immer mit einem expliziten Vertragsnamen gearbeitet werden. Dieser Vertragsname muss auch beim Export angegeben werden.
[Export("MyCarContract")] public class BMW : CarHost.ICarContract
Es versteht sich von alleine, dass die importierende Klasse so implementiert werden muss, das es zu keinen Laufzeitfehlern kommt. Folgendes Beispiel lässt sich zwar übersetzen, erzeugt bei der Ausführung aber eine Exception.
foreach (var car in cars) Console.WriteLine(car.StopEngine("Sebastian"));
Die Methode StopEngine() ist in der importierten Klasse nicht vorhanden. Es kommt zu einem Laufzeitfehler. Von daher ist die Verwendung des Schlüsselwortes dynamic nicht ohne Gefahren. Viele Fehler machen sich erst zur Laufzeit bemerkbar.
verzögerte Importe
Seit dem .NET Framework 4 wird die Klasse Lazy<T> mitgeliefert. Diese Klasse kann eingesetzt werden, wenn ein Objekt erst beim ersten Zugriff initialisiert werden soll.
[ImportMany(typeof(ICarContract))] private Lazy<ICarContract>[] cars = null;
Soll auf das Objekt zugegriffen werden, so muss von der Klasse Lazy<T> die Eigenschaft Value benutzt werden. Die foreach-Schleife sieht wie folgt aus:
foreach (Lazy<ICarContract> car in cars) Console.WriteLine(car.Value.StartEngine("Sebastian"));
Sehr gut ist die Arbeitsweise zu erkennen, wenn man in den Klassen BMW und Mercedes einen Standard-Konstruktor einbaut und die foreach-Schleife aus der Hauptanwendung entfernt. Man wird dann feststellen, dass die Konstruktoren nicht aufgerufen werden. Erst wenn die foreach-Schleife wieder eingebaut wird, werden auch die Konstruktoren aufgerufen.
Parameterübergabe per Konstruktor
MEF benutzt zum Instanzieren von Komponenten immer den Standardkonstruktor. Soll ein anderer Konstruktor verwendet werden, so muss dieser mit dem Attribut ImportingConstructor dekoriert werden. Es darf nur ein Konstruktor das Attribut ImportingConstructor besitzen. Besitzen mehrere Konstruktoren das Attribut, kommt es zu einem Laufzeitfehler. Ebenfalls tritt ein Laufzeitfehler auf, wenn es keinen Standardkonstruktor gibt und kein Konstruktor mit dem Attribut ImportingConstructor dekoriert wurde. Interessant ist hier, dass die Konstruktoren ohne Probleme als private deklariert werden können.
Alle Parameter des Konstruktors werden automatisch als Importe deklariert. Somit muss es in dem Host nur noch einen passenden Export geben.
using System; using System.ComponentModel.Composition; namespace CarBMW { [Export(typeof(CarHost.ICarContract))] public class BMW : CarHost.ICarContract { [ImportingConstructor] private BMW(int parameter) { Console.WriteLine(String.Format("Parameter: {0}.", parameter)); } public string StartEngine(string name) { return String.Format("{0} starts the BMW.", name); } } } using System; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; namespace CarHost { class Program { [Import(typeof(ICarContract))] private Lazy<ICarContract> carPart { get; set; } [Export] private int Parameter { get; set; } static void Main(string[] args) { new Program().Run(); } void Run() { var catalog = new DirectoryCatalog("."); var container = new CompositionContainer(catalog); this.Parameter = 10; container.ComposeParts(this); Console.WriteLine(carPart.Value.StartEngine("Sebastian")); container.Dispose(); } } }
Man kann (und man sollte auch) hier mit Vertragsnamen arbeiten. Spätestens wenn mehrere Parameter vom gleichen Typ vorhanden sind, ist dieses notwendig. Im Konstruktor wird hierzu das Attribut Import explizit vor jedem Parameter angegeben.
[ImportingConstructor] private BMW([Import("ConstructorParameter")]int parameter) { Console.WriteLine(String.Format("Parameter: {0}.", parameter)); }
Das Attribut Export im Host muss natürlich den gleichen Vertragsnamen erhalten.
[Export("ConstructorParameter")] private int Parameter { get; set; }
Beispiel 3 (Visual Studio 2010) auf GitHub
optionale Importe
Es wird immer davon ausgegangen, dass zu jedem Import auch ein passender Export vorhanden ist. Ist dieses nicht der Fall, wird bei der Methode ComposeParts() eine Exception ausgelöst. Soll dieses Verhalten geändert werden, so kann bei dem Import-Attribut die Option AllowDefault angegeben werden.
[Import(typeof(ICarContract), AllowDefault = true)] private ICarContract carPart = null;
Ist kein passender Export vorhanden, so wird durch den Aufruf von ComposeParts() keine Exception ausgelöst und die Variable carPart bleibt auf Null.
Diese Option ist nicht bei dem Attribut ImportMany vorhanden. Dort wird es auch nicht benötigt. Gibt es keinen passenden Export zu ImportMany, so erzeugt ComposeParts() eine leere Liste.
Export verhindern
Damit ein Export an einen Import gebunden werden kann, muss der Vertragsname und der Typ übereinstimmen. Es gibt aber noch zwei weitere Gründe, die ein Binden verhindern.
Steht das Attribut Export an einer abstrakten Klasse, so wird der Export nicht zur Verfügung gestellt.
[Export] public abstract class Mercedes { // ... }
Zusätzlich kann aber auch mit dem Attribut PartNotDiscoverable der Export verhindert werden.
[PartNotDiscoverable] [Export] public class Mercedes { // ... }
Bis hierhin konnten nur die wichtigsten Grundlagen von MEF aufgezeigt werden. Das Potential ist aber bei weitem noch nicht ausgeschöpft. Deshalb geht es im 2. Teil weiter mit den Metadaten und den Erstellungsrichtlinien.
Sehr guter Beitrag Steffan!
Setzte MEF auch schon seit einiger Zeit ein. Doch leider habe ich noch nicht die Zeit gehabt mich eingehend damit zu beschäftigen.
Dein Beitrag hat mir noch einige Kniffs und Tricks gezeigt.
Danke und weiter so!
Eine Frage habe ich da noch.
Zerstört die Klasse Lazy auch das Objekt wieder?
Grüße
Hi,
um den Lifecycle der Objekte geht es im 3. Teil. Ich rechne damit, dass ich den Beitrag bis Ende KW24 posten werde.
Stefan
Ich habe Komponenten in bereits kompilierten Assemblies die bestimmte Schnittstellen implementieren, die aber kein Export-Attribute (und auch sonst keinen Verweis auf MEF haben).
Kann ich diese trotzdem mit MEF benutzen? Wie?
Hi,
eine Methode, wie solche Assemblies direkt mit MEF verwendet werden können, ist mir nicht bekannt. Ich würde versuchen,um die Assemblies einen Wrapper zu bauen,
Und es geht doch. ExportProvider ist das Stichwort. Hierzu wird es in Kürze einen entsprechenden Artikel geben.
Ich habe als Anfänger versucht Deine Anleitung durchzuführen. Leider scheitere ich schon beim Erstellen des Projekts, da die Namespaces bei Program.cs und beim Interface gleich sind.
Beim erstellen der Klassenbibliothek für das Interface kann ich das Projekt beim erstellen erst nach dem es angelegt wurde umstellen.
Wenn ich es jetzt versuche es aus der IDE auszuführen, funktioniert es nicht. Bei Deinem Beispiel schon. Ich versuche den Fehler herauszufinden, allerdings finde ich nichts.
Wohl wird ein Ordner public angelegt aus dem ich die CarHost.exe aufrufen kann und es auch funktioniert ABER es fehlt die CarContract.dll.
Was mache ich beim erstellen eines neuen Projekts falsch?
Hallo Alex,
du solltest beachten, das die einzelnen Beispiele aus mehreren Assemblies bestehen. Am besten schaust du dir die fertigen Beispiele an. Die Links zu den fertigen Visual Studio Solutions findest du ebenfalls in dem Post.
Stefan
Richtig klasse Beitrag, ich freu mich auf die restlichen neun 🙂
Guter Artikel welcher den Einstieg in MEF einfach macht:-) Danke!
Hallo,
das ist wirklich ein super artikel, selten sowas klares und verstaendliches gelesen.
ich habe nur eine frage, es ist ja auch moeglich dass ich nicht in allen dll wie hier die methode startengine aufrufen moechte. gibt es eine moeglichkeit das lazy object auf
eine eigene klasse zu casten oder in einem struct abzulegen, dass ich dann komfortabel
spaeter je nach bedarf auf eines der jeweiligen objecten aus den dlls zugreifen kann?
Oder wie macht man das am geschicktesten?
viele gruesse,
michael
Hallo Michael,
du musst natürlich nicht immer von allen AddIns die Methoden aufrufen. Der Host holt sich immer das AddIn, welches benötigt wird. Die Metadaten lassen sich hierbei recht gut als Filter einsetzen. Nur von diesem AddIn werden die Methoden vom Host genutzt.
Stefan
So alt und immer noch Wunderbar! Ich nutze MEF schon lange für WPF View Kopplung. Nun muss ich mal tiefer rein, um Plungins zu erstellen.
Christoph
Ich starte das Programm, es wird aber nichts angezeigt
Hi,
für welche Version des .NET Frameworks hast du das Programm compiliert?
Stefan