5

I'm trying to upgrade a .NET Framework class library to .NET 6 and have run into some issues around AppDomains. I didn't write the library originally, but my understanding is that it creates an AppDomain separate from the one the .NET runtime created for it, instantiates some of its own types using this new app domain by calling AppDomain.CreateInstanceAndUnwrap and those instances then load other third-party assemblies for inspection. I presume the reason it instantiated its own types in this way was to completely isolate the code processing the third-party assemblies and those assemblies themselves from the library's execution context.

Reading documentation and .NET blogs, I've learned that AppDomains were retired as of .NET Core or later, and the correct way to load assemblies in an isolated fashion is to use AssemblyLoadContexts. I've therefore overridden AssemblyLoadContext with my own subclass, and used that to load the library's assembly, instantating its types using Assembly.CreateInstance and using those instances to load third-party assemblies, as what I've read online suggests this is the cleanest way to ensure you're loading assemblies into a context separate to your code's own.

There are a couple of uncertainties around this approach - I'm not sure if it's still necessary to instantiate our library's types in a separate context. If it is, I have a problem around casting the instances from System.Object returned by Assembly.CreateInstance to their correct original types.

Here is my AssembyLoadContext derivative:

namespace MyNamespace
{
    class MyLoadContext : AssemblyLoadContext
    {
        private AssemblyDependencyResolver _resolver;
        public MyLoadContext(string basePath)
        {
            _resolver = new AssemblyDependencyResolver(basePath);

           
        }

        protected override Assembly Load(AssemblyName assemblyName)
        {
            string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
            if(null != assemblyPath)
            {
                return LoadFromAssemblyPath(assemblyPath);
            }

            return null;

        }
    }

}

Here is my class which tries to other load types from the same assembly as the class:

namespace MyNamespace
{

    public class MyClass
    {

        MyClass1 _ClassToLoad1
        MyClass2 _ClassToLoad2;
        
        MyLoadContext _assemblyLoadContext;
                        

        
        public MyClass()
        {
        
        }
        
        public LoadClasses
        {
            _assemblyLoadContext = new MyLoadContext((Assembly.GetExecutingAssembly().Location));
            Assembly assembly = _assemblyLoadContext.LoadFromAssemblyName(new AssemblyName("MyAssembly"));
            
            
            dynamic myClassObj1 = assembly.CreateInstance("MyNamespace.MyClass1");
            dynamic myClassObj2 = assembly.CreateInstance("MyNamespace.MyClass2");
            
            _classToLoad1 = (MyClass1) myClassObj1;
            _classToLoad2 = (MyClass2) myClassObj2;

            
        }
        

    }


}

When I try and cast myClassObj1 and myClassObj2 to instances of MyClass1 and MyClass2, it throws an InvalidCastException because the objects reside in a different AssemblyLoadContext to the code performing the cast.

I've read that the .NET runtime treats types created in different AssemblyLoadContexts as unequal, so the cast fails and assigns null to variables with the correct type, even though they came from the same assembly and have the same name and implementation. I'm not sure how to address this issue, although I assign the result of Assembly.Create instance to a dynamic and invoke that, which seems like a hack.

I read one way is to externalise the types being cast into their own assembly, but this seems like overkill and would mean extra overhead managing yet another library. I also don't know why this wasn't a problem when using separate AppDomains which achieve the same thing conceptually.

ericc
  • 334
  • 1
  • 14
  • 1
    Can you provide some more details, a [Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example) would be ideal, but at least sketch out a class definition in one assembly and also example of "a problem around casting the instances" – BurnsBA Oct 06 '22 at 14:32
  • 1
    The exact same casting exception risk exists for AppDomains. So if the original codebase doesn't throw then you wouldn't expect it either after porting to ALC. Set lasers to stun. – Hans Passant Oct 06 '22 at 18:03

1 Answers1

1

I believe your problem goes back to

Types are per-assembly; if you have "the same" assembly loaded twice, then types in each "copy" of the assembly are not considered to be the same type.

https://stackoverflow.com/a/2500820/1462295

It's hard to tell, but I'm thinking from your code that the assembly you are trying to load is already loaded. Otherwise the types you are trying to cast to wouldn't exist. (Side note, since the type is known, you can skip the dynamic which it looks like you're only using to avoid the cast exception). This can happen if you have a project reference to another assembly, but you also try to load this other assembly at runtime.

You can check if the assembly is already loaded at run time with something like

AppDomain.CurrentDomain.AssemblyLoad += CurrentDomain_AssemblyLoad;

private static void CurrentDomain_AssemblyLoad(object? sender, AssemblyLoadEventArgs args)
{
    var loadedAssemblies = System.Reflection.Assembly.GetExecutingAssembly().GetReferencedAssemblies().Select(x => x.FullName);

    if (loadedAssemblies.Contains(args.LoadedAssembly.FullName))
    {
        throw new InvalidOperationException();
    }
}

You may or may not fix the issue using "Strong Naming" ~ Assembly loading Problem ("Could not load type")

So you can check for the assembly, or load dynamically if it's not already available.

_assemblyLoadContext = new MyLoadContext((Assembly.GetExecutingAssembly().Location));

var assemblyName = AssemblyLoadContext.GetAssemblyName("MyAssembly.dll");
Assembly assembly = null;

// this is actually "AssemblyName", so still need to resolve to "Assembly" type
var alreadyLoadedAssembly = System.Reflection.Assembly.GetExecutingAssembly().GetReferencedAssemblies().FirstOrDefault(x => x.FullName == assemblyName.FullName);

if (object.ReferenceEquals(null, alreadyLoadedAssembly))
{
    assembly = _assemblyLoadContext.LoadFromAssemblyName(assemblyName);
}
else
{
    assembly = System.Reflection.Assembly.GetExecutingAssembly();
}

MyClass1 _classToLoad1 = (MyClass1)assembly.CreateInstance("MyNamespace.MyClass1");
MyClass2 _classToLoad2 = (MyClass2)assembly.CreateInstance("MyNamespace.MyClass2");
BurnsBA
  • 4,347
  • 27
  • 39
  • Yes, MyClass, MyClass1 and MyClass2 are all implemented in MyAssembly.dll so my code is effectively loading the same assembly twice. Is there a way to instantiate types from an assembly in a separate AssemblyLoadContext, without having to load that assembly again? Or maybe the question should be whether there is a way to instantiate types in a separate execution context equivalent to AppDomains without requiring AssemblyLoadContexts? – ericc Oct 07 '22 at 09:01