| 

.NET C# Java Javascript Exception

0
Im 1. Teil ging es um die Grundlagen, die Imports und Exports. Der 2. Teil schließt an den ersten an und zeigt weitere Leistungsmerkmale des Managed Extensibility Framework (MEF).  Schwerpunkt sind dieses Mal die Metadaten und die Erstellungsrichtlinien. Metadaten Exports können über Metadaten weitere Informationen bereitstellen. Diese Informationen werden über die Klasse Lazy<> abgefragt, ohne [...]

Im 1. Teil ging es um die Grundlagen, die Imports und Exports. Der 2. Teil schließt an den ersten an und zeigt weitere Leistungsmerkmale des Managed Extensibility Framework (MEF).  Schwerpunkt sind dieses Mal die Metadaten und die Erstellungsrichtlinien.


Metadaten

Exports können über Metadaten weitere Informationen bereitstellen. Diese Informationen werden über die Klasse Lazy<> abgefragt, ohne dass von dem Composable Part eine Instanz erzeugt werden muss.

Für das Beispiel wird wieder auf den ersten Teil zurückgegriffen. Es gibt eine Anwendung (CarHost.exe) die per Import verschiedene Autos (BMW.dll und Mercedes.dll) an sich bindet. Ein Contract (CarContract.dll) beinhaltet die Schnittstelle, über die der Host auf die Exports zugreift.

Die Metadaten sollen aus drei Werten bestehen. Zum einen ein String, in dem der Name (Name) steht. Als zweites eine Aufzählung, mit der Angabe einer Farbe (Color). Als letztes ein Integer mit dem Preis (Price).

