MEF Teil 8 – Eigenen ExportProvider erstellen

Das Managed Extensibility Framework (MEF) kann durch verschiedene Möglichkeiten erweitert werden. Eine Variante sind eigene Export-Provider. Ein Export-Provider macht genau das, was der Name schon aussagt, er stellt Exports der Klasse CompositionContainer zur Verfügung. Wie die Exports gefunden und instanziiert werden, ist komplett unter eigener Kontrolle. Eine weitere Variante ist die Benutzung der Klasse CompositionBatch, die allerdings deutlich weniger Möglichkeiten bietet.

Die Verwendung von MEF ist mehr als einfach. Klassen, Methoden oder Eigenschaften werden mit den Attributen Export und Import dekoriert. Container und Kataloge sorgen dafür, dass die Exports zu den passenden Imports finden. Manchmal ist es aber nötig, diesen Prozess selber zu kontrollieren. Eine davon ist die Verwendung der Klasse CompositionBatch.

Die Klasse CompositionBatch

Die Klasse kann man sich als Liste von ComposableParts vorstellen.

ClassCompositionBatch

Mit den Methoden AddPart() und RemovePart() können Objekte vom Typ ComposablePart hinzugefügt oder gelöscht werden. Es gibt von der Methode AddPart() auch eine Variante, die als Parameter ein Object erwartet. Auf diese Weise lassen sich beliebige Objekte hinzufügen.

class Program
{
    [ImportMany]
    private ICarContract[] CarParts { get; set; }

    static void Main(string[] args)
    {
        new Program().Run();
    }

    void Run()
    {
        var container = new CompositionContainer();
        var batch = new CompositionBatch();
        ComposablePart partHost = batch.AddPart(this);
        ComposablePart partBMW = batch.AddPart(new BMW());
        ComposablePart partMercedes = batch.AddPart(new Mercedes());
        container.Compose(batch);

        foreach (ICarContract carPart in CarParts)
            Console.WriteLine(carPart.GetName());

        container.Dispose();
    }
}

public interface ICarContract
{
    string GetName();
}

[Export(typeof(ICarContract))]
public class Mercedes : ICarContract
{
    public string GetName()
    {
        return "Mercedes";
    }
}

public class BMW : ICarContract
{
    public string GetName()
    {
        return "BMW";
    }
}

Mit der Methode AddPart() wird je eine Instanz von BMW und Mercedes der Klasse CompositionBatch hinzugefügt. Außerdem muss eine Instanz der Klasse Program übergeben werden, da dort der Import definiert ist. Da die Klasse BMW nicht mit dem Attribut Export dekoriert wurde, kann es nicht mit der Eigenschaft CarParts verlinkt werden. Wird das Programm ausgeführt, so ist nur das Objekt der Klasse Mercedes in dem Array CarParts enthalten.

CommandWindowSample01

Beispiel 1 (Visual Studio 2010) auf GitHub

Mit der Klasse CompositionBatch hat man unter Kontrolle, welche ComposableParts genutzt werden und welche eben nicht. Allerdings müssen alle Objekte mit dem Attribute Export dekoriert werden. Noch flexibler ist der Einsatz eines eigenen Export-Providers. Das später folgende Beispiel zeigt, wie auch Klassen berücksichtigt werden können, die nicht mit dem Attribut Export dekoriert wurden. Doch zuvor müssen zum Verständnis einige Klassen genauer betrachtet werden.

Aufbau von MEF

Das Managed Extensibility Framework kann in drei Ebenen aufgeteilt werden. Zum einem gibt es die Klassen, mit der das attributbasierte Programmiermodell umgesetzt wird. Eine weitere Ebene definiert die Container. Die Primitives sind die wichtigsten internen Systemklassen.

LayersOfMEF

Der Container-Layer hat keine Abhängigkeiten zum attributbasierten Programmiermodell, sondern nur zu den Primitives, die direkt vom Container benutzt werden. Dadurch kann das attributbasierte Programmiermodell ausgetauscht werden, ohne dass der Container neu implementiert werden muss.

