Gesucht, gefunden:
Ein wiederverwendbarer Extensions-Manager für .net

Das Managed Extensibility Framework – richtig eingesetzt, eine leichtgewichtige Alternative zu Bibliotheken wie Prism & Co.

Die Zeiten, in denen neue Anwendungen in Form monolither Softwaremonster entwickelt worden sind, sind (hoffentlich) inzwischen überall vorbei. Während gerade bei komplexen und/ oder über viele Dienste verteilten Anwendungen häufig eine Microservice-Architektur zum Einsatz kommt, profitieren häufig aber auch zunächst nur für den lokalen Einsatz konzipierte Anwendungen von einem gewissen Grad der Modularisierung:

  • Neue Funktionen können auch ohne ein komplettes Refactoring hinzugefügt werden; häufig auch solche, die im Rahmen eines ersten Konzepts noch gar nicht vorgesehen waren
  • Der Wartungsaufwand bzw. die Fehlersuche kann deutlich reduziert werden indem die Gesamtanwendung in kleine, gut wartbare Codeabschnitte zerlegt wird
  • Mehrere Programmierer oder Teams können parallel an der Entwicklung der Software arbeiten, z.B. indem ein Team sich auf den Anwendungskern und die Schnittstellen konzentriert, während sich andere Entwickler mit der Implementierung der (Fach-)Funktionen befassen

Kernstück dieses Design-Konzepts ist die lose Kopplung der Plugins, wobei eine Hostanwendung die notwendige Schnittstellen und Basis-Datentypen bereitstellt und sich anschließend um das Auffinden und Instanzieren der verschiedenen Schnitstellenimplementierungen kümmert. Sie sorgt ebenfalls dafür, dass die verschiedenen Erweiterungen (engl. Extensions) bei Bedarf auch gut miteinander kommunizieren können.

Häufig kommen zum Aufbau einer solchen Architektur spezielle Bibliotheken von Drittanbietern zum Einsatz. Eine davon ist Prism. Nicht selten ist es dabei notwendig ganz bestimmte Design-Muster oder Namenskonventionen im Quellcode einzuhalten. Dies gilt zwar auch für das Managed Extensibility Framework (MEF), dafür ist dieses aber ohne zusätzliche Installationen, Lizenzen oder Abhängigeiten nutzbare, da es – als Bestandteil des .net-Frameworks (auch .net Core) – standardmäßig mitinstalliert wird.

Ein komplettes MEF-Tutorial würde wohl den Rahmen dieses Beitrags sprengen. Zudem gibt es bereits gute Artikel zu diesem Thema, so viel aber trotzdem dazu: MEF basiert auf Reflection, was bedeutet, dass, basierend auf einer Liste von Assemblies oder Datei- und Verzeichnispfaden, ein Katalog von Schnittstellenimplementierungen erstellt wird, der als Auflistung von Lazy-Objekten zur Verfügung steht. Mithilfe von Metadaten können die Erweiterungen beschrieben werden, so dass z.B. ein bestimmtes Plugin gefunden und ausgeführt werden kann. Stefan Henneken hat in dieses Konzept in einem entsprechenden Artikel ausführlich beschrieben.

Auch wenn MEF-Features mit wenigen Zeilen Code genutzt werden können, empfiehltsich die Erstellung einer wiederverwendbaren Bibliothek, um ggf. zusätzliche Funktionen nutzen oder bereitstellen zu können. In der von mir öffentlich bereitgestellten BYTES.NET-Bibliothek findet sich eine derartige Manager-Klasse.

ExtensionsManager stellt eine Update-Funktion zur Verfügung, die, basierend auf einem Array von Dateisystempfaden, die Plugins enumeriert und als Extensions-Eigenschaft bereitstellt. Um die Klasse möglichst flexibel zu halten, wurde sie mit generischen Typen dekoriert, so dass sie z.B. wie folgt instanziiert werden kann:

Dim _manager as ExtensionsManager(of MyInterface, MyMetadata) = new ExtensionsManager(of MyInterface, MyMetadata)

Damit dies möglich ist, musste die private Variable _allExtensions, die das Ergebnis der MEF-Katalogerstellung beinhaltet, mit ImportMany ohne Angabe eines einschränkenden Datentyps dekoriert werden. Leider unterstützt MEF aktuell keine generischen Datentypen.

<ImportMany()>
Private _allExtensions As ObservableCollection(Of Lazy(Of TInterface, TMetadata)) = New ObservableCollection(Of Lazy(Of TInterface, TMetadata))

Das bedeutet aber auch, dass MEF zunächst alle gefundenen Plugins, unabhängig vom jeweiligen Datentyp/ Interface, zurückgibt. Solange die Anwendung lediglich über eine einzelne Schnittstellendefinition verfügt, ist dies unproblematisch. Schwieriger wird es bei mehreren Interfaces.

Um dennoch auch mit multi-Interface-Anwendungen kompatibel zu sein, wurde Update mit dem Aufruf der ValidateExtensions-Methode erweitert:

Private Sub ValidateExtensions()
    _validExtensions = New ObservableCollection(Of Lazy(Of TInterface, TMetadata))

        For Each extension In _allExtensions
            Try
                If ImplementsInterface(extension) Then
                    _validExtensions.Add(extension)
                End If
             Catch ex As Exception
             End Try
         Next

End Sub

Diese Methode durchläuft alle gefunden Extensions und prüft ob das als (generischer) Datentyp definierte Interface implementiert wird. Ist dies der Fall, wird das Plugin zur Liste valider Plugins (für diesen Manager) hinzugefügt.

Neben dem (Standard-)ExtensionsManager, enthält die BYTES.NET auch noch eine ExtendedExtensionsManager-Klasse, die es ermöglicht Plugins über Pfade zu finden, die mithilfe von Wildcards definiert wurden bzw. Erweiterung mithilfe einer ID in die Ausgabeliste hinzuzunehmen oder davon auszuschließen. Ein entsprechendes Beispiel ist in der Beispiel-Anwendung im zugehörigen Github-Repository zu finden.