Announcement tells us:
Assembly unloadability is a new capability of AssemblyLoadContext. This new feature is largely transparent from an API perspective, exposed with just a few new APIs. It enables a loader context to be unloaded, releasing all memory for instantiated types, static fields and for the assembly itself. An application should be able to load and unload assemblies via this mechanism forever without experiencing a memory leak.
Also, this design notes has mentioning of "statics".
I have tried this straightforward test:
static void Main()
{
Proxy.X = 15;
var alc = new AssemblyLoadContext("MyTest", true);
var asm = alc.LoadFromAssemblyName(typeof(Program).Assembly.GetName());
var proxy = (Proxy)asm.CreateInstance(typeof(Proxy).FullName);
Console.WriteLine(proxy.Increment());
}
class Proxy
{
public static int X;
public int Increment() => ++X;
}
It outputs "16", which means that isolation doesn't work.
My goal is to unit-test class static members which can throw exceptions. Usual tests can affect each other's behavior by triggering type initializers, so I need to isolate them in the cheapest possible way. Test should run on .NET Core 3.0.
Is it right way to do it, and can AssemblyLoadContext
help with it?
Yes, it does isolate static variables.
If we look at the newest design notes, we see this addition:
LoadFromAssemblyName
This method can be used to load an assembly into a load context different from the load context of the currently executing assembly. The assembly will be loaded into the load context on which the method is called. If the context can't resolve the assembly in its Load method the assembly loading will defer to the Default load context. In such case it's possible the loaded assembly is from the Default context even though the method was called on a non-default context.
Calling this method directly on the AssemblyLoadContext.Default will only load the assembly from the Default context. Depending on the caller the Default may or may not be different from the load context of the currently executing assembly.
This method does not "forcefully" load the assembly into the specified context. It basically initiates a bind to the specified assembly name on the specified context. That bind operation will go through the full binding resolution logic which is free to resolve the assembly from any context (in reality the most likely outcome is either the specified context or the default context). This process is described above.
To make sure a specified assembly is loaded into the specified load context call AssemblyLoadContext.LoadFromAssemblyPath and specify the path to the assembly file.
It's little bit frustrating, because now I need to determine the exact location of the assembly to load (there's no easy way to "clone" already loaded assemblies).
This code works (outputs "1"):
static void Main()
{
Proxy.X = 15;
var alc = new AssemblyLoadContext("MyTest", true);
var asm = alc.LoadFromAssemblyPath(typeof(Program).Assembly.Location);
var proxy = asm.CreateInstance(typeof(Proxy).FullName);
Console.WriteLine(proxy.GetType().GetMethod("Increment").Invoke(null, null));
}
class Proxy
{
public static int X;
public static int Increment() => ++X;
}
(Notice, now we can't cast to Proxy
class, because it is different from the run-time class of proxy
variable, even being the same class...)