Die Klasse ComposablePart

Zentraler Bestandteil von MEF ist die Klasse ComposablePart. Über Exports stellen ComposableParts ihre Funktionalitäten anderen Systemen zur Verfügung. Sollen Funktionalitäten anderer Systeme verwendet werden, so geschieht dieses über Imports. Über die Auflistungen ExportDefinitions und ImportDefinitions verwaltet die Klasse ihre Imports und Exports.

ClassComposablePart

Die Klasse ComposablePartDefinition

Vereinfacht gesagt beschreibt die Klasse ComposablePartDefinition ein ComposablePart. ComposablePartDefinition stellt die ImportDefinition und ExportDefinition zur Verfügung. Diese sind notwendig, um mit der Methode CreatePart() ComposableParts zu erzeugen.

ClassComposablePartDefinition

Somit kann die Klasse ComposablePartDefinition als Erzeugerklasse (Factory class) für ComposableParts gesehen werden. Die Eigenschaften ExportDefinitions, ImportDefinitions und Metadata werden beim Anlegen über die Methode CreatePart() an das ComposablePart übergeben.

Die Klasse ImportDefinition

Die Klasse findet Verwendung bei ComposablePart. Die Klasse ComposablePart enthält die Eigenschaft ImportDefinitions die eine Auflistung von ImportDefinition darstellt.

Vereinfacht ausgedrückt definiert die Klasse ImportDefinition einen ‘Filter’ der entscheidet, welche Exports welchem Import zugeordnet werden.

Für jedes Attribut Import wird eine Instanz von ImportDefinition angelegt.

ClassImportDefinition

Drei Eigenschaften sind bei dieser Klasse besonders wichtig.

