Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 6 Min.

Assemblies und .NET Core

Ein Plug-in-System mit .NET Core aufbauen.
© dotnetpro
Assemblies sind die Grundbausteine von .NET und .NET Core. Sie beherbergen Typen und Metainformationen, die für die Ausführung komplexer Anwendungen erforderlich sind. Doch oft entsteht der Wunsch nach einem Plug-in-System mit .NET und dann stellt sich die Frage, wie man Assemblies dynamisch laden und den Code darin ausführen kann. Vor allem dann, wenn die nachgeladenen Assemblies auch während der Laufzeit der Anwendung wieder entladen werden sollen, wird es spannend – und leider auch komplex. Nutzt man das .NET Framework, muss man sich mit Themen wie AppDomains auseinandersetzen. Mit .NET Core wurde das Konzept der AppDomains allerdings verworfen – die Alternative lernen Sie hier kennen.

Das Beispiel-Plug-in

Als Beispiel für diesen Artikel dient ein einfaches Projekt, welches für .NET Standard 2.0 kompiliert wird. Dadurch lässt sich die Assembly sowohl mit dem .NET Framework als auch mit .NET Core laden und ausführen. Innerhalb des Beispielprojekts befindet sich nur eine einzelne Klasse namens MyPlugin mit einer Methode namens DoSomething, welche eine kurze Textzeile auf der Konsole ausgibt. Ziel ist es, die Klasse dynamisch zur Laufzeit zu laden und auszuführen.

public class MyPlugin 
{ 
  public void DoSomething() 
  { 
    Console.WriteLine(„It works! :-)“); 
  } 
} 
 
Die Klasse MyPlugin befindet sich in einem isolierten Projekt und wird einmalig kompiliert. Sie liegt daher in einer Assembly namens MyPluginLibrary.dll auf der Festplatte. Nachdem diese Assembly keine Abhängigkeiten von anderen Assemblies aufweist, reicht sie für das erste Beispiel vollkommen aus.

Dynamisch klingt gut ...

Legt man nun ein weiteres Projekt an, welches mit dem .NET Framework erstellt wird, könnte man die DLL direkt referenzieren, erhält zur Compile-Zeit bereits volle Unterstützung und ist dadurch in der Lage, die Klasse MyPlugin direkt zu instanzieren. Dies ist aber gerade bei Plug-ins nicht gewünscht, sondern die Erweiterung soll erst zur Laufzeit eingefügt werden. Dies wäre im einfachsten Fall durch folgende Variante möglich:

static void Main(string[] args) 
{ 
  var currentAssemblyFolderPath = 
    Path.GetDirectoryName( 
    typeof(Program).Assembly.Location); 
  var absolutePath = 
    Path.Combine(currentAssemblyFolderPath, 
    @"MyPluginLibrary.dll"); 

  var assembly = 
    Assembly.LoadFile(absolutePath); 
  var myPluginType = 
    assembly.GetType("MyPluginLibrary.MyPlugin"); 
  var instanceOfPlugin = 
    Activator.CreateInstance(myPluginType); 

  myPluginType.GetMethod( 
    "DoSomething")?.Invoke( 
    instanceOfPlugin, null); 

  Console.WriteLine("Hello World!"); 
} 
Beim Ausführen dieses Codes wird die Assembly mit dem konkreten Namen direkt geladen und per Reflexion der passende Typ gesucht, gefunden und instanziert <span class="text-bildnachweis">(Bild 1)</span>. 
Einfache Varianteeiner Art von .NET-Plug-in(Bild 1) © Autor
Natürlich könnte man nun bereits eine benutzerdefinierte Logik einbauen, welche die Assembly zum Beispiel aufgrund einer Namenskonvention lädt oder aber aufgrund eines (eventuell zur Compile-Zeit bekannten) Interfaces sucht. Die Möglichkeiten sind hier vielfältig.Was in diesem Beispiel allerdings sehr stört, ist, dass die Assembly nach dem Laden gesperrt wird. Versucht man sie zu löschen, während die Anwendung läuft, so wird dies verweigert, siehe Bild 2.
Die Assemblykann während der Laufzeit nicht gelöscht werden, ein Entladen des Plug-ins ist nicht möglich(Bild 2) © Autor
Selbst wenn der Garbage Collector die Instanz „wegräumt“, so bleibt die Assembly so lange geladen, bis die Anwendung geschlossen wird. Erst danach ist es wieder möglich, die Assembly zu entfernen. Echte Plug-in-Anwendungen arbeiten anders.Der bis hierhin vorgestellte Code ist im .NET Framework und unter .NET Core identisch und funktioniert somit in beiden Welten. Die APIs sind deckungsgleich und werden sogar vom .NET-Standard abgebildet.

