Generating code at runtime, classes

I’ve always been intrigued by the concept of runtime code generation. It’s incredibly powerful, and JavaScript makes it seem almost effortless with eval. The eval function executes code as if it were written directly within the existing code, granting access to local variables and more. This has traditionally been trickier in languages like C#. While C# has evolved over the years (as discussed in a long while ago), I wanted to revisit runtime code generation in this language, given that some older methods have faded from memory and newer ones have emerged.

Generating code at runtime ideally involves providing a code string, much like JavaScript’s eval, and then executing it. In JavaScript, if the string represents a function or class declaration (in pre-ES6 style), executing it makes it available for subsequent invocation. This means you can execute instructions immediately or define reusable “code blocks.” C#, however, distinguishes between defining classes/methods and executing code. Therefore, I’ll cover creating classes, generating reusable code blocks (delegates to dynamically generated code), and executing one-off statements in separate posts.

Generating Classes Dynamically

There are three primary approaches to runtime class creation in C#, all ultimately relying on the same mechanism: using the compiler to generate an assembly from source code, loading it into memory, and retrieving the desired Type object from this assembly. Instances of this type are then created using Activator.CreateInstance.

  • CodeDOM: This older method has two major drawbacks. First, it relies on launching the csc.exe compiler behind the scenes (as mentioned in in this post, the pre-Roslyn compilers’ inability to function as a library is perplexing). Consequently, the generated assembly cannot reside in memory and is written to a temporary file like “C:\temp\randomName.dll,” negatively impacting performance. This harks back to my initial encounter with C# runtime code generation around 2002, which involved directly invoking the CSC compiler using Process.Start("csc.exe"...) The CodeDOM merely provides an abstraction layer above this process.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    private static Type CreateTypeViaCodeDom(string code, string typeName, IEnumerable<string> assemblyNames)
    {
        CodeDomProvider cpd = new CSharpCodeProvider();
        var cp = new CompilerParameters();
        foreach (string assemblyName in assemblyNames)
        {
            cp.ReferencedAssemblies.Add(assemblyName);
        }
    
        cp.GenerateExecutable = false;
        CompilerResults cr = cpd.CompileAssemblyFromSource(cp, code);
        //the assembly gets written to disk (c:\\Temp\\randomName.dll)
    
        Type tp = cr.CompiledAssembly.GetType(typeName);
        return tp;
    } 
    
  • Roslyn (Microsoft.CodeAnalysis…): Roslyn embodies the Compiler as a Service (CaaS) concept, where the compiler exists within libraries. This eliminates the need to invoke csc.exe externally, and assemblies can be generated directly in memory.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    
    private static Type CreateTypeViaRoslyn(string code, string typeName, IEnumerable<Assembly> references)
    {
        var assemblyName = Path.GetRandomFileName();
        var syntaxTree = CSharpSyntaxTree.ParseText(code);
    
        List<MetadataReference> metadataReferences = references.Select(it => (MetadataReference)(MetadataReference.CreateFromFile(it.Location))).ToList();
        var compilation = CSharpCompilation.Create(assemblyName, new[] { syntaxTree },
            metadataReferences,
            new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
    
        using (MemoryStream stream = new MemoryStream())
        {
            var result = compilation.Emit(stream);
    
            if (!result.Success)
            {
                var failures = result.Diagnostics.Where(diagnostic =>
                    diagnostic.IsWarningAsError ||
                    diagnostic.Severity == DiagnosticSeverity.Error);
    
    
                var message = ""; // failures.Select(x => $"{x.Id}: {x.GetMessage()}").Join("\n");
                throw new InvalidOperationException("Compilation failures!\n\n" + message + "\n\nCode:\n\n" + code);
            }
    
            stream.Seek(0, SeekOrigin.Begin);
            Assembly asm = Assembly.Load(stream.ToArray());
            return asm.GetType(typeName);
        }
    } 
    
  • Mono’s Compiler as a Service: Predating Roslyn, Mono’s CaaS has long enabled features like Mono’s REPL. It functions independently of other Mono-specific libraries and can be used with Microsoft’s Framework by referencing the Mono.CSharp.dll NuGet package. Most examples focus on executing small code snippets rather than defining reusable classes. However, the following unconventional approach, while somewhat peculiar, effectively achieves this purpose:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    private static Type CreateTypeViaMono(string code, string typeName, IEnumerable<Assembly> references)
    {
        var evaluator = new Evaluator(new CompilerContext(
                        new CompilerSettings(),
                        new ConsoleReportPrinter()
                ));
    
        // Make it reference our own assembly so it can use IFoo
        foreach (Assembly assembly in references)
        {
            evaluator.ReferenceAssembly(assembly);
        }
    
        // Feed it some code
        evaluator.Compile(code);
        Assembly asm = ((Type)evaluator.Evaluate("typeof(" + typeName + ");")).Assembly;
        return asm.GetType(typeName);
    } 
    

Methods 2 and 3 share the same signature. Method 1, however, requires the names of referenced assemblies instead of the assembly objects themselves.

Let’s illustrate with an example. Suppose we have an IFormatter interface and want to create a new Formatter class that implements this interface. The code might look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Type upperCaseFormatterType = CreateTypeViaXXX(@"
            namespace Formatters
            {
                public class UpperCaseFormatter : Formatters.IFormatter
                {
                    public string Format(string s) { return s.ToUpper(); }
                }
            }",
            "Formatters.UpperCaseFormatter",
            new List<Assembly>()
            {
                typeof(Formatters.IFormatter).Assembly
            }
            //pass directly "Formatters.dll" for the DOM case
);

IFormatter formatter = (IFormatter)(Activator.CreateInstance(upperCaseFormatterType));
Console.WriteLine(formatter.Format("abc"));

This post draws upon information and code snippets found in this Post, this post, and this question.

Licensed under CC BY-NC-SA 4.0
Last updated on Apr 17, 2023 01:26 +0100