Eigenschaft Bedeutung
ContractName Der Vertragsname ist immer ein String. Wird bei den Attributen Import oder Export nichts weiter angegeben, so wird der Datentyp der Variable genommen, an der das Attribut steht.
Im Konstruktor der Klasse ImportAttribute und ExportAttribute kann auch direkt ein String angegeben werden ([Export(“MyContractName”)]). Sehr verbreitet ist auch die Variante, im Konstruktor eine Variable vom Typ Type anzugeben. In diesem Fall wird der Fully Qualified Name als Vertragsname benutzt ([Export(typeof(IContract)]). Die Eigenschaft ContractName hat nichts mit dem eigentlichen Datentyp des Exports oder Imports zu tun.
Cardinality ImportCardinality ist ein enum und kann die folgenden Werte haben:
ZeroOrOne: kein oder ein Export wird erwartet.
ExactlyOne: Genau ein Export wird erwartet
ZeroOrMode: kein oder beliebig viele Exports werden erwartet.
Constraint Diese Eigenschaft kann einen Lambda-Ausdruck enthalten. Hier findet die eigentliche Entscheidung statt, ob und welcher Export zurückgegeben wird.

Die Klasse ContractBasedImportDefinition

Die Klasse ContractBasedImportDefinition wird von ImportDefinition abgeleitet. Da bei der Eigenschaft Constraint kein Lambda-Ausdruck verwendet wird, ist die Handhabung in einigen Fällen einfacher.

ClassBasedImportDefinition

Die Klasse ExportDefinition

Wie weiter oben zu sehen ist, wird die Klasse ExportDefinition bei der Eigenschaft Constraint der Klasse ImportDefinition benutzt. Während die Klasse ImportDefinition definiert, welcher Export bei einem Import benötigt wird, definiert die Klasse ExportDefinition den Export an sich.

Ebenfalls findet die Klasse Verwendung bei ComposablePart. Die Klasse ComposablePart enthält die Eigenschaft ExportDefinitions, die eine Auflistung von ExportDefinition darstellt.

Ähnlich der Klasse ImportDefinition wird für jedes Attribut Export eine Instanz von ExportDefinition angelegt.

ClassExportDefinition

Die Klasse ExportDefinition besteht im Wesentlichen aus den Eigenschaften ContractName und Metadata. ContractName beinhaltet den Namen des Schnittstellenvertrages. Der Vertragsname eines Exports muss mit dem eines Imports übereinstimmen. Die Eigenschaft Metadata beinhaltet ein Dictionary mit zusätzlichen Informationen, die andere Anwendungen auswerten können.

Die Klasse Export

Die Klasse Export repräsentiert einen Export. Wird das attributbasierte Programmiermodell verwendet, so ist dieses ein Objekt, das mit dem Attribut Export dekoriert wurde.

ClassExport

Klassenübersicht

Im Folgenden sind nochmals alle besprochenen Klassen gegenübergestellt.

ClassOverview

Die Klasse ExportProvider

Ausgangspunkt für einen eigenen Export-Provider ist die abstrakte Klasse ExportProvider. Von dieser Klasse muss der eigene Export-Provider abgeleitet werden. MEF liefert schon einige Klasse mit, die von ExportProvider abgeleitet sind. Die bekannteste Klasse ist CompositionContainer.

VererbungExportProvider

Eine der wichtigsten Methoden ist GetExportsCore():

protected abstract IEnumerable<Export> GetExportsCore(ImportDefinition definition,
                                                      AtomicComposition atomicComposition)

Die Methode erwartet u.a. die Definition des Imports, abgebildet durch die Klasse ImportDefinition. In erster Linie ist damit der Vertragsname vom Import gemeint. Zurückgeliefert wird eine Auflistung der Exports, die den gewünschten Kriterien entsprechen.

Des weiteren gibt es in der Klasse ExportProvider eine große Anzahl von Methoden, die mit GetExport…() beginnen. Diese Methoden brauchen bei einem eigenen Export-Provider nicht implementiert werden. Sie dienen nur dazu, den Aufruf der Methode GetExportsCore() auf verschiedene Art und Weise zu kapseln.

Export-Provider an den Container übergeben

Ein oder mehrere Export-Provider werden über den Konstruktor an den Container übergeben. Es stehen zwei Varianten zur Verfügung:

public CompositionContainer(params ExportProvider[] providers)

public CompositionContainer(ComposablePartCatalog catalog,
                            params ExportProvider[] providers)

Ähnlich wie bei Katalogen, gibt es auch bei den Export-Providern eine Aggregate-Klasse, AggregateExportProvider. Diese enthält eine Liste von Objekten, die von ExportProvider abgeleitet sind. Jeder Export-Provider wird dieser Liste hinzugefügt. Kataloge werden über die Klasse CatalogExportProvider der Aggregate-Klasse hinzugefügt. Die Klasse CatalogExportProvider hat die Eigenschaft Catalog und kann Objekte vom Typ ComposeablePartCatalog (wie z. B. also AssemblyCatalog, DirectoryCatalog, TypeCatalog, ..) aufnehmen. Somit sieht der Container nur Export-Provider. Entweder eigene Export-Provider oder Kataloge, die mit Hilfe der Klasse CatalogExportProvider in einen Export-Provider verpackt wurden.

Beispiel

Das folgende Beispiel ähnelt sehr dem ersten Beispiel. Allerdings wird dem Container eine Instanz der Klasse AssemblyCatalog übergeben. Über diesen Katalog sollen die Klassen gefunden werden, die mit dem Attribut Export dekoriert wurden. Bei diesem Beispiel ist es die Klasse Mercedes. Das zweite Objekt, das an den Container übergeben wird, ist der Export-Provider MyExportProvider. Die Klasse ist von ExportProvider abgeleitet und erwartet im Konstruktor den Vertragsname und den Typ des Objektes, das der Container berücksichtigen soll (die Klasse BMW). Der Vertragsname muss natürlich der gleiche sein wie beim Import. Der Typ des Objektes wird hier als Fully Qualified Name angegeben.

public class Program
{
    [ImportMany]
    private ICarContract[] CarParts { get; set; }

    static void Main(string[] args)
    {
        new Program().Run();
    }

    void Run()
    {
        CompositionContainer container = null;

        try
        {
            var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
            var provider = new MyExportProvider(typeof(ICarContract).FullName,
                                                typeof(BMW).FullName);
            container = new CompositionContainer(catalog, provider);
            container.ComposeParts(this);

            foreach (ICarContract carPart in CarParts)
                Console.WriteLine(carPart.GetName());
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex.Message);
        }
        container.Dispose();
    }
}

