Informationen zu System.Runtime.Loader.AssemblyLoadContext

Die Klasse AssemblyLoadContext wurde in .NET Core eingeführt und ist in .NET Framework nicht verfügbar. Durch diesen Artikel wird die Dokumentation zur AssemblyLoadContext-API um konzeptionelle Informationen ergänzt.

Dieser Artikel richtet sich an Entwickler, die dynamisches Laden implementieren, insbesondere an Entwickler dynamisch geladener Frameworks.

Was ist der AssemblyLoadContext?

Jede Anwendung unter .NET 5 und höher oder .NET Core verwendet implizit AssemblyLoadContext. Er stellt den Laufzeitanbieter zum Suchen und Laden von Abhängigkeiten dar. Wenn eine Abhängigkeit geladen wird, wird eine AssemblyLoadContext-Instanz aufgerufen, um sie zu suchen.

  • AssemblyLoadContext stellt einen Dienst zum Suchen, Laden und Zwischenspeichern von verwalteten Assemblys und anderen Abhängigkeiten bereit.
  • Um das dynamische Laden und Entladen von Code zu unterstützen, erstellt er einen isolierten Kontext für das Laden von Code und dessen zugehörigen Abhängigkeiten in einer eigenen AssemblyLoadContext-Instanz.

Regeln zur Versionsverwaltung

Eine einzelne AssemblyLoadContext-Instanz ist auf das Laden genau einer Version einer Assembly für jeweils einen einfachen Assemblynamen beschränkt. Wenn ein Assemblyverweis für eine AssemblyLoadContext-Instanz aufgelöst wird, die bereits eine Assembly mit diesem Namen geladen hat, wird die angeforderte Version mit der geladenen Version verglichen. Die Auflösung ist nur erfolgreich, wenn die geladene Version gleich oder höher der angeforderten Version ist.

Wann benötigen Sie mehrere AssemblyLoadContext-Instanzen?

Die Einschränkung, dass eine einzelne AssemblyLoadContext-Instanz nur eine Version einer Assembly laden kann, kann beim dynamischen Laden von Codemodulen zu einem Problem werden. Jedes Modul wird unabhängig kompiliert und kann von verschiedenen Versionen einer Assembly abhängen. Dieses Problem tritt häufig auf, wenn verschiedene Module von unterschiedlichen Versionen einer häufig verwendeten Bibliothek abhängen.

Um das dynamische Laden von Code zu unterstützen, ermöglicht die AssemblyLoadContext-API das Laden von in Konflikt stehenden Versionen einer Assembly in derselben Anwendung. Jede AssemblyLoadContext-Instanz stellt eine eindeutige Wörterbuchzuordnung für jede AssemblyName.Name einer bestimmten Assembly-Instanz bereit.

Außerdem bietet sie einen einfachen Mechanismus zum Gruppieren von Abhängigkeiten eines Codemoduls für das spätere Entladen.

Die AssemblyLoadContext.Default-Instanz

Die AssemblyLoadContext.Default-Instanz wird beim Start automatisch von der Runtime aufgefüllt. Sie verwendet Standardüberprüfungen, um alle statischen Abhängigkeiten zu finden.

Damit werden die gängigsten Szenarien zum Laden von Abhängigkeiten gelöst.

Dynamische Abhängigkeiten

AssemblyLoadContext verfügt über verschiedene Ereignisse und virtuelle Funktionen, die überschrieben werden können.

Die AssemblyLoadContext.Default-Instanz unterstützt nur das Überschreiben der Ereignisse.

In den Artikeln Ladealgorithmus für verwaltete Assemblys, Ladealgorithmus für Satellitenassemblys und Ladealgorithmus für nicht verwaltete (native) Bibliotheken werden alle verfügbaren Ereignisse und virtuellen Funktionen beschrieben. In den Artikeln werden die relativen Positionen der einzelnen Ereignisse und Funktionen in den Ladealgorithmen veranschaulicht. Diese Informationen werden in diesem Artikel nicht wiederholt.

In diesem Abschnitt werden die allgemeinen Prinzipien der relevanten Ereignisse und Funktionen behandelt.

  • Wiederholbarkeit. Eine Abfrage für eine bestimmte Abhängigkeit muss immer zu derselben Antwort führen. Es muss dieselbe geladene Abhängigkeitsinstanz zurückgegeben werden. Diese Anforderung ist grundlegend für die Cachekonsistenz. Für verwaltete Assemblys wurde eigens ein Assembly-Cache erstellt. Der Cacheschlüssel ist ein einfacher Assemblyname: AssemblyName.Name.
  • Normalerweise keine Fehlerauslösung. Es wird erwartet, dass diese Funktionen null zurückgeben, anstatt einen Fehler auszulösen, dass die angeforderte Abhängigkeit nicht gefunden werden kann. Durch das Auslösen wird die Suche vorzeitig beendet und eine Ausnahme an den Aufrufer zurückgegeben. Die Auslösung sollte auf unerwartete Fehler wie eine beschädigte Assembly oder Fälle mit nicht genügend Arbeitsspeicher beschränkt sein.
  • Vermeidung von Rekursion. Beachten Sie, dass diese Funktionen und Handler die Laderegeln für die Suche nach Abhängigkeiten implementieren. Ihre Implementierung sollte keine APIs aufrufen, die Rekursion auslösen. Ihr Code sollte in der Regel Ladefunktionen von AssemblyLoadContext aufrufen, die ein bestimmtes Pfad- oder Speicherverweisargument erfordern.
  • Laden in den richtigen AssemblyLoadContext. Die Wahl des richtigen Orts für das Laden von Abhängigkeiten ist anwendungsspezifisch. Die Auswahl wird durch diese Ereignisse und Funktionen implementiert. Wenn Ihr Code AssemblyLoadContext-Funktionen vom Typ „Laden nach Pfad“ aufruft, sollten Sie dafür die Instanz verwenden, in die der Code geladen werden soll. In einigen Fällen besteht die einfachste Lösung darin, null zurückzugeben und das Laden AssemblyLoadContext.Default zu überlassen.
  • Berücksichtigung von Thread-Races. Das Laden kann von mehreren Threads ausgelöst werden. Der AssemblyLoadContext verarbeitet Thread-Races, indem er die Assemblys einzeln im Cache hinzufügt. Die Instanz des letztes Threads wird dann verworfen. Fügen Sie in Ihrer Implementierungslogik keine zusätzliche Logik hinzu, die nicht ordnungsgemäß mehrere Threads verarbeiten kann.

