Understand Advanced AssemblyLoadContext with C#
I shared the fundamental of AssemblyLoadContext usage with the following Post. I’d like to plugin loading advanced. usages.
Loading the different version’s library at the same time
Loading application as a plugin is an excellent feature. We can develop a framework that has extensibility. However, we can use the AssemblyLoadContext with different purpose. Think about the case you want to use the different versions of the library at the same time.
Usually, if we have a library that has a different version, it will cause binding redirect that and make these libraries to the same version. However, what if it has major version change, the class/method is different, but wants to support both versions. That is today’s challenge.
However, AssemblyLoadContext also solves this issue. Using the AssemblyLoadContext, It will load different versions libraries to the different context. So that it is possible to have it side by side.
In this example, we have ServcieBus4Plugin and ServiceBus5Plugin. Each Plugin references Microsoft.Azure.WebJobs.Extensions.ServiceBus package 4 and 5 respectively. MultiplePluginLoadingSpike package dynamically load plugins through PluginBase that is interface project. All the plugins implement the interface.
Each plugin references different version’s libraries. The Azure SDK has different namespace depending on different Web Jobs versions.
Execute the application. I can see both libraries exist in a single process side by side.
How to share the dependency between plugins and caller
However, there is one trivial problem is there. Imagine, if you want to share the ILogger object between caller and plugins. You might suppose, adding Package reference on Plugin Base might solve the issue.
For ServiceBus4 Plugin, the version of Microsoft.Extensions.Logging was 2.1.0. ServiceBus5 Plugin references 2.1.1. So that I added version 2.1.1. Build and double check the deps.json has the proper version. You will find the ServiceBusPluing.deps.json under your bin/Debug/net6.0 directory.
The file includes all the information of dependency. AssemblyLoadContext references the file when they resolve the dependency. You can find all Micorosoft.Extensions.Logging is binding redirected to 2.1.1.
I added an interface method on ICommand that is reside on the PluginBase project. That interface is referenced by all the plugins. That has ILoggerFactory. Hit F5 for a run.
Form the caller of the plugin, Simply create a loggerFactory and pass it to plugin side.
It causes an exception. It says, ServiceBusPlugin doesn’t have an implementation.
The reason it happens is, We need to understand the context for AssemblyLoadContext. If you look at the bin directory for caller and plugins, you will find Microsoft.Extensions.Logging.dll. All of them are version 2.1.1.
If you don’t use AssemblyLoadContext, the dll will be read on the DefaultContext. In this case, Microsoft.Extensions.ILogger is read from Microsoft.Extensions.Logging.dll that is reside on the MultiPluginLoadingSpike bin directory. However, it is considered as different one that read from ServiceBusPluginV4/V5 directories even if it is the same. version.
Since the LoggerFactory instance is created on DefaultContext, it is not the same as the one on ServiceBusPluginV4/V5 so that, it complains V4/V5 plugin doesn’t have implementation that is used with interface, because they are implement the same version’s library on the different context.
Solution
We have two solutions for solving this issue.
Write a logic on AssemblyLoadContext
PluginLoadContext (github.com)
AssemblyLoadContext can be overwrite the methods. For example, On Load() method, when you read a specific Assembly, you can read from a specific directory. If you don’t have many Assemblies that you want to share among the caller and plugins, you can use this method.
Remove the duplicated DLLs.
If you have a lot of DLLs that you want to share among the caller and plugins. You can directly remove the duplicated DLLs. We can prevent to generate DLLs on our plugin project by writing csproj with Private as false and ExcludeAssets as runtime. This is available for PackageReference. However, in this case, we want to keep the Microsoft.Azure.WebJobs.Extensions.ServiceBus dependency’s DLLs but only want to remove ILogger related DLLs. so that we can’t use the method. I created a project that removes the duplication between caller and plugins. You can find it on my sample application.
<ItemGroup>
<ProjectReference Include="..\PluginBase\PluginBase.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
Once you remove all duplicated DLLs, it will solve the problem. If you are interested, you can run my sample application.
Why does this method help?
If you remove the Microsoft.Extensions.Logging.dll from plugins bin directory and leave it only on DefaultContext. If you read it from caller, it will read from DefaultContext. If you read it from ServiceBusPluginV4 context with AssemblyLoadContext, you can’t find the DLL that is written in the deps.json. Then AssemblyLoadContext will start the fallback process. That tries to read from the DefaultContext, then it solves. So that it will reference the same DLL.
Limitation
The isolation by the Context is not perfect. If you read several plugin contexts on DefaultContext, then if you use reflection on it. The reflection will find several types. If I add the following method, after loading both plugins on the caller, the reflection fails. Since ServiceBusPluginV4 and ServiceBusPluginV5 have the same class name.
If you do the same thing on ServiceBusV4Plugin context, it is ok.
Conclusion
Our usage is limited, and we can’t accept a lot of plugins, so we might use this method. However, since there is limitation for the reflection, we need to be careful to share the libraries that passed between caller and plugins. Also need to be testable, God knows if reflection is used on the DefaultContext with the dependency. The interface project of plugins should be thin as possible might be a promising idea. However, you might enjoy to learn how the AssemblyLoadContext behave.