public interface ICarContract
{
    string GetName();
}

[Export(typeof(ICarContract))]
public class Mercedes : ICarContract
{
    public string GetName()
    {
        return "Mercedes";
    }
}

public class BMW : ICarContract
{
    public string GetName()
    {
        return "BMW";
    }
}

Im Konstruktor der Klasse MyExportProvider wird ein Objekt der Klasse Export angelegt. Wichtig sind hierbei die Metadaten. Über die Metadaten wird der zugrunde liegende Datentyp übergeben (Sample02.ICarContract). Bei diesem Beispiel ist das gleichzeitig auch der Vertragsname. Im Debugger sieht das Objekt export wie folgt aus:

WatchWindow01

Durch den Aufruf von ComposeParts() im Hauptprogramm, wird im Export-Provider die Methode GetExportsCore() aufgerufen. Hier wird entschieden, welche Exports zu dem Import passen. Erzeugt wird das Objekt in der Methode CreatePart(). Diese wird über den Delegate funcCreatePart für jedes Objekt vom Typ Export, das im Konstruktor erzeugt wurde, einmal aufgerufen. In diesem Beispiel wird einfach in der aktuellen Assembly nach dem gewünschten Typ gesucht und eine Instanz erzeugt.

public class MyExportProvider : ExportProvider
{
    private List<Export> Exports { get; set; }
    private string TypeName { get; set; }

    public MyExportProvider(string contractName, string typeName)
    {
        Func<object> funcCreatePart = new Func<object>(CreatePart);
        this.TypeName = typeName;

        this.Exports = new List<Export>();
        var metadata = new Dictionary<string, object>();
        metadata.Add(CompositionConstants.ExportTypeIdentityMetadataName,
                     contractName);

        var exportDefinition = new ExportDefinition(contractName, metadata);
        var export = new Export(exportDefinition, funcCreatePart);
        this.Exports.Add(export);
    }

    public object CreatePart()
    {
        Type partType = Assembly.GetExecutingAssembly().GetType(this.TypeName);
        object instance = Activator.CreateInstance(partType);
        return instance;
    }

    protected override IEnumerable<Export> GetExportsCore(ImportDefinition definition,
                                                          AtomicComposition atomicComposition)
    {
        return this.Exports.Where(x => definition.IsConstraintSatisfiedBy(x.Definition));
    }
}

Wie im ersten Beispiel wird auch hier die Klasse BMW nicht mit dem Attribut Export dekoriert. Trotzdem wird auch diese Klasse von MEF berücksichtigt. Möglich macht das der eigene Export-Provider, der unabhängig von den Attributen die gewünschte Instanz erzeugt.

Beispiel 2 (Visual Studio 2010) auf GitHub

Wer an weiteren Informationen zu MEF interessiert ist, dem sei die MEF CodePlex Seite sehr empfohlen. Insbesondere der Bereich Architecture Overview und der Artikel Hosting the .NET Composition Primitives. Hilfreich ist auch der Artikel von Klaus Aschenbrenner aus der Zeitschrift dotnetpro Ausgabe 07/2009.

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.

4 thoughts on “MEF Teil 8 – Eigenen ExportProvider erstellen”

Leave a comment