How to add dynamic compilation to your C# projects?

Dynamic compilation is an awesome feature to add to your projects. Especially frameworks will benefit from the compilation of dynamic expressions and scripts. There are two main ways of doing it: the Code DOM Compiler and the Roslyn project. I'll show how to implement them both.

Dynamic code compilation

We will compile a string into a DLL, so we need that string to contain a class. To interact with the compiled object we must define an interface (I guess it could be done with reflection, but that makes things unnecessary complicated).

There are basically 2 kinds of expressions; those that generate a result and those that don't (scripts). By creating two interfaces for these types of behaviors we allow the end user to implement them.

/// <summary>
/// Indicates the object implements an object producer.
/// </summary>
public interface IProducer
{
    /// <summary>
    /// Runs the producer.
    /// </summary>
    /// <returns>The result.</returns>
    object Run();
}

/// <summary>
/// Indicates the object implements a runnable script.
/// </summary>
public interface IScript
{
    /// <summary>
    /// Runs the script.
    /// </summary>
    void Run();
}

You can define your own interface. These interfaces will be used by the extension methods that I'll build on top to the compiler interface later.

Compiler abstraction

Let's define an abstraction of a compiler: the ICompiler interface. The idea is that an ICompiler builds the code into an new assembly. In order to build code you must give it the paths of the DLL's you are referencing in your code.

using System.Reflection;

public interface ICompiler
{
    /// <summary>
    /// Compiles the specified code the sepcified assembly locations.
    /// </summary>
    /// <param name="code">The code.</param>
    /// <param name="assemblyLocations">The assembly locations.</param>
    /// <returns>The assembly.</returns>
    Assembly Compile(string code, params string[] assemblyLocations);
}

Code DOM Compiler implementation

The main way of doing compilation is the .Net Code DOM Compiler. It can be used by adding a reference to the Microsoft.CSharp DLL. Let's look at the implementation:

using KeesTalksTech.Utilities.Compilation;
using Microsoft.CSharp;
using System;
using System.CodeDom.Compiler;
using System.Reflection;

/// <summary>
/// Compiler that uses the <see cref="CSharpCodeProvider"/> for compilation.
/// </summary>
/// <seealso cref="KeesTalksTech.Utilities.Compilation.ICompiler" />
/// <seealso cref="System.IDisposable" />
public class CodeDomCompiler : ICompiler, IDisposable
{
    private readonly CSharpCodeProvider compiler = new CSharpCodeProvider();
    /// <summary>
    /// Compiles the specified code the sepcified assembly locations.
    /// </summary>
    /// <param name="code">The code.</param>
    /// <param name="assemblyLocations">The assembly locations.</param>
    /// <returns>
    /// The assembly.
    /// </returns>
    /// <exception cref="KeesTalkstech.Utilities.Compilation.CodeDom.CodeDomCompilerException">Assembly could not be created.</exception>
    public Assembly Compile(string code, params string[] assemblyLocations)
    {
var parameters = new CompilerParameters();
        parameters.GenerateExecutable = false;
        parameters.GenerateInMemory = true;

        foreach (string assemblyLocation in assemblyLocations)
        {
            parameters.ReferencedAssemblies.Add(assemblyLocation);
        }

        var result = compiler.CompileAssemblyFromSource(parameters, code);

        if (result.Errors.Count > 0)
        {
            throw new CodeDomCompilerException("Assembly could not be created.", result);
        }

        try
        {
            return result.CompiledAssembly;
        }
        catch(Exception ex)
        {
            throw new CodeDomCompilerException("Assembly could not be created.", result, ex);
        }
    }

    /// <summary>
    /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
    /// </summary>
    public void Dispose()
    {
        compiler.Dispose();
    }
}

