Search code examples
c#reflectioncasting.net-7.0

Why am I getting a casting error in .NET 7 after using reflection?


Is reflection and casting different in .NET 7 vs .NET Framework? I'm porting a project and getting this casting error after moving the code over. The wierd thing is this class implements that interface. This code works in .NET 4.x.

    foreach (Assembly pluginAssembly in pluginAssemblies)
    {
        try
        {
            // Look for class(s) with our interface and construct them
            foreach (var type in pluginAssembly.GetTypes())
            {
                Type iDesigner = type.GetInterface(typeof(IFireworksDesigner).FullName);
                if (iDesigner != null)
                {
                    object instance = Activator.CreateInstance(type); // creates an object 
                    IFireworksDesigner designer  = (IFireworksDesigner)instance; // throws an exception
                    // do stuff
                }
            }
        }
        catch(Exception ex)
        {
            //Something really bad must have happened. 
            MessageBox.Show("Fatal error reflecting plugins in assembly '" + pluginAssembly.FullName + "'.\r\n" +
                "The error message is:\r\n\r\n" + ex.Message);
        }
    }

Update: I've made a sample repos at https://github.com/chrpai/reflection

With a FW48 EXE calling .NET Standard 2.0 it works. With a Core7 EXE calling either a .NET Standard 2.0 or .NET 7 DLL it fails.


Solution

  • I got the reason, the conclusion is the interface the class implemented and the interface you try to cast to exist in different AssemblyLoadContexts, so it fails.


    Details

    The documentation of LoadFile says:

    LoadFile does not load files into the load-from context

    That means LoadFile will load an assembly into a new context, the source code also verifies this.

    AssemblyLoadContext alc = new IndividualAssemblyLoadContext($"Assembly.LoadFile({normalizedPath})");
    result = alc.LoadFromAssemblyPath(normalizedPath);
    

    After you load LibCore.dll by LoadFile, there are 2 AssemblyLoadContexts exist in the program, both of them have an interface IPlugin with the same name and same assembly name. So the code type.GetInterface(typeof(IPlugin).FullName) even using AssemblyQualifiedName is not able to distinguish between different contexts.

    Using the following code to verify.

    // IndividualAssemblyLoadContext #2
    Console.WriteLine(AssemblyLoadContext.GetLoadContext(iDesigner.Assembly));
    // DefaultAssemblyLoadContext #0
    Console.WriteLine(AssemblyLoadContext.GetLoadContext(typeof(IPlugin).Assembly));
    

    Solution 1

    Use LoadFrom instead of LoadFile. LoadFrom loads an assembly into load-from context, so the interface can be shared.

    pluginAssemblies.Add(Assembly.LoadFrom(file));
    

    Solution 2

    Use an individal core library, for example you can have 3 projects.

    ConsoleCoreCore.exe
    
    LibCore.dll
        public interface IPlugin
        public class Server
    
    Lib1.dll
        public class LibStandardPlugin : IPlugin
    

    Both Lib1.dll and ConsoleCoreCore.exe refer to LibCore.dll

    ConsoleCoreCore.exe -> LibCore.dll <- Lib1.dll
    

    Now if you load Lib1.dll from LibCore.dll by LoadFile, Lib1.dll will be loaded into an individal context, but its IPlugin interface is still the one from LibCore.dll.

    // IndividualAssemblyLoadContext #1
    Console.WriteLine(AssemblyLoadContext.GetLoadContext(type.Assembly));
    // DefaultAssemblyLoadContext #0
    Console.WriteLine(AssemblyLoadContext.GetLoadContext(iDesigner.Assembly));
    // DefaultAssemblyLoadContext #0
    Console.WriteLine(AssemblyLoadContext.GetLoadContext(typeof(IPlugin).Assembly));