Reputation: 406
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 AssemblyLoadContext
s. 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.
Upvotes: 8
Views: 2454
Reputation: 4939
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");
Upvotes: 2