Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Assembly unloading after seriaization with Json.NET

We need to unload plugins for our software. Plugins are loaded into separate AssemblyLoadContext (.net core 3.0) and communicate with the main software via serialized data. If use Json.NET for serialization, references for ever serialized object's type never released and if this type is defined inside a plugin, AssemblyLoadContext never unloaded.

We managed to achieve plugin unloading with complicated reflection magic - some manual cache cleaning inside System.ComponentModel.TypeConverter assembly, but it looks like a dirty hack.

Another option is to include Newtonsoft.Json in a plugin and load it in a separate context. Though even when not only Newtonsoft.Json.dll but even System.ComponentModel.TypeConverter.dll are loaded in a separate context it doesn't help. Caches are still filled in a default context.

Does anyone knows, is there a good way to unload plugin after serialization of its types with Json.NET?

Many thanks in advance!

like image 455
Pavel Z Avatar asked Nov 15 '22 15:11

Pavel Z


1 Answers

Are you properly clearing your weak references after unloading your AssemblyLoadContext for the plugin?

Take a look at this documentation:

https://learn.microsoft.com/en-us/dotnet/standard/assembly/unloadability

However, the unload doesn't complete immediately. As previously mentioned, it relies on the garbage collector to collect all the objects from the test assembly. In many cases, it isn't necessary to wait for the unload completion. However, there are cases where it's useful to know that the unload has finished. For example, you may want to delete the assembly file that was loaded into the custom AssemblyLoadContext from disk. In such a case, the following code snippet can be used. It triggers garbage collection and waits for pending finalizers in a loop until the weak reference to the custom AssemblyLoadContext is set to null, indicating the target object was collected. In most cases, just one pass through the loop is required. However, for more complex cases where objects created by the code running in the AssemblyLoadContext have finalizers, more passes may be needed.

[MethodImpl(MethodImplOptions.NoInlining)]
static int ExecuteAndUnload(string assemblyPath, out WeakReference alcWeakRef)
{
    var alc = new TestAssemblyLoadContext();
    Assembly a = alc.LoadFromAssemblyPath(assemblyPath);

    alcWeakRef = new WeakReference(alc, trackResurrection: true);

    var args = new object[1] {new string[] {"Hello"}};
    int result = (int) a.EntryPoint.Invoke(null, args);

    alc.Unload();

    return result;
}

Executing your logic, unloading and getting weak reference:

WeakReference testAlcWeakRef;
int result = ExecuteAndUnload("absolute/path/to/your/assembly", out testAlcWeakRef);

Run Garbage collection and wait for finalizers for weak reference alive. May repeat up to 10 times if needed:

for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

Take a look at this thread too, it looks oddly similar to what you described: https://github.com/dotnet/runtime/issues/13283

like image 157
Adolfo Perez Avatar answered Dec 17 '22 11:12

Adolfo Perez