I'm working on a plug-in system and users are very likely to present new assemblies containing classes identically named right down to the namespace because they are installing a newer version of something already installed.
What is the expected behaviour when you load an assembly that contains classes already defined by other assemblies already loaded?
In DNFX this was clearly defined by AppDomains but they have been replaced in netcore by AssemblyLoadContexts which are not assembly execution contexts.
When I load the same assembly twice no exception is reported but I'd like to know whether I have replaced the classes or had no effect, and any other semantics of this. Suggested research terms or links to relevant documentation would be very welcome with your answer.
So far I've found this: https://github.com/dotnet/runtime/issues/39783
It appears to say that using AssemblyLoadContexts you can load an assembly more than once, but it says nothing about what that means for two classes with identically full names, for code that instantiates them.
I suppose when I load an assembly object and explicitly use it to CreateInstance I will get the class defined in that assembly, but I would still like to see documentation on this.
I think I've resolved this for myself.
The following experiment
using System;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
namespace reload
{
class Program
{
static void Main(string[] args)
{
var alc1 = new ALC();
var alc2 = new ALC();
Assembly assy1, assy2;
using (var stream = new FileStream("./assy.dll", FileMode.Open))
assy1 = alc1.LoadFromStream(stream);
using (var stream = new FileStream("./assy.dll", FileMode.Open))
assy2 = alc2.LoadFromStream(stream);
var currentDomain = AppDomain.CurrentDomain;
foreach (var item in currentDomain.GetAssemblies())
{
Console.WriteLine($"{item.FullName}");
}
Console.WriteLine(assy1 == assy2);
}
}
public class ALC : AssemblyLoadContext
{
public ALC() : base(isCollectible: true) { }
protected override Assembly Load(AssemblyName name) => null;
}
}
produces this
System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
reload, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
System.Runtime, Version=4.2.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
System.Runtime.Loader, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
System.Runtime.Extensions, Version=4.2.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
System.Console, Version=4.1.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
(2) XYZ.StepHandlers, Version=1.2.3.4, Culture=neutral, PublicKeyToken=null
False
From this result it is clear that there are two distinct XYZ.StepHandlers
assemblies loaded, and they are not the same object.
Looking at the documentation for Assembly.CreateInstance
If the runtime is unable to find typeName in the Assembly instance, it returns null instead of throwing an exception.
This confirms my suspicion that calling CreateInstance
on an assembly object would resolve the type strictly within that assembly object. In normal code with assemblies project-referenced, resolution of the providing assembly depends on the assumption that an assembly is loaded exactly once.
In the plug-in scenario, we can violate this assumption. But we already have an explicit reference to the assembly so there's no ambiguity.
In case you were wondering, I don't let them load duplicates gratuitously. When I get a new assembly I have to load it to pull the assembly name and version and work out whether it's new or replaces an existing one (not to mention whether it's actually a compatible plug-in and not some random DLL).