In this lesson, you will learn how to use multiple AssemblyLoadContext instances to isolate different versions of the same library within a single process.
Managing Dependency Isolation
In earlier lessons, we discussed how the .NET runtime resolves types and manages the execution environment. By default, .NET uses a Default load context that holds every assembly loaded by your application. However, imagine you are building a plugin-based system where two different plugins require different versions of the same library—for example, Newtonsoft.Json version 12 and version 13.
If you load both into the Default context, you will encounter a type conflict because the runtime cannot distinguish between two versions of the same assembly in a single shared space. This is where creating custom AssemblyLoadContext instances becomes essential.
How Isolation Works
An AssemblyLoadContext acts as a logical sandbox. When you create a custom instance, you tell the runtime, "Keep these assemblies separated from the others."
Here is a simplified pattern for creating a custom context:
using System.Runtime.Loader;
using System.Reflection;
public class PluginLoadContext : AssemblyLoadContext
{
private AssemblyDependencyResolver _resolver;
public PluginLoadContext(string pluginPath) : base(isCollectible: true)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly? Load(AssemblyName assemblyName)
{
string? assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
return assemblyPath != null ? LoadFromAssemblyPath(assemblyPath) : null;
}
}
By inheriting from AssemblyLoadContext, you override the Load method to define exactly where the runtime should search for dependencies. When you instantiate two separate PluginLoadContext objects for different plugins, the runtime treats the assemblies loaded by each context as entirely unique types, even if they share the same name and version. This allows your application to handle complex, heterogeneous environments without forcing "version pinning" on your entire process.
When you are finished with a plugin, you can call Unload() on its specific context, allowing the garbage collector to reclaim the memory used by those assemblies—a feat impossible with the Default context.
Key Takeaway: By utilizing custom AssemblyLoadContext instances, you create isolated silos that allow a single process to host conflicting dependency versions safely and reclaim resources through unloading.