Understanding Dynamic Assembly Loading with AssemblyLoadContext in C#

Tsuyoshi Ushio
4 min readJul 4, 2022

--

I encountered an issue that .NET Framework app reading a plugin in runtime. One person uses Library A, and the other uses Library B. Both tests work. However, after merged the two Pull Requests, it works on visual studio, however, not in test environment. The libraries they introduce use the same library but a different version.

Usually, we found it is automatically binding redirection happens. That means the library is automatically upgraded to a newer version. However, under the dynamic assembly loading context with .NET Framework, it seems not happened. To unblock this issue, use AssemblyLoadContext class to make binding redirection works with the plugin scenario.

TL; DL

You can refer to my sample application, which includes several scenarios to do it.

TsuyoshiUshio/AssemblyLoadContextPike (github.com)

Why AssemblyLoadContext?

We used to use AppDomain for reading an Assembly dynamically. However, they have several issues.

How to: Load Assemblies into an Application Domain — .NET Framework | Microsoft Docs

We can load assembly like this. It works well for simple scenarios.

Assembly plugin = Assembly.LoadFrom("path/to/plugin.dll");

However, it causes these problems. This great blog post explains the following issues.

1. Locating Dependency assembly

2. Dependency Mismatch

3. Side by side and race conditions

4. Native library loading

These issues are exactly the issues what I am facing. NET Core and later framework introduced AssemblyLoadContext. So that I decided to move .NET 6 from .NET Framework.

What is AssemblyLoadContext?

The official documentation says.

It provides a service of locating, loading, and caching managed assemblies and other dependencies.

To support dynamic code loading and unloading, it creates an isolated context for loading code and its dependencies in their own AssemblyLoadContext instance.

Understanding AssemblyLoadContext — .NET | Microsoft Docs

It looks like a perfect match for my purpose. Also, it supports unmanaged libraries (native libraries).

How to write it?

Implement AssemblyLoadContext The following sample overwrite Load() and LoadUnmanagedDll() methods. AssemblyDependencyResolver resolves assemblies and native libraries to paths based on the dependencies of a given assembly.

AssemblyLoadContext Sample (github.com)

Once create the custom AssemblyLoadContext, You can load the Assembly.

var loadContext = new PluginLoadContext(pluginLocation);            Assembly assembly = loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(pluginLocation)));
Official Doc Sample Structure

Once you read an Assembly, you can get the types that exist in the Assembly, filter it out with the interface type, then you create the instance with reflection.

foreach(Type type in assembly.GetTypes())
{
if (typeof(ICommand).IsAssignableFrom(type))
{
ICommand? result = Activator.CreateInstance(type) as ICommand;
if (result != null)
{
count++;
yield return result;
}
}
}

Managed Assembly Loading Algorithm

You might want to know how the code resolves dependencies. Default context is the main application assembly and its static dependency.

Let’s test the behavior. I just change a dependency DLL’s name to a different name. However, if you change the DLL that is passed to AssemblyLoadContext constructor, it won’t hook anything. Just through an exception. These hooks are used for dependency resolution.

Rename the dependency for testing.

Adding the following code to handle the handler.

PluginLoadContext loadContext = new PluginLoadContext(pluginLocation);loadContext.Resolving += CustomEventHandler;
:
private static Assembly? CustomEventHandler(AssemblyLoadContext context, AssemblyName assemblyName)
{
Console.WriteLine($"Coundn't resovle {assemblyName.Name}");
return null; // Fail to read.
}

AppDomain is the old method that is used in .NET Framework, after .NET Core, We use AssemblyLoadContext. For .NET Core, there is exactly one AppDomain. We should use AssemblyLoadContext for now. However, I just wanted to test the behavior. Let’s add this code.

AppDomain currentDomain = AppDomain.CurrentDomain;
currentDomain.AssemblyResolve += CustomEventHandlerForAppDomain;
:
private static Assembly CustomEventHandlerForAppDomain(object sender, ResolveEventArgs args){
Console.WriteLine("Resolving...");
return typeof(Program).Assembly;
}

Once you execute my sample application with passing redirect argument, you will see it will hit the handler.

What happens in these cases?

Let’s test it in the following scenario.

Reading the library that has version conflict.

Let’s say, you might want to load Service Bus Extension V4 and Service Bus Extension V5 at the same time. The answer is it works.

Plugin reference the .NET Framework library from .NET 6.

Yes. If you are lucky, it works. You can compile at least. If you are unlucky, you will face a runtime error if the library use is incompatible with .NET. 6.
TsuyoshiUshio/AssemblyLoadContextPike (github.com)

Plugin with Native Libraries

Yes. No problem. Support unmanaged.

The following post will show a more advanced use case.

--

--