Wie werden dynamische Abhängigkeiten isoliert?

Jede AssemblyLoadContext-Instanz stellt einen eindeutigen Geltungsbereich für Assembly-Instanzen und Type-Definitionen dar.

Es gibt keine binäre Isolation zwischen diesen Abhängigkeiten. Sie werden nur isoliert, da sie sich gegenseitig nicht anhand des Namens finden können.

In jedem AssemblyLoadContext gilt Folgendes:

Freigegebene Abhängigkeiten

Abhängigkeiten können problemlos von mehreren AssemblyLoadContext-Instanzen gemeinsam genutzt werden. Im allgemeinen Modell lädt ein AssemblyLoadContext eine Abhängigkeit. Der andere verwendet die Abhängigkeit über einen Verweis auf die geladene Assembly.

Diese Form der gemeinsamen Verwendung ist für die Runtimeassemblys erforderlich. Diese Assemblys können nur in den AssemblyLoadContext.Default geladen werden. Dasselbe gilt auch für Frameworks wie ASP.NET, WPF oder WinForms.

Es wird empfohlen, gemeinsam genutzte Abhängigkeiten in AssemblyLoadContext.Default zu laden. Diese Form der gemeinsamen Nutzung ist das gängige Entwurfsmuster.

Die gemeinsame Nutzung wird im Code der benutzerdefinierten AssemblyLoadContext-Instanz implementiert. AssemblyLoadContext verfügt über verschiedene Ereignisse und virtuelle Funktionen, die überschrieben werden können. Wenn eine dieser Funktionen einen Verweis auf eine Assembly-Instanz zurückgibt, die in einer anderen AssemblyLoadContext-Instanz geladen wurde, wird die Assembly-Instanz gemeinsam verwendet. Der Standardladealgorithmus orientiert sich für das Laden am AssemblyLoadContext.Default, um das Muster für die gemeinsame Nutzung zu vereinfachen. Weitere Informationen finden Sie unter Ladealgorithmus für verwaltete Assemblys.

Probleme bei der Typkonvertierung

Wenn zwei AssemblyLoadContext-Instanzen Typdefinitionen mit demselben name enthalten, sind sie nicht vom selben Typ. Sie weisen nur dann denselben Typ auf, wenn sie von derselben Assembly-Instanz stammen.

Noch komplizierter wird es, da die Meldungen zu Ausnahmen bei diesen nicht übereinstimmenden Typen verwirrend sein können. Auf die Typen wird in den Ausnahmemeldungen anhand ihrer einfachen Typnamen verwiesen. In diesem Fall lautet die allgemeine Ausnahmemeldung in etwa folgendermaßen:

Ein Objekt vom Typ „IsolatedType“ kann nicht in den Typ „IsolatedType“ konvertiert werden.

Probleme bei der Debugtypkonvertierung

Bei einem Paar nicht übereinstimmender Typen ist es darüber hinaus wichtig, Folgendes zu wissen:

Bei den beiden Objekten a und b ist die Auswertung der folgenden Informationen im Debugger hilfreich:

// In debugger look at each assembly's instance, Location, and FullName
a.GetType().Assembly
b.GetType().Assembly
// In debugger look at each AssemblyLoadContext's instance and name
System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(a.GetType().Assembly)
System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(b.GetType().Assembly)

Beheben von Problemen bei der Typkonvertierung

Es gibt zwei Entwurfsmuster für das Beheben dieser Typkonvertierungsprobleme.

  1. Verwenden Sie allgemeine freigegebene Typen. Der jeweilige freigegebene Typ kann ein primitiver Runtimetyp sein, oder Sie können einen neuen freigegebenen Typ in einer freigegebenen Assembly erstellen. Häufig handelt es sich bei dem freigegebenen Typ um eine Schnittstelle, die in einer Anwendungsassembly definiert ist. Weitere Informationen finden Sie unter Wie werden Abhängigkeiten gemeinsam genutzt?

  2. Verwenden Sie Marshallingtechniken, um Typen zu konvertieren.