This text is about a type conversion difficulty when using multiple assembly load contexts. The shown code is also at GitHub.
.NET assemblies are loaded to assembly load contexts (ALCs). Usually assemblies are loaded to the default ALC. Using additional ALCs allows to load different versions of the same assembly. This is useful for apps that load plugins. One may further end up with multiple ALCs when using .NET assemblies from native programs by means of the runtime delegate load_assembly_and_get_function_pointer_fn.
The noteworthy difficulty is that types from different assembly instances differ even if the types are the same in the source and even if the two assemblies were loaded from the same file. This means that casting an object from one assembly instance to a source-equivalent type in another assembly instance fails with an InvalidCastException. Such an exception is triggered by the following program. Having caught the exception, the program demonstrates different ways to use an object from a different ALC avoiding that cast and hence the exception.
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); } }
The program loads three Lib assembly instances. From these Lib assembly instances, the program gets instances of the Lib.Time type listed below. Next, the program outputs the following lines
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.
Having loaded the Lib assembly three times, the program attempts to cast the blueTime to a Lib.Time type from the default ALC in the try block. The thrown exception reads
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 32That exception message is misleading because actually one Lib.Time type is from the blue context.
Having printed the exception the program continues to call the method Lib.Time.ToUnixTimeMilliseconds on the blueTime object in several ways.
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(); } }
One way is to call it through reflection. A less verbose way is to use dynamic binding. Note that extension methods syntax does not work with dynamic binding. Another way is to use types of a shared assembly loaded to the default ALC. The code demonstrates this with an interface AppLib.Time and a delegate AppLib.UnixTimeMilliseconds defined in the shared AppLib assembly.
namespace AppLib { public interface Time { long ToUnixTimeMilliseconds(); } public delegate long UnixTimeMilliseconds(); }
Both the App assembly and the Lib assembly statically reference and use the AppLib assembly. That way, the AppLib is loaded into the default ALC by the managed assembly loading algorithm and shared with the blue ALC.
Another way is to convert the blueTime object to the Lib.Time type in the default context through copying instead of casting. At the end of the Main method, the program constructs a Lib.Time instance in the default ALC by means of blueTime's milliseconds. The ToUnixTimeMilliseconds method of this instance can be called directly.