Running .NET applications in-process using AppDomains

When testing a compiler for a managed language a very convenient end-to-end testing technique is to compile a test program, then run it and verify that it gives the expected output. Not only you cover all parts of the compiler in this manner (parser, binder and emitter), but also you verify that your compiler produces correct IL (otherwise the CLR won’t load and verify your assembly) and your final program has the expected behavior (output has to match).

One downside is that if you have 50,000 test programs, you have to pay the process startup cost and the CLR startup cost 50,000 times. AppDomains to the rescue – they were originally designed as lightweight managed processes, so why not use them as such?

To demonstrate this approach, we’re going to write a .NET program that can run any .NET Console application, intersept its output to the Console, and print it out for demo purposes. First of all, let’s create a C# console application that prints out “Hello World” and save it as Program.exe. Then let’s create a sample “verifier” program that will start and run Program.exe without spinning up a separate process and a separate CLR instance:

 using System;
using System.IO;
using System.Reflection;
 
namespace AppDomainTools
{
    public class Launcher : MarshalByRefObject
    {
        public static void Main(string[] args)
        {
            TextWriter originalConsoleOutput = Console.Out;
            StringWriter writer = new StringWriter();
            Console.SetOut(writer);
 
            AppDomain appDomain = AppDomain.CreateDomain("Loading Domain");
            Launcher program = (Launcher)appDomain.CreateInstanceAndUnwrap(
                typeof(Launcher).Assembly.FullName,
                typeof(Launcher).FullName);
 
            program.Execute();
            AppDomain.Unload(appDomain);
 
            Console.SetOut(originalConsoleOutput);
            string result = writer.ToString();
            Console.WriteLine(result);
        }
 
        /// <summary>
        /// This gets executed in the temporary appdomain.
        /// No error handling to simplify demo.
        /// </summary>
        public void Execute()
        {
            // load the bytes and run Main() using reflection
            // working with bytes is useful if the assembly doesn't come from disk
            byte[] bytes = File.ReadAllBytes("Program.exe");
            Assembly assembly = Assembly.Load(bytes);
            MethodInfo main = assembly.EntryPoint;
            main.Invoke(null, new object[] { null });
        }
    }
}

This approach is especially beneficial if you have to run a lot of small programs – you save a lot on process startup costs and CLR startup costs. Note that I’m representing an assembly as a plain byte array – this allows us to avoid disk I/O if the assembly was just compiled and wasn’t even saved to disk, so you can compile and run it immediately, without ever writing the compiler’s output to disk.

Also note that Launcher inherits from MarshalByRefObject – this allows us to pass data back and forth across AppDomain boundaries. If you like, just create an instance field in the Launcher class and it will get automatically serialized/deserialized when it passes through the DoCallback invocation point.