Let’ see in this blog post the new possibility offered by .NET Core 3.0 preview 2 to load and unload assemblies at run time using AssemblyLoadContext.
Here are some of the scenarios that motivated this work:
- Ability to load multiple versions of the same assembly within a given process (e.g. for plugin frameworks)
- Ability to load assemblies explicitly in a context isolated from that of the application.
- Ability to override assemblies being resolved from application context.
- Ability to have isolation of statics (as they are tied to the LoadContext)
- Expose LoadContext as a first class concept for developers to interface with and not be a magic.
In our case, the idea of this tiny project is the following:
- Watch a C# source code file for any modification using Rx and the FileWatcher
- On any change on that file, load it in memory as text
- Compile the file using Roslyn into an assembly which is kept in memory
- Execute the entry point of the assembly
- Unload the assembly
We will start with a simple hello world application, what else 😉 and we will allow the main application to pass some arguments to the dynamically compiled assembly.
First, we are using some Rx code to observe the file, I won’t go into that detail because it is not the purpose of that post. The code is coming from Wes Higbee from the repository g0t4/Rx-FileSystemWatcher.
We are using Rx-FileSystemWatcher to observe the Sources folder and filter for DynamicProgram.cs. When this file is changed, we trigger the build, load the assembly generated, find the entry point and invoke it passing “France” as the first parameter.
The main program delegates the compilation to the Compiler class, which is using Roslyn to compile the C# file DynamicProgram.cs. If there are some compilation errors, those are displayed on the console output. Otherwise, the compilation result is a Hello.dll returned as a byte array.
Then, the main program delegate to the Runner class which is in charge of loading and executing the entry point of the just compiled new assembly.
We are marking the method LoadAndExecute with [MethodImpl(MethodImplOptions.NoInlining)] so that the method cannot be inlined and to ensure that nothing would be kept alive.
We are loading the assembly using our own simple implementation of AssemblyLoadContext, this is just to mark that the context is collectible. So that we can unload the assembly using the method AssemblyLoadContext.Unload().
In fact, the unloading does not happen immediately, it will wait that the GC collect the assembly. This is why we are calling GC.Collect() and GC.WaitForPendingFinalizers() in the Execute method. This is not mandatory but in our case, we want to be sure that the previous assembly is unloaded before compiling and loading the new one.
Let’s run the application, change the file Program.cs in the folder Sources and see it working 😎
This is opening some new capabilities which we might explore in some new posts!
You can access to the whole project on Github, laurentkempe/DynamicRun.
Finally, you can read even more about it on “Using and debugging unloadability in .NET Core“ and can also have a look at this interesting project natemcmaster/DotNetCorePlugins which starts to talk about the same topic on “Make plugins unloadable“.