Der folgende Post behandelt eigentlich mehrere Themen. Zum einen geht es um das Erstellen eines eigenen ExportProviders, der über die App.config die zu ladenden Parts ausliest. Zum anderen sollen die geladenen Parts in einer eigenen AppDomain ausgeführt werden.
Das Managed Extensibility Framework (MEF) nutzt ein attributbasiertes Programmiermodell, um die einzelnen Exports an die Imports zu binden. Manchmal kann es aber von Vorteil sein, wenn über eine Konfigurationsdatei die Auswahl der Parts vorgegeben wird. Hierzu bietet sich die App.config an. In einem älteren Post wurde das Erstellen von benutzerdefinierten Konfigurationsabschnitten in der App.config schon vorgestellt. Der Focus soll mehr auf das Erstellen eines ExportProviders gelegt werden, der die Parts in eine eigene AppDomain lädt und dort mit eingeschränkten Rechten zur Ausführung bringt. Grundlagen zur Erstellung eigener ExportProvider findet ihr hier.
Die zu ladenen Parts sind in der App.config aufgelistet:
<configuration> <configSections> <section name="AddInSection" type="Sample01.AddInConfigurationSection, CarHost"/> </configSections> <AddInSection> <AddIns> <AddIn id="1" contract="CarContract.ICarContract" assemblyName="BMW" typeName="Sample01.BMW"/> <AddIn id="2" contract="CarContract.ICarContract" assemblyName="Mercedes" typeName="Sample01.Mercedes"/> </AddIns> </AddInSection> </configuration>
Beide Parts sind identisch aufgebaut und befinden sich jeweils in separaten Assemblies. Wie auch zu erkennen ist, besitzen die Klassen BMW und Mercedes nicht das Attribut Export. Schließlich soll die App.config vorgeben, welche Assemblies zu laden sind, nicht das attributbasierte Programmiermodell von MEF. Da die Assemblies in einer eigenen AppDomain ausgeführt werden sollen und an die Methode Parameter übergeben werden, müssen die Klassen von MarshallByRefObject abgeleitet werden.
namespace Sample01 { public class BMW : MarshalByRefObject, ICarContract { public void WriteString(string name) { try { Console.WriteLine("BMW: WriteString()"); File.WriteAllText(@"C:\Test.txt", name); } catch (Exception ex) { Console.WriteLine(ex.Message); } } } } namespace Sample01 { public class Mercedes : MarshalByRefObject, ICarContract { public void WriteString(string name) { try { Console.WriteLine("Mercedes: WriteString()"); File.WriteAllText(@"C:\Test.txt", name); } catch (Exception ex) { Console.WriteLine(ex.Message); } } } }
Die Methode WriteString() soll schreibend auf eine Datei zugreifen. Im Falle einer Exception wird die Fehlermeldung auf die Konsole ausgegeben.
Die Schnittstelle ICarContract liegt ebenfalls in einer separaten Assembly und beinhaltet die gemeinsam genutzte Methode.
namespace CarContract { public interface ICarContract { void WriteString(string name); } }
Die Hauptanwendung ist eher unspektakulär. Es wird eine Instanz des eigenen ExportProviders angelegt. Dieser wird an den Container übergeben. Die Methode ComposeParts() triggert das Laden und Binden der einzelnen Komponenten an. In der Foreach-Schleife wird zum Schluss von jedem Part die enthaltene Methode WriteString() aufgerufen.
namespace Sample01 { public class Program { [ImportMany(typeof(ICarContract))] private IEnumerable<Lazy<ICarContract>> CarParts { get; set; } static void Main(string[] args) { new Program().Run(); } void Run() { CompositionContainer container = null; var provider = new ConfigExportProvider(); container = new CompositionContainer(provider); container.ComposeParts(this); foreach (Lazy<ICarContract> carPart in CarParts) carPart.Value.WriteString("Test"); container.Dispose(); } } }
Spannend ist die Implementierung des ExportProviders. Im Konstruktor wird der Inhalt der App.config durchlaufen und für jeden Eintrag eine Instanz der Klasse Export angelegt.
Wichtig ist die Methode CreatePart() die im Konstruktor übergeben wird (Zeile 23). Diese erzeugt die einzelnen Parts. Ausgeführt wird CreatePart() sobald MEF die einzelnen Parts benötigt. Da die Imports über die Klasse Lazy<T> gespeichert werden, geschieht dieses bei dem ersten Zugriff auf das Objekt, also bei Aufruf der Methode WriteString().
Zuvor ruft MEF noch die Methode GetExportsCore() auf. Dort wird die Liste der Exports zurückgegeben, die zu dem Import passen.
namespace Sample01 { public class ConfigExportProvider : ExportProvider { private List<Export> Exports { get; set; } public ConfigExportProvider() { this.Exports = new List<Export>(); AddInConfigurationSection myConfigurationSection = (AddInConfigurationSection)ConfigurationManager.GetSection("AddInSection"); foreach (AddInSectionValue sectionValue in myConfigurationSection.SectionValues) { var metadata = new Dictionary<string, object>(); metadata.Add( CompositionConstants.ExportTypeIdentityMetadataName, sectionValue.Contract); var exportDefinition = new ExportDefinition(sectionValue.Contract, metadata); string assemblyName = sectionValue.AssemblyName; string typeName = sectionValue.TypeName; var export = new Export(exportDefinition, () => CreatePart(assemblyName, typeName)); this.Exports.Add(export); } } public object CreatePart(string assemblyName, string typeName) { var info = new AppDomainSetup { ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase }; var grantSet = new PermissionSet(PermissionState.None); grantSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution)); // soll der Dateizugriff erlaubt sein, so muss die folgende Zeile eingefügt werden // grantSet.AddPermission(new FileIOPermission(PermissionState.Unrestricted)); var appDomain = AppDomain.CreateDomain("AddIn AppDomain", AppDomain.CurrentDomain.Evidence, info, grantSet); var instance = appDomain.CreateInstanceAndUnwrap(assemblyName, typeName); return instance; } protected override IEnumerable<Export> GetExportsCore( ImportDefinition definition, AtomicComposition atomicComposition) { return this.Exports.Where(x => definition.IsConstraintSatisfiedBy(x.Definition)); } } }
In der Methode CreatePart() wird eine separate AppDomain mit stark eingeschränkten Rechten angelegt. In dieser wird die Assembly geladen. Die Rechte sind soweit eingeschränkt, dass Dateizugriffe nicht erlaubt sind.
Wird das Programm ausgeführt, so endet der Versuch, in eine Datei zu schreiben, in einer SecurityException.
jetzt ist das Sample im Teil 8 auch verständlich, da fehlen paar “” (spitze Klammern).
Die spitzen Klammern sollten jetzt vollzählig vorhanden sein.
Ich habe bei WordPress immer das Problem, das im Quellcode Wörter in spitzen Klammern nicht dargestellt werden. Durch das Einfügen von Leerzeichen konnte ich mich immer behelfen.
Auch wird die horizontale Scroll bar innerhalb der Quellcode Bereiche nicht mehr angezeigt; zumindest nicht im IE. Firefox hat damit keine Probleme. Vielleicht hat jemand ein Tipp für mich 😉
Stefan,
danke für die 10 ausführlichen Teile über MEF.
HI,
vielen Dank. Deine Artikel waren wirklich sehr Hilfreich.
Wollte ein eigenes Plugin Framework entwickeln, MEF scheint hier aber die bessere Wahl zu sein.
lg
Hallo,
vielen Dank für die TOP Anleitungen 🙂
Mfg
Frank
Könnten Sie ein Beispiel zeigen, wie man DirectoryCatalog und CreatePart nutzt ?
Vielen Dank.
Vielen Dank für den super Artikel. Ich versuche gerade es mit WPF und mit der MVVM Struktur einzubinden. Hast du vielleicht einige Ansätze (Beispiele) die du schon gemacht hast? Ich komme nämlich an einige Stellen nicht weiter. Vielen lieben Dank
Ich persönlich habe hierzu keine Beispiele. Ein Kollege hat mir allerdings einige Links empfohlen, die evtl. weiterhelfen:
https://blogs.msmvps.com/bsonnino/2013/08/31/developing-modular-applications-with-mef/
https://markheath.net/post/screencast-modular-wpf-with-mef-mvvm
https://www.eidias.com/blog/2013/7/26/plugins-in-wpf-mvvm-with-mef
Ich hoffe, das dir die Artikel weiterhilfen.