Abhängigkeiten

Einfach ist dieses Unterfangen so lange, bis das Plug-in eigene Abhängigkeiten benötigt, da diese beim Laden gefunden werden müssen. Grundsätzlich sucht die Runtime Abhängigkeiten dynamisch. Das bedeutet, es wird versucht, die Abhängigkeiten der Assembly beim Ausführen aufzulösen. Die Abhängigkeit kann dabei neben der eigentlichen Host-Anwendung liegen, beispielsweise in einem Unterordner [1], oder im Fall des .NET Frameworks im Global Assembly Cache.Liegt die Abhängigkeit jedoch woanders, hat die Runtime kaum Chancen, die gesuchten Assemblies zu finden. In solch einem Fall hilft nur noch das statische Event AppDomain.CurrentDomain.AssemblyResolve.

AppDomain.CurrentDomain.AssemblyResolve += 
  (o, e) =&gt; 
{ 
  var assemblyName = 
    AssemblyName.GetAssemblyName(e.Name); 
  return null; 
}; 
 
In diesem Eventhandler wird untypischerweise ein return-Wert verlangt. Dieser kann null sein für „nicht gefunden“ oder aber die eigentliche Assembly repräsentieren. Um dies nachzustellen, wurde in das Library-Projekt ein NuGet-Paket installiert (zum Beispiel Newtonsoft.Json) und auch ein Typ aus dem Paket in der Methode verwendet.Das ist wichtig, denn wenn kein Typ aus der Abhängigkeit verwendet wird, so wird auch nicht versucht, die abhängige Assembly zu laden. Bemerken kann man das Laden beziehungsweise den Versuch, die Assembly zu laden, erst beim Aufrufen der Methode. Eine Exception wird deshalb auch erst dann ausgelöst, wenn die Methode aufgerufen wird, vergleiche Bild 3.
Exception:Das System kann die Datei nicht finden(Bild 3) © Autor

AppDomains und .NET Framework

Mit dem .NET Framework ist es möglich, eine benutzerdefinierte AppDomain zu erzeugen. Damit erstellt man eine Art Sandbox, welche in der Lage ist, Assemblies zu laden, aber auch die Ausführung von Code innerhalb dieser AppDomain zu beschränken.Mithilfe einer AppDomain lassen sich sehr mächtige Systeme entwickeln, die isoliert vom eigentlich Business-Code laufen können. Ein sehr interessantes Feature im Sinne der Plug-ins ist dabei, dass man eine AppDomain auch wieder entladen und somit auch die Assembly während der Laufzeit wieder freigeben kann.

var currentAssemblyFolderPath = 
  Path.GetDirectoryName( 
  typeof(Program).Assembly.Location); 
var absolutePath = 
  Path.Combine(currentAssemblyFolderPath, 
  @“MyPluginLibrary.dll“); 

var domain = 
  AppDomain.CreateDomain(„PluginDomain“); 
var typeInPluginDomain = 
  domain.Load(File.ReadAllBytes(absolutePath)); 