Es gibt mehrere Varianten, wie die Exports dem Import die Metadaten zur Verfügung stellen können:

    Metadaten werden mithilfe des Attributes ExportMetadata bekannt gegeben. Dazu wird jedes Element der Metadaten durch ein Name-Werte-Paar beschrieben. Der Name ist immer vom Typ String, während der Wert vom Typ Object ist. Evtl. kann es notwendig sein, einen Wert explizit durch den cast-Operator in den gewünschten Datentyp zu wandeln. In diesem Fall muss bei Price der Wert nach dem Datentyp uint gewandelt werden. 

    Es werden zwei Exports angelegt, die jeweils die gleichen Metadaten anbieten. Die Werte der Metadaten sind allerdings unterschiedlich.

    using System;
    using System.ComponentModel.Composition;
    using CarContract;
    namespace CarMercedes
    {
     [ExportMetadata("Name", "Mercedes")]
     [ExportMetadata("Color", CarColor.Blue)]
     [ExportMetadata("Price", (uint)48000)]
     [Export(typeof(ICarContract))]
     public class Mercedes : ICarContract
     {
     private Mercedes()
     {
     Console.WriteLine("Mercedes constructor.");
     }
     public string StartEngine(string name)
     {
     return String.Format("{0} starts the Mercedes.", name);
     }
     }
    }
    
    using System;
    using System.ComponentModel.Composition;
    using CarContract;
    namespace CarBMW
    {
     [ExportMetadata("Name", "BMW")]
     [ExportMetadata("Color", CarColor.Black)]
     [ExportMetadata("Price", (uint)55000)]
     [Export(typeof(ICarContract))]
     public class BMW : ICarContract
     {
     private BMW()
     {
     Console.WriteLine("BMW constructor.");
     }
     public string StartEngine(string name)
     {
     return String.Format("{0} starts the BMW.", name);
     }
     }
    }
    

    Die Schnittstelle ICarContract bietet die Methode an, auf die der Import später zugreifen kann. Er stellt auch den ‘Vertrag’ zwischen dem Import und den Exports da. Im gleichen Namespace ist auch die Aufzählung CarColor definiert.

    using System;
    namespace CarContract
    {
     public interface ICarContract
     {
     string StartEngine(string name);
     }
     public enum CarColor
     {
     Unkown,
     Black,
     Red,
     Blue,
     White
     }
    }
    

    Erreichbar sind die Metadaten für den Import über die Klasse Lazy<T, TMetadata>. Hierzu bietet die Klasse die Eigenschaft Metadata an. Metadata ist vom Typ Dictionary<string, object>.

    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.ComponentModel.Composition.Hosting;
    using CarContract;
    namespace CarHost
    {
     class Program
     {
     [ImportMany(typeof(ICarContract))]
     private IEnumerable< Lazy< ICarContract, Dictionary< string, object >>> CarParts { get; set; }
     
     static void Main(string[] args)
     {
     new Program().Run();
     }
     void Run()
     { 
     var catalog = new DirectoryCatalog(".");
     var container = new CompositionContainer(catalog);
     container.ComposeParts(this);
     foreach (Lazy< ICarContract, Dictionary< string, object >> car in CarParts)
     {
     if (car.Metadata.ContainsKey("Name"))
     Console.WriteLine(car.Metadata["Name"]);
     if (car.Metadata.ContainsKey("Color"))
     Console.WriteLine(car.Metadata["Color"]);
     if (car.Metadata.ContainsKey("Price"))
     Console.WriteLine(car.Metadata["Price"]);
     Console.WriteLine("");
     }
     foreach (Lazy< ICarContract > car in CarParts)
     Console.WriteLine(car.Value.StartEngine("Sebastian"));
     container.Dispose();
     }
     }
    }
    

    Wird auf ein bestimmtes Element der Metadaten zugegriffen, so sollte geprüft werden ob der Export das gewünschte Element überhaupt definiert hat. Es kann durchaus sein, dass Imports unterschiedliche Metadaten anbieten.

    Bei der Ausführung des Programms ist gut zu erkennen, dass die Export-Parts durch den Zugriff auf die Metadaten nicht initialisiert werden. Erst der Zugriff auf die Methode StartEngine() erzeugt eine Instanz und ruft dadurch den Konstruktor auf.

    CommandWindowSample01

    Da die Metadaten in eine Klasse vom Typ Dictionary<string, object> abgelegt werden,  können diese beliebig viele Metadaten enthalten. Dieses hat Vor- und Nachteile. Der Vorteil ist, dass alle Metadaten optional sind und beliebige Informationen anbieten können. Der Wert ist ja vom Typ Object. Genau hierdurch geht aber die Typsicherheit verloren. Dieses ist ein großer Nachteil. Beim Zugriff muss immer geprüft werden, ob die gewünschten Metadaten überhaupt vorhanden sind. Ansonsten kann es zu unangenehmen Laufzeitfehlern kommen.

    Beispiel 1 (Visual Studio 2010)

    2. Variante: typsicher per Schnittstelle

    So wie die zur Verfügung stehenden Methoden und Eigenschaften eines Exports durch eine Schnittstelle festgelegt werden (ICarContract), so können auch die Metadaten durch eine Schnittstelle definiert werden. In diesem Fall legen Eigenschaften die einzelnen Werte fest, die später verfügbar sind. Es dürfen nur Eigenschaften definiert werden, die über einen get-Accessor zugänglich sind (wird trotzdem ein set-Accessor definiert, kommt es zu einem Laufzeitfehler).

    Für unser Beispiel werden die drei Eigenschaften vom gewünschten Datentyp angelegt. Die Definition der Schnittstelle für die Metadaten ist somit wie folgt:

    public interface ICarMetadata
    {
     string Name { get; }
     CarColor Color { get; }
     uint Price { get; }
    }
    

    Die Schnittstelle für die Metadaten wird bei der Prüfung zwischen Import und Export benutzt. Alle Exporte müssen die definierten Metadaten bereitstellen. Fehlende Metadaten führen ebenfalls zu einem Laufzeitfehler. Das Attribut DefaultValue kann benutzt werden, wenn eine Eigenschaft optional ist.

    [DefaultValue((uint)0)]
    uint Price { get; }
    

    Damit nicht alle Metadaten bei einem Export definiert werden müssen, werden alle Eigenschaften in diesem Beispiel mit dem Attribute DefaultValue dekoriert.

    using System;
    using System.ComponentModel;
    namespace CarContract
    {
     public interface ICarMetadata
     {
     [DefaultValue("NoName")]
     string Name { get; }
    
     [DefaultValue(CarColor.Unkown)]
     CarColor Color { get; }
    
     [DefaultValue((uint)0)]
     uint Price { get; }
     }
    }
    

    Die Schnittstelle ICarContract und die Exports werden genauso erstellt wie in dem ersten Beispiel.

    Für den Zugriff auf die Metadaten wird bei der Klasse Lazy<T, TMetadata> für TMetadata die Schnittstelle für die Metadaten angegeben. In diesem Beispiel ist es die Schnittstelle ICarMetadata. Über die Eigenschaft Metadata stehen somit die einzelnen Metadaten zur Verfügung.

    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.ComponentModel.Composition.Hosting;
    using System.Linq;
    using CarContract;
    namespace CarHost
    {
     class Program
     {
     [ImportMany(typeof(ICarContract))]
     private IEnumerable< Lazy< ICarContract, ICarMetadata >> CarParts { get; set; }
     
     static void Main(string[] args)
     {
     new Program().Run();
     }
     void Run()
     { 
     var catalog = new DirectoryCatalog(".");
     var container = new CompositionContainer(catalog);
     container.ComposeParts(this);
     foreach (Lazy< ICarContract, ICarMetadata > car in CarParts)
     {
     Console.WriteLine(car.Metadata.Name);
     Console.WriteLine(car.Metadata.Color);
     Console.WriteLine(car.Metadata.Price);
     Console.WriteLine("");
     }
     // invokes the method only of black cars
     var blackCars = from lazyCarPart in CarParts
     let metadata = lazyCarPart.Metadata
     where metadata.Color == CarColor.Black
     select lazyCarPart.Value;
     foreach (ICarContract blackCar in blackCars)
     Console.WriteLine(blackCar.StartEngine("Sebastian"));
     Console.WriteLine("");
     // invokes the method of all imports
     foreach (Lazy< ICarContract > car in CarParts)
     Console.WriteLine(car.Value.StartEngine("Sebastian"));
     container.Dispose();
     }
     }
    }}
    

    Da die Schnittstelle ICarMetadata Name und Typ der Metadaten festlegt, kann auf diese direkt zugegriffen werden. Diese Typsicherheit bringt noch einen kleinen, aber hilfreichen Vorteil mit sich. Auf die Eigenschaft CarParts kann jetzt mit LINQ zugegriffen werden. So lässt sich an Hand der Metadaten eine Filterung durchführen, so dass nur bestimmte Imports benutzt werden.

    In der ersten foreach-Schleife werden von allen Exports die Metadaten ausgegeben. Danach wird mit LINQ eine Abfrage erstellt, die in einer Auflistung nur die Exports bereitstellt, deren Metadaten einen bestimmten Wert haben. Hier muss Color den Wert CarColor.Black haben. Nur von diesen Exports wird die Methode StartEngine() aufgerufen. In der letzten foreach-Schleife erfolgt der Aufruf der Methode bei allen Exports. 

    CommandWindowSample02

    Auch hier ist gut zu erkennen, dass weder das Ausgeben aller Metadaten noch die LINQ-Abfrage einen Export initialisiert. Erst der Aufruf der Methode StartEngine() führt zum Anlegen einer Instanz und somit zum Aufruf des Konstruktors.

    Beispiel 2 (Visual Studio 2010)

    Aus meiner Sicht sollte bei Metadaten nach Möglichkeit immer mit Schnittstellen gearbeitet werden. Auch wenn der Aufwand etwas größer ist, so bleiben einem doch ungeliebte Laufzeitfehler erspart.

    3. Variante: typsicher per Schnittstelle und benutzerdefinierten Exportattributen

    Die Angaben der Metadaten bei den Exports hat noch einen Nachteil. Der Name muss als String angegeben werden. Gerade bei langen Namen können sich hier schnell Schreibfehler einschleichen. Der Compiler kann diese Schreibfehler nicht erkennen. Solche Fehler machen sich erst zur Laufzeit bemerkbar. Angenehmer wäre es, wenn Visual Studio bei der Eingabe alle gültigen Metadaten auflistet und Schreibfehler vom Compiler bemerkt werden. Erreicht werden kann dieser Komfort durch das Anlegen einer eigenen Attribut-Klasse für die Metadaten. Hierzu braucht das vorherige Beispiel nur um eine Klasse erweitert werden.

    using System;
    using System.ComponentModel.Composition;
    namespace CarContract
    {
     [MetadataAttribute]
     public class CarMetadataAttribute : Attribute
     {
     public string Name { get; set; }
     public CarColor Color { get; set; }
     public uint Price { get; set; }
     }
    }
    

    Die Klasse muss mit dem Attribut MetadataAttribute dekoriert werden und von der Klasse Attribute abgeleitet werden. Eigenschaften legen die einzelnen Werte fest, die über die Metadaten exportiert werden sollen. Datentyp und Name der Eigenschaften muss mit der Schnittstelle für die Metadaten übereinstimmen. Die Schnittstelle ICarContract wurde zuvor wie folgt definiert: 

    using System;
    using System.ComponentModel;
    namespace CarContract
    {
     public interface ICarMetadata
     {
     [DefaultValue("NoName")]
     string Name { get; }
    
     [DefaultValue(CarColor.Unkown)]
     CarColor Color { get; }
    
     [DefaultValue((uint)0)]
     uint Price { get; }
     }
    }
    

    Das Dekorieren eines Exports mit Metadaten kann jetzt mit dem selbst definierten Attribut erfolgen. 

    [CarMetadata(Name="BMW", Color=CarColor.Black, Price=55000)]
    [Export(typeof(ICarContract))]
    public class BMW : ICarContract
    {
     // ...
    }
    

    Visual Studio kann jetzt den Entwickler bei der Eingabe unterstützen. Alle gültigen Metadaten werden beim Editieren angezeigt. Auch kann der Compiler jetzt erkennen, ob nur gültige Metadaten angegeben wurden.

    VisualStudioSample03

    Beispiel 3 (Visual Studio 2010)

    4. Variante: typsicher per Schnittstelle und Aufzählungen bei benutzerdefinierten Exportattributen

    Bisher durften bei den Metadaten keine mehrfachen Einträge vorkommen. Vorstellbar wäre aber eine Auflistung, die Optionen enthält, die miteinander kombiniert werden können. Das Auto-Beispiel soll so erweitert werden, dass auch die Ausstattung des Audio-Systems angegeben werden kann. Hierzu wird erst einmal ein enum definiert, das alle möglichen Optionen enthält:

    public enum AudioSystem
    {
     Without,
     Radio,
     CD,
     MP3
    }
    

    Das Interface ICarMetadata wird jetzt um eine Eigenschaft vom Typ AudioSystem erweitert.

    using System;
    using System.ComponentModel;
    namespace CarContract
    {
     public interface ICarMetadata
     {
     [DefaultValue("NoName")]
     string Name { get; }
    
     [DefaultValue(CarColor.Unkown)]
     CarColor Color { get; }
    
     [DefaultValue((uint)0)]
     uint Price { get; }
    
     [DefaultValue(AudioSystem.Without)]
     AudioSystem[] Audio { get; } 
     }
    }
    

    Da ein Radio auch ein CD-Player enthalten kann, muss es möglich sein, bestimmte Metadaten mehrfach anzugeben. Bei dem Export sieht die Deklaration der Metadaten wie folgt aus:

    using System;
    using System.ComponentModel.Composition;
    using CarContract;
    namespace CarBMW
    {
     [CarMetadata(Name="BMW", Color=CarColor.Black, Price=55000)]
     [CarMetadataAudio(AudioSystem.CD)]
     [CarMetadataAudio(AudioSystem.MP3)]
     [CarMetadataAudio(AudioSystem.Radio)]
     [Export(typeof(ICarContract))]
     public class BMW : ICarContract
     {
     private BMW()
     {
     Console.WriteLine("BMW constructor.");
     }
     public string StartEngine(string name)
     {
     return String.Format("{0} starts the BMW.", name);
     }
     }
    }
    
    using System;
    using System.ComponentModel.Composition;
    using CarContract;
    namespace CarMercedes
    {
     [CarMetadata(Name="Mercedes", Color=CarColor.Blue, Price=48000)]
     [CarMetadataAudio(AudioSystem.Radio)]
     [Export(typeof(ICarContract))]
     public class Mercedes : ICarContract
     {
     private Mercedes()
     {
     Console.WriteLine("Mercedes constructor.");
     }
     public string StartEngine(string name)
     {
     return String.Format("{0} starts the Mercedes.", name);
     }
     }
    }
    

    Während der Mercedes nur ein Radio besitzt, so enthält der BMW zusätzlich einen CD-Player und einen MP3-Player.

    Um das zu erreichen, wird eine weitere Attribut-Klasse angelegt. Diese Attribut-Klasse repräsentiert die Metadaten für die Audioausstattung (CarMetadataAudio).

    using System;
    using System.ComponentModel.Composition;
    namespace CarContract
    {
     [MetadataAttribute]
     [AttributeUsage(AttributeTargets.Class, AllowMultiple=true)]
     public class CarMetadataAudioAttribute : Attribute
     {
     public CarMetadataAudioAttribute(AudioSystem audio)
     {
     this.Audio = audio;
     }
     public AudioSystem Audio { get; set; }
     }
    }
    

    Damit das Attribut mehrfach angegeben werden kann, muss die Klasse mit dem Attribut AttributeUsage dekoriert werden. Bei diesem Attribut wird AllowMultiple anschließend auf true gesetzt. Die Attribut-Klasse wurde hier mit einem Konstruktor versehen, der als Parameter den Wert direkt entgegen nimmt.

    Die Ausgabe der mehrfachen Metadaten erfolgt in einer weiteren Schleife (siehe Zeile 28 und Zeile 29):

    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.ComponentModel.Composition.Hosting;
    using System.Linq;
    using CarContract;
    namespace CarHost
    {
     class Program
     {
     [ImportMany(typeof(ICarContract))]
     private IEnumerable< Lazy< ICarContract, ICarMetadata >> CarParts { get; set; }
     
     static void Main(string[] args)
     {
     new Program().Run();
     }
     void Run()
     { 
     var catalog = new DirectoryCatalog(".");
     var container = new CompositionContainer(catalog);
     container.ComposeParts(this);
     foreach (Lazy< ICarContract, ICarMetadata > car in CarParts)
     {
     Console.WriteLine("Name: " + car.Metadata.Name);
     Console.WriteLine("Price: " + car.Metadata.Price.ToString());
     Console.WriteLine("Color: " + car.Metadata.Color.ToString());
     foreach (AudioSystem audio in car.Metadata.Audio)
     Console.WriteLine("Audio: " + audio);
     Console.WriteLine("");
     }
     foreach (Lazy< ICarContract > car in CarParts)
     Console.WriteLine(car.Value.StartEngine("Sebastian"));
     container.Dispose();
     }
     }
    }
    

    Die Ausführung des Programms liefert das erwartete Ergebnis:

    CommandWindowSample04

    Beispiel 4 (Visual Studio 2010)

    Es gibt noch eine weitere Variante. Diese werde ich aber erst unter dem Kapitel Vererbung von Exporten, in einem späteren Blog vorstellen. Bei dieser Variante können durch ein Attribut der Export und die Metadaten gleichzeitig dekoriert werden.

    Erstellungsrichtlinien (Creation Policy)

    In den bisherigen Beispielen wurden durch die Attribute Export und ImportMany mehrere Exporte nur an einen Import gebunden. Wie verhält sich aber MEF, wenn ein Export mehreren Imports zu Verfügung steht? Dazu soll das obere Beispiel etwas angepasst werden. Die Exports und der Contract bleiben unverändert. Im Host werden statt einer, zwei Listen angelegt. Beide Listen sollen die gleichen Exporte aufnehmen.

    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.ComponentModel.Composition.Hosting;
    using CarContract;
    namespace CarHost
    {
     class Program
     {
     [ImportMany(typeof(ICarContract))]
     private IEnumerable< Lazy< ICarContract > > CarPartsA { get; set; }
     [ImportMany(typeof(ICarContract))]
     private IEnumerable< Lazy< ICarContract > > CarPartsB { get; set; }
     static void Main(string[] args)
     {
     new Program().Run();
     }
     void Run()
     { 
     var catalog = new DirectoryCatalog(".");
     var container = new CompositionContainer(catalog);
     container.ComposeParts(this);
     foreach (Lazy< ICarContract > car in CarPartsA)
     Console.WriteLine(car.Value.StartEngine("Sebastian"));
     Console.WriteLine("");
     foreach (Lazy< ICarContract > car in CarPartsB)
     Console.WriteLine(car.Value.StartEngine("Michael"));
     container.Dispose();
     }
     }
    }
    

    Mit dieser Anpassung wird jedem Export zwei Listen (Imports) zugewiesen. Die Ausgabe des Programms lässt allerdings vermuten, dass jeder Export nur einmal instanziiert wird.

    CommandWindowSample05

    Beispiel 5 (Visual Studio 2010)

    Findet das Managed Extensibility Framework zu einem Import den passenden Export, so wird von dem Export eine Instanz erzeugt. Diese Instanz wird mit allen weiteren passenden Imports geteilt. MEF behandelt jeden Export als Singleton.

    Dieses Standardverhalten kann sowohl auf der Seite der Exports, als auch bei den Imports durch die Erstellungsrechtlinien (Creation Policy) beeinflusst werden. Die jeweiligen Erstellungsrichtlinien können den Zustand Shared, NonShared oder Any haben. Standardeinstellung ist Any. Ein Export, für den Shared oder NonShared angegeben wird, stimmt nur mit einem Import überein, für den der gleiche Wert oder Any angegeben wurde. Importe oder Exporte müssen zueinander kompatible Erstellungsrichtlinien besitzen, damit diese als übereinstimmend betrachtet werden. Wird für die Importe als auch für die Exporte Any angegeben (oder auch keine Angabe), dann werden beide Parts auf Shared festgelegt.

    Bei einem Export wird die Erstellungsrichtlinie durch das Attribut PartCreationPolicy festgelegt.

    [Export(typeof(ICarContract))]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    

    Die Eigenschaft RequiredCreationPolicy legt bei dem Attribut Import oder ImportAny die Erstellungsrichtlinie fest.

    [ImportMany(typeof(ICarContract), RequiredCreationPolicy = CreationPolicy.NonShared)]
    private IEnumerable< Lazy< ICarContract > > CarPartsA { get; set; }
    

    Die folgende Ausgabe zeigt das Beispiel, bei dem die Erstellungsrichtlinie auf NonShared gesetzt wurde. Von jedem Export existieren jetzt zwei Instanzen.

    CommandWindowSample06

    Beispiel 6 (Visual Studio 2010)

    Erstellungsrichtlinien können auch miteinander kombiniert werden. Ich habe bei dem Import eine Liste mit NonShared und zwei weitere mit Shared dekoriert.

    [ImportMany(typeof(ICarContract), RequiredCreationPolicy = CreationPolicy.NonShared)]
    private IEnumerable< Lazy< ICarContract > > CarPartsA { get; set; }
    
    [ImportMany(typeof(ICarContract), RequiredCreationPolicy = CreationPolicy.Shared)]
    private IEnumerable< Lazy< ICarContract > > CarPartsB { get; set; }
    
    [ImportMany(typeof(ICarContract), RequiredCreationPolicy = CreationPolicy.Shared)]
    private IEnumerable< Lazy< ICarContract > > CarPartsC { get; set; }
    

    Die Ausgabe zeigt, wie MEF die Instanzen anlegt und den einzelnen Imports zuordnet

    CommandWindowSample06b

    Die erste Liste hat eigenständige Instanzen von den Exports. Liste zwei und Liste drei teilen sich die gleichen Instanzen.

    Ausblick

    Ich finde es sehr erfreulich, dass solch ein Framework standardisiert wurde. Einige Teams von Microsoft setzen MEF bereits erfolgreich ein. Die bekanntesten Beispiele hierfür sind Visual Studio 2010 und Expression Blend. Bleibt zu hoffen, dass noch mehr Produkte folgen werden und dass dadurch die Weiterentwicklung von MEF sichergestellt wird.

    Im 3. Teil geht es um den Lifecycle der Composable Parts.


    mef managed-extensibility-framework lazy import composable-parts metadata export importmany exportmetadata defaultvalue erstellungsrichtlinien partcreationpolicy
    Schreibe einen Kommentar:
    Themen:
    partcreationpolicy erstellungsrichtlinien defaultvalue exportmetadata importmany export metadata composable-parts import lazy managed-extensibility-framework mef
    Entweder einloggen... ...oder ohne Wartezeit registrieren
    Benutzername
    Passwort
    Passwort wiederholen
    E-Mail