/// <summary>
/// Object that stores the compilation exception for the Roslyn compiler.
/// </summary>
/// <seealso cref="System.Exception" />
public class CodeDomCompilerException : Exception
{
    /// <summary>
    /// Gets the result.
    /// </summary>
    /// <value>
    /// The result.
    /// </value>
    public CompilerResults Result { get; private set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="RoslynCompilationException" /> class.
    /// </summary>
    /// <param name="message">The message.</param>
    /// <param name="result">The result.</param>
    /// <param name="innerException">The inner exception.</param>
    public CodeDomCompilerException(string message, CompilerResults result, Exception innerException = null) : base(message, innerException)
    {
        this.Result = result;
    }
}
		

It is pretty straight forward. You might want to change the parameters.GenerateInMemory = true; for bigger projects and use a file based caching mechanism. I'll show how to implement caching later.

Roslyn Compiler implementation

The .Net team has been very busy building a new managed compiler for C#:

The .NET Compiler Platform ("Roslyn") provides open-source C# and Visual Basic compilers with rich code analysis APIs. You can build code analysis tools with the same APIs that Microsoft is using to implement Visual Studio!

First install the Roslyn NuGet package:

  • Install-Package Microsoft.CodeAnalysis -Version 2.0.0-beta1
  • dotnet add package Microsoft.CodeAnalysis --version 2.0.0-beta1
  • <PackageReference Include="Microsoft.CodeAnalysis" Version="2.0.0-beta1" />

Let's inspect the ICompiler implementation:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System;
using System.IO;
using System.Linq;
using System.Reflection;

/// <summary>
/// Compiler that uses the Roslyn compiler (<see cref="CSharpCompilation" />).
/// </summary>
/// <seealso cref="KeesTalksTech.Utilities.Compilation.ICompiler" />
public class RoslynCompiler : ICompiler
{
    /// <summary>
    /// Gets the compilation options.
    /// </summary>
    /// <value>
    /// The options.
    /// </value>
    public CSharpCompilationOptions Options { get; } = new CSharpCompilationOptions(
        OutputKind.DynamicallyLinkedLibrary,
        reportSuppressedDiagnostics: true,
        optimizationLevel: OptimizationLevel.Release,
        generalDiagnosticOption: ReportDiagnostic.Error
    );

    /// <summary>
    /// Compiles the specified code the sepcified assembly locations.
    /// </summary>
    /// <param name="code">The code.</param>
    /// <param name="assemblyLocations">The assembly locations.</param>
    /// <returns>
    /// The assembly.
    /// </returns>
    /// <exception cref="KeesTalksTech.Utilities.Compilation.Roslyn.RoslynCompilationException">Assembly could not be created.</exception>
    public Assembly Compile(string code, params string[] assemblyLocations)
    {
        var references = assemblyLocations.Select(l => MetadataReference.CreateFromFile(l));

        var compilation = CSharpCompilation.Create(
            "_" + Guid.NewGuid().ToString("D"),
            references: references,
            syntaxTrees: new SyntaxTree[] { CSharpSyntaxTree.ParseText(code) },
            options: this.Options
        );

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

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

            throw new RoslynCompilationException("Assembly could not be created.", compilationResult);
        }
    }
}

/// <summary>
/// Object that stores the compilation exception for the Roslyn compiler.
/// </summary>
/// <seealso cref="System.Exception" />
public class RoslynCompilationException : Exception
{
    /// <summary>
    /// Gets the result.
    /// </summary>
    /// <value>
    /// The result.
    /// </value>
    public EmitResult Result { get; private set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="RoslynCompilationException"/> class.
    /// </summary>
    /// <param name="message">The message.</param>
    /// <param name="result">The result.</param>
    public RoslynCompilationException(string message, EmitResult result) : base(message)
    {
        this.Result = result;
    }
}

Remember: the Roslyn project is still in beta! You might want to use an IoC mechanism in your projects to resolve the compiler implementation.

Cached result compiler

Now we've got two compilers, let's create a memory cached version using a ConcurrentDictionary.

using System;
using System.Collections.Concurrent;
using System.Reflection;

/// <summary>
/// Performs compilation and caches the result in memory.
/// </summary>
/// <seealso cref="KeesTalksTech.Utilities.Compilation.ICompiler" />
public class CachedCompiler : ICompiler
{
    private readonly ConcurrentDictionary<string, Assembly> cache = new ConcurrentDictionary<string, Assembly>();
    private readonly ICompiler compiler;