// Die nachfolgende Zeile funktioniert hier nicht:
File.Delete(absolutePath); 
AppDomain.Unload(domain); 
' Allerdings funktioniert hier die folgende Zeile:
File.Delete(absolutePath); 
 
Versucht man nun, denselben Code mit .NET Core auszuführen, verwehrt leider eine NotSupportedException den Erfolg. Man kann auf AppDomains aus Kompatibilitätsgründen vom API aus zwar zugreifen, aber nicht alle Funktionalitäten lassen sich in .NET Core nutzen. So erlaubt es .NET Core beispielsweise nicht, eine eigene AppDomain zu erzeugen, siehe Bild 4.
Exception:.NET Core verweigert das Erzeugen einer AppDomain(Bild 4) © Autor

Die neue Lösung: AssemblyLoadContext

Als Lösung für solche Szenarien ist in .NET Core die neue Klasse AssemblyLoadContext [2] vorgesehen, von der man eine eigene Ableitung erstellen kann. Dabei ist es möglich, gleich zwei sehr mächtige Dinge anzugeben.

internal class MyPluginLoadContext : AssemblyLoadContext 
{ 
  public MyPluginLoadContext() : 
    base(isCollectible: true) 
  { 
  } 
  protected override Assembly Load(
    AssemblyName assemblyName) 
  { 
    return base.Load(assemblyName); 
  } 
} 
 
Einerseits erlaubt eine Konstruktorüberladung anzugeben, ob der Kontext collectable ist – also ob der Kontext entladen werden darf. Dadurch werden spezielle Mechanismen angewendet, die es sowohl dem Garbage Collector als auch der Runtime möglich machen, diesen Kontext und alle Assemblies, die in diesem Kontext geladen wurden, wieder zu entladen.Andererseits kann man nun auch die Methode Load überschreiben und selbst angeben, wie der Kontext Abhängigkeiten laden soll. Das bedeutet: Das statische Event ist nicht mehr zwingend notwendig und man kann im Kontext selbst bestimmen, wie Abhängigkeiten aufzulösen sind. Dies wird übrigens auch für nicht verwaltete Assemblies (Dlls) unterstützt. Mit dieser Klasse kann nun wie folgt verfahren werden:

var currentAssemblyFolderPath = 
  Path.GetDirectoryName( 
  typeof(Program).Assembly
  .Location); 

var absolutePath = 
  Path.Combine(
  currentAssemblyFolderPath, 
  @"MyPluginLibrary.dll"); 

MyPluginLoadContext loadContext = 
  new MyPluginLoadContext(); 

var assembly = 
  loadContext.LoadFromAssemblyPath(
  absolutePath); 
loadContext.Unload(); 
 
Dadurch wird die Assembly in den Kontext geladen und der Kontext später mit der Methode Unload wieder freigegeben.Das funktioniert sehr ähnlich wie die AppDomains. Die neue Variante ist zwar nicht so mächtig wie die alte, da man den Code sicherheitstechnisch nicht einschränken kann, dafür hat man eine viel feinere Steuerung, wie mit dem Kontext umzugehen ist. Gefährlich wird es vor allem, wenn man den Kontext mehrmals instanziert und den Typ mehrmals lädt.Über AppDomains hinweg kann das .NET Framework hier etwas vorbeugen, da sich über die Location einer Assembly feststellen lässt, ob zwei Assemblies identisch sind. Die .NET-Core-Lösung lässt dies aber außer Acht und erlaubt es, dieselbe Assembly mehrmals zu laden (Bild 5).
Dieselbe Quelle, aber nicht identisch(Bild 5) © Autor
Noch gravierender wird das Problem, wenn man einen Kontext mehrmals instanziert und den Typ mehrfach lädt, da diese im Debugger nahezu identisch aussehen, es aber nicht sind, wie Bild 6 deutlich macht.
Zwei Kontexteund zwei Instanzen desselben Plug-ins(Bild 6) © Autor

