How To Run Compiled C# Code At Runtime

by Kenji Elzerman
Learn C# Essential Concepts and Frameworks for New Developers - Kens Learning Curve

In a previous article, I showed you how to compile C# code stored in a string. I showed you how to validate C# code written in a simple string. Let’s use that code and run compiled C# code using… C# code.

Goals

Please note that this article is not about compiling C# code at runtime. For that, I would like to redirect you to another article I wrote a while ago: Compile C# Code At Runtime Made Easy. This article is about executing compiled code from a memory stream.

In this article you

  • Will get a brief overview of the previous article
  • Create an assembly from a CSharpCompilation
  • Invoke the method in the compiled code class and show the result
  • Create a method in a string that returns an int and accepts a parameter, which will be executed in the compiled assembly.

Previously On Kens Learning Curve…

In the previous article, I have created a class with the following code:

public class ValidationService
{
    public IEnumerable<Diagnostic> Execute(string code)
    {
        string assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location);
        string assemblyName = Path.GetRandomFileName();

        CSharpCompilationOptions defaultCompilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
            .WithOverflowChecks(true)
            .WithOptimizationLevel(OptimizationLevel.Release);

        IEnumerable<MetadataReference> defaultReferences = new[]
        {
            MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "mscorlib.dll")),
            MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.dll")),
            MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Core.dll")),
            MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Runtime.dll")),
            MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Linq.dll")),
            MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location),
        };

        CSharpCompilation compilation = CSharpCompilation.Create(
            assemblyName,
            syntaxTrees: new[] {
                CSharpSyntaxTree.ParseText(code)
            },
            references: defaultReferences,
            options: defaultCompilationOptions);

        using var ms = new MemoryStream();
        EmitResult result = compilation.Emit(ms);

        if (result.Success)
            return null;

        IEnumerable<Diagnostic> failures = result.Diagnostics.Where(diagnostic =>
            diagnostic.IsWarningAsError ||
            diagnostic.Severity == DiagnosticSeverity.Error);

        return failures;
    }
}

In short, it has one method (Execute) that receives a string with code. The method will create a C# compilation with references, the code, and some basic options. Then it will compile and emit the result in a memory stream.

If all goes well it will return NULL. If something is wrong, could be about anything, it will return the errors in a list of Diagnostic objects.

I will be using and expanding this class and method in this article.

From Compiled Code To An Assembly

To run compiled C# code, we first need to create an assembly, which is essentially a grouping of types and resources within a single logical unit. This logical unit can either be an executable file or a DLL. However, it is also possible to store the assembly in the computer’s memory and use it from there.

Although it sounds really tough to create such an assembly, it’s actually really simple; it’s already there when we compile it. All we have to do is read from the memory and let the Assembly class load it.

In the current code, when the result.Success is true, it returns NULL. Here we are going to add the loading of the assembly.

if (result.Success)
{
    ms.Seek(0, SeekOrigin.Begin);
    return Assembly.Load(ms.ToArray());
}

The ms.Seek method sets the position of the current memory stream to the beginning of a specific memory location. We need this to convert the memory stream to an array, which can then be passed to Assembly.Load.

If you have decided to save the compiled code to an actual file, you will need the Assembly.LoadFrom, which allows you to load an assembly file (DLL or EXE) from the disk of the computer.

The method returns the wrong type. So I changed it to Assembly. This will give an error on the return failures. I changed it to this:

List<string> sb = new();

IEnumerable<Diagnostic> failures = result.Diagnostics.Where(diagnostic =>
        diagnostic.IsWarningAsError ||
        diagnostic.Severity == DiagnosticSeverity.Error);

foreach (Diagnostic diagnostic in failures)
    sb.Add(diagnostic.GetMessage());

Exception ex = new(sb.ToString());

throw ex;

Complete Code

For those who are completely lost (or just want the code), here is the complete code of ValidationService:

public class ValidationService
{
    public Assembly Execute(string code)
    {
        string assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location);
        string assemblyName = Path.GetRandomFileName();

        CSharpCompilationOptions defaultCompilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
            .WithOverflowChecks(true)
            .WithOptimizationLevel(OptimizationLevel.Release);

        IEnumerable<MetadataReference> defaultReferences = new[]
        {
            MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "mscorlib.dll")),
            MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.dll")),
            MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Core.dll")),
            MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Runtime.dll")),
            MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Linq.dll")),
            MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location),
        };

        CSharpCompilation compilation = CSharpCompilation.Create(
            assemblyName,
            syntaxTrees: new[] {
                CSharpSyntaxTree.ParseText(code)
            },
            references: defaultReferences,
            options: defaultCompilationOptions);

        using var ms = new MemoryStream();
        EmitResult result = compilation.Emit(ms);

        if (result.Success)
        {
            ms.Seek(0, SeekOrigin.Begin);
            return Assembly.Load(ms.ToArray());
        }

        List<string> sb = new();

        IEnumerable<Diagnostic> failures = result.Diagnostics.Where(diagnostic =>
                diagnostic.IsWarningAsError ||
                diagnostic.Severity == DiagnosticSeverity.Error);

        foreach (Diagnostic diagnostic in failures)
            sb.Add(diagnostic.GetMessage());

        Exception ex = new(sb.ToString());

        throw ex;
    }
}

