Description
Description
When you create a MethodInfo
from a generic method in a non-collectible assembly, then call MakeGenericMethod
with a type from a collectible assembly and use the returned MethodInfo
in System.Linq.Expressions.Expression.Call
the latter assembly will never be unloaded.
Reproduction Steps
Create an assembly SomeCollectibleAssembly
with a single class in it:
namespace SomeCollectibleAssembly;
public class SomeType
{ }
Compile this assembly and note the path of the compiled assembly. Then create a console application with the following code:
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
namespace MinimalRepro;
internal static class Program
{
private const string ASSEMBLY_PATH = """C:\Path\To\SomeCollectibleAssembly.dll""";
private const string ASSEMBLY_NAME = "SomeCollectibleAssembly";
private static void Main(string[] args)
{
WorkWithCollectibleAssembly();
while (true)
{
if (!IsAssemblyLoaded(ASSEMBLY_NAME))
{
Console.WriteLine("The assembly has been unloaded.");
break;
}
Console.WriteLine("The assembly has not been unloaded, collecting ...");
GC.Collect();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void WorkWithCollectibleAssembly()
{
var assembly = LoadAssembly();
var someType = assembly.GetType("SomeCollectibleAssembly.SomeType")
?? throw new Exception("SomeType not found");
var someGenericMethod = typeof(Program).GetMethod(nameof(SomeGenericMethod), BindingFlags.NonPublic | BindingFlags.Static)
?? throw new Exception("SomeGenericMethod not found");
var method = someGenericMethod.MakeGenericMethod(someType);
// Commenting out this line allows unloading to succeed.
_ = Expression.Call(method);
}
private static Assembly LoadAssembly()
{
var alc = new AssemblyLoadContext(null, isCollectible: true);
try
{
return alc.LoadFromAssemblyPath(ASSEMBLY_PATH);
}
finally
{
alc.Unload();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static bool IsAssemblyLoaded(string assemblyName)
{
return AppDomain.CurrentDomain.GetAssemblies().Any(a => a.GetName().Name == assemblyName);
}
private static void SomeGenericMethod<T>() { }
}
Expected behavior
It is expected that the assembly is eventually collected by the GC and unloads, i.e. the reproduction program prints:
The assembly has not been unloaded, collecting ...
The assembly has been unloaded.
Actual behavior
The program never terminates and prints
The assembly has not been unloaded, collecting ...
The assembly has not been unloaded, collecting ...
The assembly has not been unloaded, collecting ...
The assembly has not been unloaded, collecting ...
The assembly has not been unloaded, collecting ...
The assembly has not been unloaded, collecting ...
repeatedly.
Regression?
No response
Known Workarounds
Using a non-generic method in a generic type works around the problem, i.e. changing the program as follows allows unloading to succeed:
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
namespace MinimalRepro;
internal static class Program
{
private const string ASSEMBLY_PATH = """C:\Path\To\SomeCollectibleAssembly.dll""";
private const string ASSEMBLY_NAME = "SomeCollectibleAssembly";
private static void Main(string[] args)
{
WorkWithCollectibleAssembly();
while (true)
{
if (!IsAssemblyLoaded(ASSEMBLY_NAME))
{
Console.WriteLine("The assembly has been unloaded.");
break;
}
Console.WriteLine("The assembly has not been unloaded, collecting ...");
GC.Collect();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void WorkWithCollectibleAssembly()
{
var assembly = LoadAssembly();
var someType = assembly.GetType("SomeCollectibleAssembly.SomeType")
?? throw new Exception("SomeType not found");
var method = typeof(SomeGenericType<>)
.MakeGenericType(someType)
.GetMethod(nameof(SomeGenericType<object>.SomeNonGenericMethod), BindingFlags.Public | BindingFlags.Static)
?? throw new Exception("Method not found");
_ = Expression.Call(method);
}
private static Assembly LoadAssembly()
{
var alc = new AssemblyLoadContext(null, isCollectible: true);
try
{
return alc.LoadFromAssemblyPath(ASSEMBLY_PATH);
}
finally
{
alc.Unload();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static bool IsAssemblyLoaded(string assemblyName)
{
return AppDomain.CurrentDomain.GetAssemblies().Any(a => a.GetName().Name == assemblyName);
}
private static class SomeGenericType<T>
{
public static void SomeNonGenericMethod() { }
}
}
Configuration
No response
Other information
I have investigated this unloading problem using WinDbg and have determined that the problem is the TypeExtensions.s_paramInfoCache
dictionary.
The problem lies in the parameter caching code: Instead of calling method.DeclaringType?.IsCollectible
, calling method.IsCollectible
here should fix the problem.