    /// <summary>
    /// Initializes a new instance of the <see cref="CachedCompiler"/> class.
    /// </summary>
    /// <param name="compiler">The compiler.</param>
    public CachedCompiler(ICompiler compiler)
    {
        if (compiler == null)
        {
            throw new ArgumentNullException(nameof(compiler));
        }

        this.compiler = compiler;
    }

    /// <summary>
    /// Compiles the specified code the sepcified assembly locations.
    /// </summary>
    /// <param name="code">The code.</param>
    /// <param name="assemblyLocations">The assembly locations.</param>
    /// <returns>
    /// The assembly.
    /// </returns>
    public Assembly Compile(string code, params string[] assemblyLocations)
    {
        string key = GetCacheKey(code, assemblyLocations);

        return cache.GetOrAdd(key, (k) =>
        {
            return compiler.Compile(code, assemblyLocations);
        });
    }

    /// <summary>
    /// Gets the cache key.
    /// </summary>
    /// <param name="code">The code.</param>
    /// <param name="assemblyLocations">The assembly locations.</param>
    /// <returns>
    /// The key.
    /// </returns>
    private string GetCacheKey(string code, string[] assemblyLocations)
    {
        string key = String.Join("|", code, assemblyLocations);
        return key;
    }
}

You might want to research a better way of locking by adding named locks.

Extension methods

Let's use extension methods to help build and retrieve results. First we'll make a settings object with all the instructions. It's a nice little container that will bundle the assembly locations, the code and the name of the class together. Especially the name is important, because it will be used to run the class.

using System.Collections.Generic;

public interface ICompilerInstructions
{
    /// <summary>
    /// Gets the assembly locations.
    /// </summary>
    /// <value>
    /// The assembly locations.
    /// </value>
    string[] AssemblyLocations { get; }

    /// <summary>
    /// Gets the code.
    /// </summary>
    /// <value>
    /// The code.
    /// </value>
    string Code { get; }

    /// <summary>
    /// Gets the name of the class. It is used to get class out of the compiled assembly.
    /// </summary>
    /// <value>
    /// The name of the class.
    /// </value>
    string ClassName { get; }
}

/// <summary>
/// Instructions that are used to compiler a piece of code. Used by the extension methods.
/// </summary>
/// <seealso cref="KeesTalksTech.Utilities.Compilation.ICompilerInstructions" />
public class CompilerInstructions : ICompilerInstructions
{
    /// <summary>
    /// Gets the assembly locations.
    /// </summary>
    /// <value>
    /// The assembly locations.
    /// </value>
    public List<string> AssemblyLocations { get; } = new List<string>();

    /// <summary>
    /// Gets the name of the class. It is used to get class out of the compiled assembly.
    /// </summary>
    /// <value>
    /// The name of the class.
    /// </value>
    public string ClassName { get; set; }

    /// <summary>
    /// Gets the code.
    /// </summary>
    /// <value>
    /// The code.
    /// </value>
    public string Code { get; set; }

    /// <summary>
    /// Gets the assembly locations.
    /// </summary>
    /// <value>
    /// The assembly locations.
    /// </value>
    string[] ICompilerInstructions.AssemblyLocations
    {
        get { return AssemblyLocations.ToArray(); }
    }
}

So let's inspect the extension methods:

  • CompileAndCreateObject - compiles the code and returns the class that was specified as the class name.
  • CompileAndCreateObject<T> - same method, but it will do the casting for you
  • RunScript - will assume your code class implements an IScrip. It will compile and run the script.
  • RunProducer - will assume your code class implements an IProducer. It will compile and run the code and return the result.
using System;

/// <summary>
/// Extends the <see cref="ICompiler" /> namespace by adding some extra compilation methods.
/// Will make life easier.
/// </summary>
public static class CompilerExtensions
{
    /// <summary>
    /// Compiles the and create object.
    /// </summary>
    /// <param name="compiler">The compiler.</param>
    /// <param name="instructions">The instructions.</param>
    /// <param name="constructorParameters">The constructor parameters.</param>
    /// <returns></returns>
    public static object CompileAndCreateObject(this ICompiler compiler, ICompilerInstructions instructions, params object[] constructorParameters)
    {
        var assembly = compiler.Compile(instructions.Code, instructions.AssemblyLocations);

        var type = assembly.GetType(instructions.ClassName);

        return Activator.CreateInstance(type, constructorParameters);
    }

    /// <summary>
    /// Compiles the and create object.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="compiler">The compiler.</param>
    /// <param name="instructions">The instructions.</param>
    /// <param name="constructorParameters">The constructor parameters.</param>
    /// <returns></returns>
    public static T CompileAndCreateObject<T>(this ICompiler compiler, ICompilerInstructions instructions, params object[] constructorParameters)
    {
        return (T)compiler.CompileAndCreateObject(instructions, constructorParameters);
    }

    /// <summary>
    /// Runs the producer.
    /// </summary>
    /// <param name="compiler">The compiler.</param>
    /// <param name="instructions">The instructions.</param>
    /// <param name="constructorParameters">The constructor parameters. Leave empty when the constructor has no parameters.</param>
    /// <returns></returns>
    public static object RunProducer(this ICompiler compiler, ICompilerInstructions instructions, params object[] constructorParameters)
    {
        var scriptObject = CompileAndCreateObject<IProducer>(compiler, instructions, constructorParameters);
        return scriptObject.Run();
    }

    /// <summary>
    /// Runs the script.
    /// </summary>
    /// <param name="compiler">The compiler.</param>
    /// <param name="instructions">The instructions.</param>
    /// <param name="constructorParameters">The constructor parameters. Leave empty when the constructor has no parameters.</param>
    public static void RunScript(this ICompiler compiler, ICompilerInstructions instructions, params object[] constructorParameters)
    {
        var scriptObject = CompileAndCreateObject<IScript>(compiler, instructions, constructorParameters);
        scriptObject.Run();
    }
}

Hello world!

Now let's build a Hello World application using all the compilers we've created:

var compiler1 = new CodeDomCompiler();
var compiler2 = new RoslynCompiler();
var compiler3 = new CachedCompiler(compiler1);
var compiler4 = new CachedCompiler(compiler2);

var instruction = new CompilerInstructions();
instruction.AssemblyLocations.Add(typeof(IScript).Assembly.Location);
//needed for Roslyn:
instruction.AssemblyLocations.Add(typeof(object).Assembly.Location);

//code does not to reside in a namespace
instruction.ClassName = Guid.NewGuid().ToString("N");
instruction.Code = @"using System;
public class " + instruction.ClassName + ": " + typeof(IScript).FullName + @"
{
    public void Run() 
    {
        Console.WriteLine(""Hello world!"");
    }
}";

Console.WriteLine("Result from: " + compiler1.GetType().Name);
compiler1.RunScript(instruction);
Console.WriteLine("Result from: " + compiler2.GetType().Name);
compiler2.RunScript(instruction);
Console.WriteLine("Result from: " + compiler3.GetType().Name);
compiler3.RunScript(instruction);
Console.WriteLine("Result from: " + compiler4.GetType().Name);
compiler4.RunScript(instruction);

compiler1.Dispose();

Console.WriteLine();
Console.Write("Ready... ");
Console.ReadKey();

So that's it. Let me know if you will use it in any of your projects. Have fun!

expand_less