-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
If you execute Newtonsoft.Json.JsonConvert.Serialize() on a custom object defined in an assembly in a collectible AssemblyLoadContext, the AssemblyLoadContext can no longer be unloaded.
We noticed this issue in our testing as we are planning to use the collectible AssemblyLoadContext feature to port our plugin code from .NET framework to .NET Core 3.0. Our plugin code currently uses AppDomains and Json.NET for serializing the parameters and results for the plugin calls. Furthermore, the parameter structure classes are defined in the plugin assemblies, so this issue now blocks our .NET Core plugins from unloading.
The unload problem seems to be caused by TypeDescriptor caching: JsonConvert.Serialize() internally seems to call TypeDescriptor.GetConverter(type) on the custom object type, which adds it to the static TypeDescriptor caches. As the TypeDescriptor is loaded in the default LoadContext, the TypeDescriptor caches will keep the references to the custom types alive, and block the plugin assembly from unloading.
A simple reproduction based on the Unloading sample can be found here: https://github.com/jvuoti/samples/tree/jsonconvert_blocking_assemblyloadcontext_unload/core/tutorials/Unloading
In the sample, the Logger plugin dependency has been modified to serialize a CustomLogMessage object, also defined in the same plugin assembly:
public class Logger
{
public class CustomLogMessage
{
public string LogMessage { get; set; }
}
public static void LogMessage(string msg)
{
var logMsg = JsonConvert.SerializeObject(new CustomLogMessage {LogMessage = msg});
Console.WriteLine(logMsg);
}
}
Once the modified plugin code is called, the sample can no longer unload the AssemblyLoadContext.
The only way we have found to get the AssemblyLoadContext to unload is to first clear the internal TypeDescriptor caches via reflection, like this:
var typeConverterAssembly = typeof(TypeConverter).Assembly;
var reflectTypeDescriptionProviderType = typeConverterAssembly.GetType("System.ComponentModel.ReflectTypeDescriptionProvider");
var reflectTypeDescriptorProviderTable = reflectTypeDescriptionProviderType.GetField("s_attributeCache", BindingFlags.Static | BindingFlags.NonPublic);
var attributeCacheTable = (Hashtable)reflectTypeDescriptorProviderTable.GetValue(null);
attributeCacheTable.Clear();
However, this does not really feel right :)
So are we just doing things wrong, and would there be a simpler workaround for this? E.g. is there some way we could force a copy of the TypeConverter to be loaded inside the collectible AssemblyLoadContext, so the caches would also be inside it?