An evaluator for simple script evaluation

In a previous blog I explored how to create a dynamic compiler. In this blog I'll explore how to create an evaluator that aids in the compilation of classes. It will take care of the ceremony needed to wrap the code into a class, compile it and retrieve the result.

The Evaluator class

The basic idea is to feed the evaluator the code and to execute it by calling Run. I've added some comments to the class to explain how it works.

using KeesTalksTech.Utilities.Compilation;
using System;
using System.Collections.Generic;

/// <summary>
/// The evaluator aids in the compilation of classes. It will take care 
/// of the ceremony needed to wrap the code into a class, compile it and retrieve the result.
/// </summary>
public class Evaluator : IEvaluator
{
	private readonly ICompiler _compiler;
	private readonly Type _producerType = typeof(IProducer);

	/// <summary>
	/// Initializes a new instance of the <see cref="Evaluator"/> class.
	/// </summary>
	/// <param name="compiler">The compiler.</param>
	public Evaluator(ICompiler compiler) : this(compiler, null)
	{
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="Evaluator"/> class. This constructor can be 
	/// used by classes that inherit the compiler to change the base type of the compiled 
	/// producer class.
	/// </summary>
	/// <param name="compiler">The compiler.</param>
	/// <param name="producerType">Type of the producer.</param>
	protected Evaluator(ICompiler compiler, Type producerType = null)
	{
		if (compiler == null)
		{
			throw new ArgumentNullException(nameof(compiler));
		}

		_compiler = compiler;

		if (producerType != null && this._producerType != producerType)
		{
			if (!typeof(IProducer).IsAssignableFrom(producerType))
			{
				throw new NotSupportedException("The baseType parameter needs to be an implementation of IProducer.");
			}

			this._producerType = producerType;
			AssemblyLocations.Add(producerType.Assembly.Location);
		}

		Usings.Add("System");
		AssemblyLocations.Add(typeof(object).Assembly.Location);
		AssemblyLocations.Add(typeof(IProducer).Assembly.Location);
	}

	/// <summary>
	/// Some definitions might live in a different assembly that
	/// needs to be referenced in order to compile the DLL.
	/// </summary>
	public List<string> AssemblyLocations { get; } = new List<string>();

	/// <summary>
	/// Adds usings to the code - this makes it easier to create 
	/// scripts because objects can be used by their name instead
	/// of their full name.
	/// </summary>
	/// <value>
	/// The usings.
	/// </value>
	public List<string> Usings { get; } = new List<string>();

	/// <summary>
	/// Generates the class en compiles it into a producer.
	/// </summary>
	/// <param name="code">The code.</param>
	/// <returns>The producer.</returns>
	public IProducer CreateProducer(string code)
	{
		var args = new CompilerInstructions();
		args.ClassName = "_" + Guid.NewGuid().ToString("N");
		args.Code = @"<<USINGS>>

public class <<CLASS_NAME>>: <<BASE_TYPE>>
{
	public <<MODIFIER>> object Run()
	{
		<<CODE>>;
	
		return null;
	}
}";
		args.Code = args.Code.Replace("<<CLASS_NAME>>", args.ClassName);
		args.Code = args.Code.Replace("<<BASE_TYPE>>", FixFullName(_producerType));
		args.Code = args.Code.Replace("<<CODE>>", code);
		args.AssemblyLocations.AddRange(this.AssemblyLocations);

		if (Usings.Count > 0)
		{
			string usings = "using " + String.Join(";\nusing ", Usings) + ";";
			args.Code = args.Code.Replace("<<USINGS>>", usings);
		}
		else
		{
			args.Code = args.Code.Replace("<<USINGS>>", "");
		}

		if (_producerType.IsClass)
		{
			args.Code = args.Code.Replace("<<MODIFIER>>", "override");
		}
		else
		{
			args.Code = args.Code.Replace("<<MODIFIER>>", "");
		}

		return _compiler.CompileAndCreateObject<IProducer>(args);
	}

	/// <summary>
	/// Runs the specified code.
	/// </summary>
	/// <param name="code">The code.</param>
	/// <returns>The result.</returns>
	public object Run(string code)
	{
		var producer = CreateProducer(code);
		return producer.Run();
	}

	/// <summary>
	/// Fixes the full name.
	/// </summary>
	/// <param name="type">The type.</param>
	/// <returns>The full name.</returns>
	private string FixFullName(Type type)
	{
		return type.FullName.Replace("+", ".");
	}
}

A script doesn't have to produce anything, that's why the generated class returns null by default. If the specified script does not have to have a return. The script also adds an extra ';' to the script.

Hello world script

So let's create a small script that prints Hello World! 10 times:

var compiler = new CodeDomCompiler();
var evaluator = new Evaluator(compiler);

for (var i = 1; i <= 10; i++)
{
	var script = "Console.WriteLine(\"" + i.ToString("00") + ": Hello world!\");";
	evaluator.Run(script);
}

The following example shows how to return a string and print it:

var compiler = new CodeDomCompiler();
var evaluator = new Evaluator(compiler);

for (var i = 1; i <= 10; i++)
{
	var script = "return \"" + i.ToString("00") + ": Hello world!\";";
	var result = evaluator.Run<string>(script);
	Console.WriteLine(result);
}

Evaluation with a special class

What if you want to interact with the object that was compiled? That's possible. First create a generic evaluator:

using KeesTalksTech.Utilities.Compilation;

/// <summary>
/// Evaluator that will generate sripts with a certain base type.
/// </summary>
/// <typeparam name="BaseType">The base type of the classes that will be generated by the evaluator.</typeparam>
/// <seealso cref="KeesTalksTech.Utiltities.Evaluation.Evaluator" />
public class Evaluator<BaseType> : Evaluator where BaseType : class, IProducer
{
	/// <summary>
	/// Initializes a new instance of the <see cref="Evaluator{BaseType}"/> class.
	/// </summary>
	/// <param name="compiler">The compiler.</param>
	public Evaluator(ICompiler compiler) : base(compiler, typeof(BaseType))
	{
	}

	/// <summary>
	/// Creates the producer.
	/// </summary>
	/// <param name="code">The code.</param>
	/// <returns>The producer.</returns>
	public new BaseType CreateProducer(string code)
	{
		return (BaseType)base.CreateProducer(code);
	}
}

Next create an abstract class that defines the interaction. This class needs to implement IProducer:

public abstract class MyProducer : IProducer
{
	public int Counter { get; set; }

	public string Message { get; set; }

	public abstract object Run();
}

Let's see it in action:

var compiler = new CodeDomCompiler();
var evaluator = new Evaluator<MyProducer>(compiler);

var script = @"
	var s = Counter.ToString(""0000"");
	s += "" "" + Message;
	Console.WriteLine(s);
";

var obj = evaluator.CreateProducer(script);
obj.Message = "Hello World!";
obj.Counter = 1;

for (var i = 0; i <= 10; i++)
{
	obj.Run();
	obj.Counter *= 2;
}

So that's it. Happy coding!

expand_less