Compiling C# code at runtime

  • Reading time:5 mins read

This article provides informations about compiling C# code at runtime from a text source. We’ll use the Roslyn compiler to dynamically build in-memory assembly.

Prerequisities

  • create Console Application .NET Core 3.1.
  • Install-Package Microsoft.CodeAnalysis.CSharp (for now it’s version 3.7.0)

Implementation of compiling C# code at runtime with Roslyn

string code = "using System; namespace InMemoryApp {class Program{private static void Main(string[] args){ Console.WriteLine(123456); } } }";

var syntaxTree = CSharpSyntaxTree.ParseText(code, new CSharpParseOptions(LanguageVersion.CSharp8));
string basePath = Path.GetDirectoryName(typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly.Location);

var root = syntaxTree.GetRoot() as CompilationUnitSyntax;
var references = root.Usings;

var referencePaths = new List<string> {
    typeof(object).GetTypeInfo().Assembly.Location,
    typeof(Console).GetTypeInfo().Assembly.Location,
    Path.Combine(basePath, "System.Runtime.dll"),
    Path.Combine(basePath, "System.Runtime.Extensions.dll"),
    Path.Combine(basePath, "mscorlib.dll")
};

referencePaths.AddRange(references.Select(x => Path.Combine(basePath, $"{x.Name}.dll")));

var executableReferences = new List<PortableExecutableReference>();

foreach (var reference in referencePaths)
{
    if (File.Exists(reference))
    {
        executableReferences.Add(MetadataReference.CreateFromFile(reference));
    }
}

var compilation = CSharpCompilation.Create(Path.GetRandomFileName(), new[] { syntaxTree }, executableReferences, new CSharpCompilationOptions(OutputKind.ConsoleApplication));

using (var memoryStream = new MemoryStream())
{
    EmitResult compilationResult = compilation.Emit(memoryStream);

    if (!compilationResult.Success)
    {
        var errors = compilationResult.Diagnostics.Where(diagnostic =>
        diagnostic.IsWarningAsError ||
        diagnostic.Severity == DiagnosticSeverity.Error)?.ToList() ?? new List<Diagnostic>();
    }
    else
    {
        memoryStream.Seek(0, SeekOrigin.Begin);

        AssemblyLoadContext assemblyContext = new AssemblyLoadContext(Path.GetRandomFileName(), true);
        Assembly assembly = assemblyContext.LoadFromStream(memoryStream);

        var entryPoint = compilation.GetEntryPoint(CancellationToken.None);
        var type = assembly.GetType($"{entryPoint.ContainingNamespace.MetadataName}.{entryPoint.ContainingType.MetadataName}");
        var instance = assembly.CreateInstance(type.FullName);
        var method = type.GetMethod(entryPoint.MetadataName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
        method.Invoke(instance, BindingFlags.InvokeMethod, Type.DefaultBinder, new object[] { new string[] { "abc" } }, null);

        assemblyContext.Unload();
    }
}

Let’s go through the code slowly. To create the in-memory assembly we need 3 things – assembly name, code to compile and the references. We’ll start with source code.

Transform string to SyntaxTree

What is SyntaxTree?

It’s immutable data structure that represents the program as a tree of names, commands and marks. It allows to manipulate on this data and can be used for code analysis. In Visual Studio you can check Syntax Visualizer that will display the syntax tree in more user-friendly way.

Language version

In the web you may find resources using the old compiler. The highest running version of .NET is 4.0. The old API is using CSharpCodeProvider class. In this example we’ll use C# version 8. Please note that you can use different language version than the application. The assembly compiled at runtime can use totally different compiling settings, assemblies, references. Still all this should be accessible at runtime, so make sure you included all the references in the app.

Get references

To get all the necessary assemblies we need to find the place where they are stored. That’s why we’re looking for Garbage Collector location. Based on this path we’ll get other assemblies. The basic references we need are:

  • object – so we can manipulate on in-built types;
  • Console – new assembly will be console application, so we can use it’s API;
  • System.Runtime.Extensions – contains mathematical functions, conversions, string comparisons…

The dependencies on other assemblies are as follow. System.Runtime depends on mscorlib. It forwards System.Type to mscorlib. mscorlib provides System.Type for System.Private.CoreLib. System.Private.CoreLib have all the implementation of base types and it’s returned with typeof(object).GetTypeInfo().Assembly.Location.

We also need to know what references have been used in the code. We can get them from SyntaxTree by checking Root.

Can I use dynamic variables?

Yes. dynamic type requires additional libraries. Add the following to the referencePaths list

var referencePaths = new List<string> {
    typeof(object).GetTypeInfo().Assembly.Location,
    typeof(Console).GetTypeInfo().Assembly.Location,
    Path.Combine(basePath, "System.Runtime.dll"),
    Path.Combine(basePath, "System.Runtime.Extensions.dll"),
    Path.Combine(basePath, "mscorlib.dll"),
    Path.Combine(basePath, "Microsoft.CSharp.dll"),
    Path.Combine(basePath, "System.Linq.Expressions.dll"),
    Path.Combine(basePath, "netstandard.dll")
};

Compile C# code at runtime

We have all the ingredients so we can create the instance of the compilation. Our goal is to create the console application in-memory so we need the MemoryStream. So the CSharpCompilation instance will be emitted to memory. The EmitResult contains the informations about compilation errors. If the build is successful we can execute the code. To achieve this we’ll get new assembly context. Why it matters? In short, we’ll avoid complications if the modules will depend on different assemblies versions. It’s decoupling the scope of the assemblies dependencies. Next steps are getting entry point, creating the instance, get the method to execute and invoke. One interesting thing might be Type.DefaultBinder.

Type.DefaultBinder explanation

We need something that will tell the compiler how to convert the types. As we don’t need any special rules for this we’re just using default implementation. Let’s check the following sample:

int i = 1;

With default binder it will be converted to Int32. Without the specified binder instead of writing int you need to use fully qualified type Int32 in this case.

 

Source code: Compiling with Roslyn Demo

Want to read more? Leave a comment!

Subscribe
Notify of

0 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments