Skip to content

Cannot unload a collectible AssemblyLoadContext if JsonConvert.Serialize() has been called on a custom object #13283

@jvuoti

Description

@jvuoti

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?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions