Hier geht es um eine Typumwandlungsschwierigkeit beim Nutzen mehrerer Assembly-Ladekontexte. Der gezeigte Code liegt auch im GitHub.
Assembly-Ladekontexte, eigentlich „assembly load context“ (ALC), werden mit .NET-Assemblies beladen. Viele Programme kommen mit einem ALC aus, dem Default-ALC. Die Nutzung zusätzlicher ALC erlaubt das Laden unterschiedlicher Versionen der selben Assembly. Das kann für das Schreiben von Plugins nützlich sein. Ferner erhält man mehrere ALC wenn man .NET-Assemblies per load_assembly_and_get_function_pointer_fn aufruft.
Die Schwierigkeit besteht darin, dass zur Laufzeit Typen aus unterschiedlichen Assembly-Instanzen verschieden sind, selbst wenn diese den selben Quelltext haben beziehungsweise aus der selben Datei geladen wurden. Das heißt, dass eine Umwandlungen (cast) eines Objekts aus der einen Assembly-Instanz hin zu einem quellgleichen Typ aus einer anderen Assemblyinstanz fehlschlägt mit einer InvalidCastException. Eine solche wird durch das folgende Programm ausgelöst. Darüber hinaus, zeigt das Programm verschiedene Wege, ein Objekt aus einem anderen ALC zu nutzen und dabei die direkte Umwandlung und damit die Exception zu vermeiden.
using static System.Console; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Loader; using System; namespace App { static class Program { static void Main(string[] args) { AssemblyLoadContext defaultContext = AssemblyLoadContext.Default; AssemblyLoadContext redContext = new AssemblyLoadContext(redContextName); AssemblyLoadContext blueContext = new AssemblyLoadContext(blueContextName); Assembly blueLib = blueContext.LoadFromAssemblyPath(LibPath); Assembly redLib = redContext.LoadFromAssemblyPath(LibPath); defaultContext.FindLib(); blueContext.FindLib(); redContext.FindLib(); object? blueTime = blueLib.GetTimeNow(); object? redTime = redLib.GetTimeNow(); WriteLine(blueTime?.ToString() ?? string.Empty); try { // The static cast throws an InvalidCastException. Lib.Time? blueTimeRef = (Lib.Time?)blueTime; } catch (System.InvalidCastException e) { WriteLine(e); WriteLine("Note that the types of blueTime and redTime {0}.", blueTime?.GetType().Equals(redTime) ?? false ? "equal" : "differ"); } // Reflection in the extension method below. blueTime?.ToUnixTimeMilliseconds()?.WriteUnixTime(); // Dynamic binding. dynamic? dynamicBlueTime = blueTime; WriteUnixTime(dynamicBlueTime?.ToUnixTimeMilliseconds()); // Using common types loaded to the default load context // such as a delegate AppLib.UnixTimeMilliseconds? func = dynamicBlueTime?.UnixTimeMilliseconds; func?.Invoke().WriteUnixTime(); // or an interface. AppLib.Time? blueTimeAgain = (AppLib.Time?)blueTime; blueTimeAgain?.ToUnixTimeMilliseconds().WriteUnixTime(); // Or marshal to the Lib.Time in the default ALC. Lib.Time? copiedBlueTime = func is not null ? Lib.Time.Of(func()) : null; copiedBlueTime?.ToUnixTimeMilliseconds().WriteUnixTime(); } const string blueContextName = "blue"; const string redContextName = "red"; static void WriteUnixTime(this long time) => WriteLine($"UNIX time is {time}ms."); static long? ToUnixTimeMilliseconds(this object? libTime) => (long?)libTime?.GetType()?.GetRuntimeMethod("ToUnixTimeMilliseconds", new Type[0])?.Invoke(libTime, null); static string LibPath => Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty, "Lib.dll"); static void FindLib(this AssemblyLoadContext context) => WriteLine($"The {context.Name} ALC has {context.GetLib()?.GetName().ToString() ?? "no Lib assembly"}."); static Assembly? GetLib(this AssemblyLoadContext context) => context.Assemblies.Where(a => a.FullName?.StartsWith("Lib,") ?? false).FirstOrDefault(); static object? GetTimeNow(this Assembly lib) => lib.GetType("Lib.Time")?.GetRuntimeProperty("Now")?.GetValue(null); } }
Das Programm lädt Lib Assembly-Instanzen. Von diesen holt es sich Instanzen des Typs Lib.Time, dessen Code weiter unten ist. Dann wird folgendes ausgegeben
The Default ALC has Lib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.
The blue ALC has Lib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.
The red ALC has Lib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.
Nach dem dreimaligen Laden der Lib Assembly versucht das Programm eine Umwandlung blueTime zu einem Lib.Time Typ aus dem Default ALC. Die daraus resultierende Exception lautet
System.InvalidCastException: [A]Lib.Time cannot be cast to [B]Lib.Time. Type A originates from 'Lib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' in the context 'Default' at location 'E:\source\WebLog\TypeConversionsWithALCs\App\bin\Debug\net6.0\Lib.dll'. Type B originates from 'Lib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' in the context 'Default' at location 'E:\source\WebLog\TypeConversionsWithALCs\App\bin\Debug\net6.0\Lib.dll'. at App.Program.Main(String[] args) in E:\source\WebLog\TypeConversionsWithALCs\App\Program.cs:line 32Die Meldung ist teilweise irreführend, da ein Lib.Time Typ vom blue-Kontext kommt.
Die Ausnahmemeldung gedruckt, fährt das Programm fort mit dem Aufrufen der Methode Lib.Time.ToUnixTimeMilliseconds auf dem blueTime-Objekt auf verschiedenen Weisen.
using System; namespace Lib { public class Time : AppLib.Time { readonly DateTimeOffset time; Time(DateTimeOffset time) { this.time = time; } public static Time Of(long unixTimeMilliseconds) => new Time(DateTimeOffset.FromUnixTimeMilliseconds(unixTimeMilliseconds)); public static Time Now => new Time(DateTimeOffset.Now); public override string ToString() => time.ToString(); public long ToUnixTimeMilliseconds() => time.ToUnixTimeMilliseconds(); public AppLib.UnixTimeMilliseconds UnixTimeMilliseconds => () => time.ToUnixTimeMilliseconds(); } }
Eine Weise macht sich Reflektion zunutze. Eine im Vergleich dazu weniger schreibintensive Weise benutzt dynamische Bindung. Das hat den Nachteil, dass sich die WriteUnixTime Erweiterungsmethode nicht per Punkt aufrufen läßt. Ein andere Weise arbeitet mit Typen aus einer geteilten Assemmbly im Default-ALC. Dabei wird ein Mal das Interface AppLib.Time und ein anderes der Delegat AppLib.UnixTimeMilliseconds verwendet. Beide liegen in einer Assembly namens AppLib.
namespace AppLib { public interface Time { long ToUnixTimeMilliseconds(); } public delegate long UnixTimeMilliseconds(); }
Sowohl die App-Assembly als auch die Lib-Assembly referenzieren die AppLib-Assembly. Dadurch wird die AppLib durch die Laufzeit in den Default-ALC geladen und geteilt mit dem blue-ALC.
Eine andere Weise wandelt das blueTime-Objekt zum Lib.Time Typ im Default-ALC ohne zu casten sondern durch Kopieren des Objekt-Zustands. Am Ende der Main-Methode baut das Programm eine Lib.Time-Instanz aus dem Default-ALC durch das Wiederverwenden von blueTimes Millisekunden. Damit kann die ToUnixTimeMilliseconds-Methode direkt gerufen werden.