Back in the day, I remember creating new AppDomain
instances to control the scope of loaded assemblies. If I wanted to load some assemblies, then release them, that was the trick to getting that to work.
Enter .NET Core, and that approach no longer works. From what I understand, Microsoft has no plans to add that capability to .NET Core anytime soon.
So, is it still possible to load, and then unload, .NET assemblies, at runtime, using .NET Core? The short answer is yes! But, Microsoft left parts of the answer as an exercise for the rest of us to figure out. Not a problem though, we’ll do that together.
Here is the listing for the assembly loader I came up. I’ll walk through the code below the listing.
public class AssemblyLoader : AssemblyLoadContext { private string _baseDirectory; private string _dynamicDirectory; private string _relativeSearchDirectory; private string _applicationDirectory; private AssemblyDependencyResolver _resolver; public AssemblyLoader( string baseDirectory, string assemblyContextName, bool isCollectible ) : base(assemblyContextName, isCollectible) { Guard.Instance().ThrowIfNullOrEmpty( baseDirectory, nameof(baseDirectory) ).ThrowIfInvalidFolderPath( baseDirectory, nameof(baseDirectory) ); _baseDirectory = baseDirectory; _dynamicDirectory = AppDomain.CurrentDomain.DynamicDirectory; _relativeSearchDirectory = AppDomain.CurrentDomain.RelativeSearchPath; _applicationDirectory = AppDomain.CurrentDomain.SetupInformation.ApplicationBase; _resolver = new AssemblyDependencyResolver( baseDirectory ); } public AssemblyLoader( string assemblyContextName, bool isCollectible ) : base(assemblyContextName, isCollectible) { _baseDirectory = AppDomain.CurrentDomain.BaseDirectory; _dynamicDirectory = AppDomain.CurrentDomain.DynamicDirectory; _relativeSearchDirectory = AppDomain.CurrentDomain.RelativeSearchPath; _applicationDirectory = AppDomain.CurrentDomain.SetupInformation.ApplicationBase; _resolver = new AssemblyDependencyResolver( _baseDirectory ); } public AssemblyLoader( bool isCollectible ) : base(isCollectible) { _baseDirectory = AppDomain.CurrentDomain.BaseDirectory; _dynamicDirectory = AppDomain.CurrentDomain.DynamicDirectory; _relativeSearchDirectory = AppDomain.CurrentDomain.RelativeSearchPath; _applicationDirectory = AppDomain.CurrentDomain.SetupInformation.ApplicationBase; _resolver = new AssemblyDependencyResolver( _baseDirectory ); } public AssemblyLoader() { _baseDirectory = AppDomain.CurrentDomain.BaseDirectory; _dynamicDirectory = AppDomain.CurrentDomain.DynamicDirectory; _relativeSearchDirectory = AppDomain.CurrentDomain.RelativeSearchPath; _applicationDirectory = AppDomain.CurrentDomain.SetupInformation.ApplicationBase; _resolver = new AssemblyDependencyResolver( _baseDirectory ); } protected override Assembly Load( AssemblyName assemblyName ) { Guard.Instance().ThrowIfNull( assemblyName, nameof(assemblyName) ); var assemblyPath = _resolver.ResolveAssemblyToPath( assemblyName ); if (false == string.IsNullOrEmpty(assemblyPath)) { return LoadFromAssemblyPath( assemblyPath ); } if (false == string.IsNullOrEmpty(_baseDirectory)) { var completePath = Path.Combine( _baseDirectory, $"{assemblyName.Name}.dll" ); if (File.Exists(completePath)) { return Assembly.LoadFrom(completePath); } } if (false == string.IsNullOrEmpty(_applicationDirectory)) { var completePath = Path.Combine( _applicationDirectory, $"{assemblyName.Name}.dll" ); if (File.Exists(completePath)) { return Assembly.LoadFrom(completePath); } } if (false == string.IsNullOrEmpty(_relativeSearchDirectory)) { var completePath = Path.Combine( _relativeSearchDirectory, $"{assemblyName.Name}.dll" ); if (File.Exists(completePath)) { return Assembly.LoadFrom(completePath); } } if (false == string.IsNullOrEmpty(_dynamicDirectory)) { var completePath = Path.Combine( _dynamicDirectory, $"{assemblyName.Name}.dll" ); if (File.Exists(completePath)) { return Assembly.LoadFrom(completePath); } } return null; } protected override IntPtr LoadUnmanagedDll( string unmanagedDllName ) { Guard.Instance().ThrowIfNullOrEmpty( unmanagedDllName, nameof(unmanagedDllName) ); var libraryPath = _resolver.ResolveUnmanagedDllToPath( unmanagedDllName ); if (false == string.IsNullOrEmpty(libraryPath)) { return LoadUnmanagedDllFromPath( libraryPath ); } return IntPtr.Zero; } }
The class derives from the AssemblyLoadContext
class, which is a Microsoft class from the System.Runtime.Loader NUGET package. That class is abstract, which is why we have to derive from it and add our own code to make everything work.
The AssemblyLoadContext
class has three constructors, so I went ahead and exposed those on my concrete class. Each one of my constructors passed parameters to the base ctor and the, also initializes the variables I’ve added to my class. _dynamicDirectory
, _relativeSearchDirectory
, and _applicationDirectory
are all copied from the current AppDomain
, whereas _baseDirectory
is copied from the ctor argument. assemblyContextName
is used by the AssemblyLoadContext
class to create a named context, for loaded assemblies. isCollectible
is a flag that tells the AssemblyLoadContext
class whether we need to be able to unload assemblies, or not.
All the constructors eventually create an instance of an AssemblyDepdencyResolver
object, which is stored in the _resolver
field of my class. We’ll need that object when we start loading assemblies.
The Load
method accepts an AssemblyName
object as an argument. We start by passing that assembly name to our AssemblyDependencyResolver
instance, to try to convert the name to a physical file path. By doing that, we’re essentially using standard .NET to try to resolve the location of the named assembly. Luckily, most of the time, that works just fine. When it does, we use the resulting file path to load the assembly, using a call to LoadAssemblyFromPath
method, which is part of the base class.
If we fail to convert the assembly name to a file path, using the AssemblyDependencyResolver
instance, then we need to get creative. In that case, the first thing we do is use whatever path is in the _baseDirectory
field, to try to locate the assembly file.
If that fails, we then move on to use whatever path is in the _applicationDirectory
field. If that fails, we then try to use whatever path is in the _relativeSearchDirectory
field. If that also fails, we then try to use whatever path is in the _dynamicDirectory
field.
If all those paths fail, and we still can’t locate the assembly, then we give up and return NULL. At that point, your private .NET assembly is hidden too well and we simply can’t find it.
The only other method on the AssemblyLoader
class is called LoadUnmanagedDll
. This method does exactly what it claims to, loading non-.NET assemblies by name. To be candid, I don’t really use this functionality, since I don’t work with many non-.NET, from C# projects. Still, the feature is there, if you need it.
Using the AssemblyLoader
class couldn’t be easier. Here is a quick example:
var loader = new AssembylLoader("path to my private assemblies"); var asm = loader.LoadAssemblyByName("myassembly.dll"); // In case you also need to unload the assembly, later ... loader.Unload();
The AssemblyLoader
class is part of my CG.Runtime NUGET package. Go HERE to find the source. Go HERE to find the package.
Thanks for reading!
Photo by Markus Spiske on Unsplash