Introduction
Today, we will look into how we can dynamically load C# code into our solution, compile this code, run it, and then interact with it to call methods, etc. This might be useful when we want to add plugin code etc.
Create the C# code
The first step is to create some C# code which we will be dynamically loading into our solution. For this, create a simple text file and place it on your machine. Add the below code to it.
using System;
public class LibClass
{
public void LibMethod()
{
Console.WriteLine("Hello, from code library!");
}
}
Create the C# solution
Next, create a console application in Visual Studio 2022 using the latest .NET 7. The solution looks like the below:
Now, add the below code to the “Program.cs” file:
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
public static class Program
{
public static void Main()
{
LibraryRunner.RunLibraryCodeFromFile("C:\\Temp\\code.txt");
}
}
public static class LibraryRunner
{
public static void RunLibraryCodeFromFile(string filePath)
{
string code = File.ReadAllText(filePath);
// Set up the compilation options
var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
// Add necessary references
var references = new[]
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
MetadataReference.CreateFromFile(typeof(System.Runtime.AssemblyTargetedPatchBandAttribute).Assembly.Location),
MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location)
};
// Compile the code
var syntaxTree = SyntaxFactory.ParseSyntaxTree(code);
var compilation = CSharpCompilation.Create("LibraryAssembly")
.WithOptions(compilationOptions)
.AddReferences(references)
.AddSyntaxTrees(syntaxTree);
using var ms = new MemoryStream();
var emitResult = compilation.Emit(ms);
if (!emitResult.Success)
{
// Handle compilation errors
foreach (var diagnostic in emitResult.Diagnostics)
{
Console.WriteLine(diagnostic.ToString());
}
}
else
{
ms.Seek(0, SeekOrigin.Begin);
// Load the compiled assembly
var assembly = Assembly.Load(ms.ToArray());
// Execute the library code
var libraryClassType = assembly.GetType("LibClass");
var libraryInstance = Activator.CreateInstance(libraryClassType);
var libraryMethod = libraryClassType.GetMethod("LibMethod");
libraryMethod.Invoke(libraryInstance, null);
}
}
}
The code is quite simple. We load the code from a text file. Then we set the values for the main library that does all the work. This is the “CSharpCompilation” library. We set the value for compilation options. Here, we are generating a dynamic linked library. We then set any references that may be required via the references array. We then set the syntax tree by passing in the code we loaded to be parsed. After that the “CSharpCompilation” class is called with these options and the library is created. In this example we do not save it as a DLL to disk. We keep it in a memory stream and load it from there. After that we select the class and method using reflection and run the method.
The complete code for this example can be found at the below:
https://github.com
Summary
In today’s article we looked into how we can load C# code dynamically, compile it, run it, and finally interact with it. This might be useful when we want to add plug-in type code which is developed by external teams or something we want to change on the fly without having to re-compile the complete main application.