Im Fall der (Not-)Fälle

Sollte es einmal so weit sein, dass man wissen möchte, aus welchen Kontext nun welcher Typ kommt, so hilft eine kleine Hilfsmethode von .NET Core.

var loadContextOfType = 
  AssemblyLoadContext.GetLoadContext( 
  typeof(string).Assembly); 
 
Mit dieser Methode lässt sich von jeder Assembly auf den Kontext schließen, aus dem sie geladen wurde. In diesem Fall wird man den Default Load Context erhalten, den Standardkontext von .NET Core. Alle Grundtypen und Standardauflösungen von Assemblies werden mithilfe dieses Kontexts aufgelöst.

Fazit

Assemblies sind und bleiben die Bausteine von Entwicklern. Viele Dinge lassen sich vom .NET Framework auf .NET Core übertragen, aber leider nicht alle. Mit dem AssemblyLoadContext der mit .NET Core 3.0 neu eingeführt wurde, sind viele Szenarien (wieder) möglich.Will man seine .NET-Core-Anwendung auf den Einsatz von Plug-ins vorbereiten, hat man damit nun eine Möglichkeit, das Vorhaben umzusetzen. Doch wie immer gilt: Mit solch einem Werkzeug bekommen wir Entwickler eine große Macht und damit Verantwortung an die Hand und sollten sehr achtsam damit umgehen, da die Bugs bei fehlerhafter Verwendung niemals schlafen.

Fussnoten

  1. Abhängigkeiten laden, http://www.dotnetpro.de/SL2004PluginCore1
  2. Die Klasse AssemblyLoadContext, http://www.dotnetpro.de/SL2004PluginCore2
  3. Plug-ins und .NET Core, http://www.dotnetpro.de/SL2004PluginCore3

Neueste Beiträge

DWX hakt nach: Wie stellt man Daten besonders lesbar dar?
Dass das Design von Websites maßgeblich für die Lesbarkeit der Inhalte verantwortlich ist, ist klar. Das gleiche gilt aber auch für die Aufbereitung von Daten für Berichte. Worauf besonders zu achten ist, erklären Dr. Ina Humpert und Dr. Julia Norget.
3 Minuten
27. Jun 2025
DWX hakt nach: Wie gestaltet man intuitive User Experiences?
DWX hakt nach: Wie gestaltet man intuitive User Experiences? Intuitive Bedienbarkeit klingt gut – doch wie gelingt sie in der Praxis? UX-Expertin Vicky Pirker verrät auf der Developer Week, worauf es wirklich ankommt. Hier gibt sie vorab einen Einblick in ihre Session.
4 Minuten
27. Jun 2025
„Sieh die KI als Juniorentwickler“
CTO Christian Weyer fühlt sich jung wie schon lange nicht mehr. Woran das liegt und warum er keine Angst um seinen Job hat, erzählt er im dotnetpro-Interview.
15 Minuten
27. Jun 2025
Miscellaneous

Das könnte Dich auch interessieren

UIs für Linux - Bedienoberflächen entwickeln mithilfe von C#, .NET und Avalonia
Es gibt viele UI-Frameworks für .NET, doch nur sehr wenige davon unterstützen Linux. Avalonia schafft als etabliertes Open-Source-Projekt Abhilfe.
16 Minuten
16. Jun 2025
Mythos Motivation - Teamentwicklung
Entwickler bringen Arbeitsfreude und Engagement meist schon von Haus aus mit. Diesen inneren Antrieb zu erhalten sollte für Führungskräfte im Fokus stehen.
13 Minuten
19. Jan 2017
Evolutionäres Prototyping von Business-Apps - Low Code/No Code und KI mit Power Apps
Microsoft baut Power Apps zunehmend mit Features aus, um die Low-Code-/No-Code-Welt mit der KI und der professionellen Programmierung zu verbinden.
19 Minuten
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige