Compiling and Executing Code in a C# App

August 2nd 2019 Roslyn .NET Core .NET Framework

Since the release of Roslyn, the complete C# compiler pipeline is available as a NuGet package and we can include it in our own application. I was wondering how difficult it would be to use it to compile some C# source code into an executable and run it.

Being able to compile the following source code into a console application would be a good start:

using System;

namespace ConsoleApp1
{
    public class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello world!");
            Console.ReadLine();
        }
    }
}

.NET Framework

The basic setup is pretty straightforward. I just needed to find the correct sequence of API calls to invoke the compiler:

var syntaxTree = SyntaxFactory.ParseSyntaxTree(SourceText.From(sourceCode));

var assemblyPath = Path.ChangeExtension(Path.GetTempFileName(), "exe");

var compilation = CSharpCompilation.Create(Path.GetFileName(assemblyPath))
    .WithOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication))
    .AddSyntaxTrees(syntaxTree);

var result = compilation.Emit(assemblyPath);

if (result.Success)
{
    Process.Start(assemblyPath);
}
else
{
    Debug.Write(string.Join(
        Environment.NewLine,
        result.Diagnostics.Select(diagnostic => diagnostic.ToString())
    ));
}

Unfortunately, the build failed with a rather long list of diagnostic errors:

(1,7): error CS0246: The type or namespace name 'System' could not be found (are you missing a using directive or an assembly reference?)
(5,18): error CS0518: Predefined type 'System.Object' is not defined or imported
(7,26): error CS0518: Predefined type 'System.String' is not defined or imported
(7,16): error CS0518: Predefined type 'System.Void' is not defined or imported
(9,13): error CS0518: Predefined type 'System.Object' is not defined or imported
(9,13): error CS0103: The name 'Console' does not exist in the current context
(9,27): error CS0518: Predefined type 'System.String' is not defined or imported
(10,13): error CS0518: Predefined type 'System.Object' is not defined or imported
(10,13): error CS0103: The name 'Console' does not exist in the current context
(5,18): error CS1729: 'object' does not contain a constructor that takes 0 arguments

It turned out that all of them were caused by a missing reference to the mscorlib.dll assembly. Adding the assembly containing one of the above-mentioned types was enough to fix it:

var compilation = CSharpCompilation.Create(Path.GetFileName(assemblyPath))
    .WithOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication))
    .AddReferences(
        MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location)
    )
    .AddSyntaxTrees(syntaxTree);

In .NET framework, that was all I needed to get it working. My code built the executable and ran it.

.NET Core

.NET Core was a different story. For starters, there was another missing assembly reference:

(9,13): error CS0103: The name 'Console' does not exist in the current context
(10,13): error CS0103: The name 'Console' does not exist in the current context

I used the same approach as before to add it:

var compilation = CSharpCompilation.Create(Path.GetFileName(assemblyPath))
    .WithOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication))
    .AddReferences(
        MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location),
        MetadataReference.CreateFromFile(typeof(Console).GetTypeInfo().Assembly.Location)
    )
    .AddSyntaxTrees(syntaxTree);

This simply resulted in a different set of diagnostic errors:

(9,21): error CS0012: The type 'Object' is defined in an assembly that is not referenced. You must add a reference to assembly 'System.Runtime, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'.
(9,13): error CS0012: The type 'Decimal' is defined in an assembly that is not referenced. You must add a reference to assembly 'System.Runtime, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'.
(10,21): error CS0012: The type 'Object' is defined in an assembly that is not referenced. You must add a reference to assembly 'System.Runtime, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'.

I had to take a different approach for adding the System.Runtime assembly. I assumed that it's in the same folder as the other .NET Core assemblies and hardcoded the assembly name:

var dotNetCoreDir = Path.GetDirectoryName(typeof(object).GetTypeInfo().Assembly.Location);

var compilation = CSharpCompilation.Create(Path.GetFileName(assemblyPath))
    .WithOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication))
    .AddReferences(
        MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location),
        MetadataReference.CreateFromFile(typeof(Console).GetTypeInfo().Assembly.Location),
        MetadataReference.CreateFromFile(Path.Combine(dotNetCoreDir, "System.Runtime.dll"))
    )
    .AddSyntaxTrees(syntaxTree);

The code finally compiled, but the generated assembly wasn't a working Windows executable:

Unhandled Exception: System.IO.FileNotFoundException: Could not load file or assembly 'System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e' or one of its dependencies. The system cannot find the file specified.

It should be started using the dotnet CLI command instead:

Process.Start("dotnet", assemblyPath);

That helped, but the console application still didn't start:

A fatal error was encountered. The library 'hostpolicy.dll' required to execute the application was not found in 'C:\Users\Damir\AppData\Local\Temp\'.

Failed to run as a self-contained app. If this should be a framework-dependent app, add the C:\Users\Damir\AppData\Local\Temp\tmp8FD4.runtimeconfig.json file specifying the appropriate framework.

No problem, I can generate the *.runtimeconfig.json file. A sample can be found in the output folder of any .NET Core project:

{
  "runtimeOptions": {
    "tfm": "netcoreapp3.0",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "3.0.0-preview6-27804-01"
    }
  }
}

Generating a JSON file is a great reason to use the new System.Text.JSON APIs in .NET Core. I hardcoded everything except the runtime version. To hopefully keep the code working as new versions of .NET Core are released, I used the improved version API available since .NET Core Preview 4:

File.WriteAllText(
    Path.ChangeExtension(assemblyPath, "runtimeconfig.json"),
    GenerateRuntimeConfig()
);

private string GenerateRuntimeConfig()
{
    using (var stream = new MemoryStream())
    {
        using (var writer = new Utf8JsonWriter(
            stream,
            new JsonWriterOptions() { Indented = true }
        ))
        {
            writer.WriteStartObject();
            writer.WriteStartObject("runtimeOptions");
            writer.WriteStartObject("framework");
            writer.WriteString("name", "Microsoft.NETCore.App");
            writer.WriteString(
                "version",
                RuntimeInformation.FrameworkDescription.Replace(".NET Core ", "")
            );
            writer.WriteEndObject();
            writer.WriteEndObject();
            writer.WriteEndObject();
        }

        return Encoding.UTF8.GetString(stream.ToArray());
    }
}

Finally, the compiled .NET Core console application started. Although the core code is the same in .NET framework and .NET Core, the latter required a lot more plumbing to get it working.

Copyright
Creative Commons License