Create and load code at runtime in .NET Core
I think loading and compiling code at runtime is a very cool and helpful feature. That's why one of my first .NET Core projects (webpac) uses this technique to load user defined types at runtime. The dynamically created types are used to get structured access to data in a PLC.
After some troubles with breaking changes in the RC versions, I started a second project called scribi. Scribi also uses dynamically created code that gets compiled to ASP.NET ApiControllers and SignalR hubs.
To run these tools, I used some classes that were not very good documented at the time of coding. One of these classes is used to find the references of my assembly, some others to compile the code from a file and to load the compiled assembly. Compared to old .NET code I developed, the reference finding and assembly loading is quite different now. The reason is that there is no AppDomain.GetAssemblies()
and Assembly.LoadFile()
in CoreClr. It took me some time to find a solution for this .NET Core.
For this blog-post I provided a project called DynCode.Sample on GitHub, where you can try the functionality on a simple project.
Find referenced assemblies
First of all you need a reference of the dependency context which holds the references of an assembly.
using System.Reflection;
var myEntryAssembly = Assembly.GetEntryAssembly();
var context = DependencyContext.Load(myEntryAssembly );
If your context should be the context of your entry assembly (to get all references of your application) you could also use DependencyContext.Default
.
You can use the following code snipet to show all assemblies your entry assembly is compiled against. To get detailed Information about the compiled libraries, you have to make the according settings (compilationOptions.preserveCompilationContext : true
) in package.json:
"compilationOptions": {
"preserveCompilationContext": true,
"emitEntryPoint": true
},
"dependencies": {
...
"Microsoft.Extensions.DependencyModel": "1.0.1-beta-003206",
...
},
public static void ShowReferences()
{
var context = DependencyContext.Default;
if (!context.CompileLibraries.Any())
Console.WriteLine("Compilation libraries empty");
foreach (var compilationLibrary in context.CompileLibraries)
{
foreach (var resolvedPath in compilationLibrary
.ResolveReferencePaths())
{
Console.WriteLine($"Compilation {compilationLibrary.Name}:{Path.GetFileName(resolvedPath)}");
if (!File.Exists(resolvedPath))
Console.WriteLine($"Compilation library resolved to non existent path {resolvedPath}");
}
}
foreach (var runtimeLibrary in context.RuntimeLibraries)
{
foreach (var assembly in runtimeLibrary.GetDefaultAssemblyNames(context))
Console.WriteLine($"Runtime {runtimeLibrary.Name}:{assembly.Name}");
}
}
Load assemblies
In my application I would also implement the ability to load some additional assembly references (this could also be used if you support a plugin system). To do this you need another nuget package:
"dependencies": {
...
"System.Runtime.Loader": "4.0.0",
...
}
This package contains a class called AssemblyLoadContext
which is placed in the System.Runtime.Loader.dll
.
To get the current load context, you can use the static property AssemblyLoadContext.Defaul
or pass an assembly into the method AssemblyLoadContext.GetLoadContext(asm);
for a different one. The
resulting context gives you the opportunity to load assemblies (from file, by name, ...).
AssemblyLoadContext.Default.Resolving += CustomResolving;
var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(path);
The context also provides a Resolving
-event which could be used to bring in your resolving code, if an assembly could not be resolved byte the LoadContext itself.
private static Assembly CustomResolving(AssemblyLoadContext arg1, AssemblyName arg2)
{
Console.WriteLine($"Try resolve: {arg2.FullName}");
//Maybe Load from different path e.g. Addon Path.
return arg1.LoadFromAssemblyPath(@"C:\Addons\" + arg2.Name + ".dll");
}
Compile files from location
"dependencies": {
...
"Microsoft.CodeAnalysis.CSharp": "2.0.0-beta3",
...
}
Now as we can determine the references and load assemblies, we can start to implement the compile method. This can be done very easily by using
the .Net Compiler Platform (formally known as Roslyn). The following listing shows how this works. Each element of the list could be represent a file of C# source code.
private static Assembly Compile(string assemblyName, IEnumerable<string> codes, IEnumerable<string> usings = null)
{
if (codes == null || !codes.Any())
throw new ArgumentNullException(nameof(codes));
//we need to get a tree per source
var trees = new List<SyntaxTree>();
var additionalUsings = usings != null
? usings.Select(s =>
SyntaxFactory
.UsingDirective(SyntaxFactory
.ParseName(s))).ToArray()
: new UsingDirectiveSyntax[0];
foreach (var code in codes)
{
// Parse the script to a SyntaxTree
var syntaxTree = CSharpSyntaxTree.ParseText(code);
var root = (CompilationUnitSyntax)syntaxTree.GetRoot();
if (additionalUsings.Any())
root = root.AddUsings(additionalUsings);
trees.Add(SyntaxFactory.SyntaxTree(root));
}
// Compile the SyntaxTree to an in memory assembly
var compilation = CSharpCompilation.Create(
assemblyName,
trees,
GetMetaDataReferences(),
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);
using (var outputStream = new MemoryStream())
{
using (var pdbStream = new MemoryStream())
{
var result = compilation.Emit(outputStream);
if (result.Success)
{
outputStream.Position = 0;
return AssemblyLoadContext.Default.LoadFromStream(outputStream);
}
else
{
Console.WriteLine(result.Diagnostics.Select(x => $"{x.ToString()}{Environment.NewLine}"));
return null;
}
}
}
}
private static IEnumerable<MetadataReference> GetMetaDataReferences()
{
return DependencyContext.Default
.CompileLibraries
.SelectMany(x => x.ResolveReferencePaths())
.Where(path => !string.IsNullOrWhiteSpace(path))
.Select(path => MetadataReference.CreateFromFile(path));
}
Summary
In the reference section you can find some additional links I used to learn and understand the functionality. I hope you find this article helpful. If this is the case or if you have some questions or find some bugs, please post in the comment section below. I will thankfully use your input to improve this blog-post.