Using The Assembly

Next up: Using the assembly.

I am using a new, clean console application. I will be adding the C# code in the string soon. For now, my Program.cs looks like this:

string exampleCode = @"";

try
{
    ValidationService validationService = new();
    Assembly assembly = validationService.Execute(exampleCode);
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

Pretty empty, but the most important part is that we have the assembly variable, which is of the type Assembly. It would be very convenient if we could execute the assembly. I mean, everything is there, right? Sorry, no.

We need to find the class and the method and then invoke the method on the class. That will execute the C# code in the assembly.

Setting The C# Code In A String

Before I continue, I want to place some C# code in the string variable exampleCode. I go with something simple like this:

string exampleCode = @"
using System;

public class HelloWorld {
    public void Greet(){
      Console.WriteLine(""Hi there!"");
    }
}";

When this is executed the words Hi there! will be printed in the console. Later I will be using a method that returns something, so stay tuned for that.

Getting The Type

Although I said we need to find the class, we actually need to find the Type. Finding and grabbing the type allows us the invoke the method.

The code to get the correct type is pretty simple:

Type type = assembly?.GetType("HelloWorld")
            ?? throw new Exception("Can't get the correct class from the assembly");

Note that the parameter of GetType is the name of the class. In the example, this is the class HelloWorld.

Now that we have the type, we can activate the method.

Invoking The Method

To invoke the method (also known when using delegates), we need to get the method first. When it’s found we can use the method Invoke on it. This method has two parameters. The first one is an activator, which will be used to create an instance of the type. A class needs to be created and initialized, right?

The second parameter is a list of parameters, which we don’t have so it will be NULL. The parameter is of the type object?[] so it can be anything.

type.GetMethod("Greet").Invoke(Activator.CreateInstance(type), null);

And that’s it!

Run The Application

If you run it now the application will compile the application and execute HelloWorld.Greet() and it looks like this:

Compiled and Execute - Run Compiled C# Code - Kens Learning Curve

Just as expected!

Using Return Data

Our Greet method just writes something on the screen. But what if we want to receive data from the method? Let’s create a method, in the exampleCode variable, that will calculate the age based on a date of birth. The variable contents look like this:

string exampleCode = @"
using System;

public class HelloWorld{
    public void Greet(){
      Console.WriteLine(""Hi there!"");
    }

    public int CalculateAge(DateTime dob) {
        DateTime today = DateTime.Today;
        var age = today.Year - dob.Year;

        if (dob.Date > today.AddYears(-age)) 
            age--;

        return age;
    }
}";

The class is still the same and the Greet method is still there. The CalculateAge, which calculates the age based on the given date and time, is new. This method works, I tried it.

To execute this method, we need to change the code a bit.

Console.WriteLine("What is your date of birth?");
string dateOfBirth = Console.ReadLine();

try
{
    ValidationService validationService = new();
    Assembly assembly = validationService.Execute(exampleCode);

    Type type = assembly?.GetType("HelloWorld")
                ?? throw new Exception("Can't get the correct class from the assembly");

    bool success = int.TryParse(type.GetMethod("CalculateAge").Invoke(Activator.CreateInstance(type), new object[] { DateTime.Parse(dateOfBirth) }).ToString(), out int age);

    if (success)
        Console.WriteLine($"You are {age} years old.");

}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

I know the method will return an int, so I invoke the CalculateAge method and parse it to a string. Invoke returns an object and the int.TryParse needs a string. In the invoke, the second parameter, I initialize a new array of object and add the given date of birth, which I parse to a DateTime type.

When you run the application and enter a date, the result should look like this:

Compiled and Execute With Parameters - Run Compiled C# Code - Kens Learning Curve

(yes, I am that old…)

Conclusion On Run Compiled C# Code At Runtime

And this is how you can execute (or invoke) compiled code at runtime.

Of course, this is all happy flow and you will run into problems if you want to do some heavy compiling and invoking. For example: I know the CalculateAge returns an int(eger). But what if you can’t look at the uncompiled code? Because you can also load a DLL on your drive and use that.

The answer is reflection, you can look at the compiled code and discover which classes, methods, and properties. You can go deeper and discover if a method has a return type or not.

But this article is to give you some basic knowledge about how to run compiled C# code at runtime. Giving you some tips and hints on where to begin and continue on your own journey. But maybe I will write a reflection article, helping a bit more.

Table Of Contents
Kens Learning Curve