Thursday, December 15, 2011

Dynamically creating lambda expressions in .Net (C#)


Dynamically creating lambda expressions in .Net (C#)

Passing lambda expressions as strings
In a situation where you want to allow a user to create his own lambda expressions at runtime, there are not a lot of options. Whether you read the expression from a file, or an input box, you end up with a string that you wish to convert. There is no method in .Net 4.0 that lets you do just that (easily).
But as for most things, workarounds exist. Below is the one I’m using. Better solutions are very welcome.

Suppose you want to call Regex.Replace with a lambda expression for the MatchEveluator. At design time this is easy:
                Regex.Replace(“MyInputString”, @”\B[A-Z]”, m => “ “ + m.ToString().ToLower());
This results in: "My input string"

But when the lambda expression is a string, that won’t work:
String input = “MyInputString”;
String expr = @”\B[A-Z]”;
String lambda = “m => \” \” + m.ToString().ToLower();
Regex.Replace(input, expr, lambda);
This results in:  "Mym => \" \" + m.ToString().ToLower()nputm => \" \" + m.ToString().ToLower()tring"


Logical, but not quite what’s intended.

In order to convert the string to a true lambda expression would take considerable effort. However, creating a regular ‘method’ at runtime is a lot easier.

The question then becomes: how can we leverage the normal compiler routines in compiling lambda ‘strings’. Here’s an example. It requires 3 steps:

Step 1: create a method that builds of the body of a class and method

private string createRegexClassInMemory(
    string input,

    string expr,
    string lambda,
    RegexOptions regexOptions = RegexOptions.None) {
        StringBuilder classbuilder = new StringBuilder("using System;”);
        classbuilder.Append(“using System.Text;");
        classbuilder.Append(“using System.Text.RegularExpressions;");

        // Note that we need to double escape 'escapes' and double accolade 'accolades'
        classbuilder.AppendFormat("namespace {0}{{", GetType().Namespace);
        classbuilder.Append(@"
            public class RegexLambdaClass{
            public static string RegexLambda(){");

        // Note that we need to double escape 'escapes', except for the lambda expression, as it gets handled differently
        input = input.Replace("\\", "\\\\");
        expr = expr.Replace("\\", "\\\\");

        // Note the lack of double-quotes around the third parameter !!!!!!
        classbuilder.AppendFormat("return Regex.Replace(\"{0}\", \"{1}\", {2}, RegexOptions.{3});",
            input,
            expr,
            lambda,
            regexOptions);
        classbuilder.Append("}}}");
        return classbuilder.ToString();
}

Step 2: create a module to compile and run this code

The code below has been adapted from “http://www.codeproject.com/KB/cs/runtimecompiling.aspx”.

using System;
using System.Text;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Reflection;
using System.Text.RegularExpressions;

namespace Invocation {

    /// Example
    /// /x myNamespace MyClass MyFunction False param1,param2,param3,etc
    /// The False states that MyFunction is not static.
    
    /// <summary>
    /// This class can be used to execute dynamic uncompiled code at runtime
    /// This class is not exception safe, all function calls should be exception handled.
    /// </summary>
    public class CodeCompiler {

        private CompilerParameters compilerparams = new CompilerParameters();
        /// <summary>
        /// Default Constructor.
        /// It is necessary to have an instance of the class so that the reflection
        /// can get the namespace of this class dynamically.
        /// </summary>
        /// <remarks>
        /// This class could be static, but I wanted to make it easy for developers
        /// to use this class by allowing them to change the namespace without
        /// harming the code.
        /// </remarks>
        public CodeCompiler() {
            compilerparams.GenerateExecutable = false;
            compilerparams.GenerateInMemory = true;
            // Add System reference
            this.addReference(typeof(System.String).Assembly.Location);
        }

        public void clearReferences() {
            compilerparams.ReferencedAssemblies.Clear();
        }

        public void addReference(string reference) {

            compilerparams.ReferencedAssemblies.Add(reference);
        }

        /// <summary>
        /// This is the main code execution function.
        /// It allows for any compilable c# code to be executed.
        /// </summary>
        /// <param name="code">the code to be compiled then executed</param>
        /// <param name="namespacename">the name of the namespace to be executed</param>
        /// <param name="classname">the name of the class of the function in the code that you will execute</param>
        /// <param name="functionname">the name of the function that you will execute</param>
        /// <param name="isstatic">True if the function you will execute is static, otherwise false</param>
        /// <param name="args">any parameters that must be passed to the function</param>
        /// <returns>what ever the executed function returns, in object form</returns>
        public object ExecuteCode(string code, string namespacename, string classname,
            string functionname, bool isstatic, params object[] args) {
            object returnval = null;
            Assembly asm = BuildAssembly(code);
            object instance = null;
            Type type = null;
            if (isstatic) {
                type = asm.GetType(namespacename + "." + classname);
            }
            else {
                instance = asm.CreateInstance(namespacename + "." + classname);
                type = instance.GetType();
            }
            MethodInfo method = type.GetMethod(functionname);
            returnval = method.Invoke(instance, args);
            return returnval;
        }

        /// <summary>
        /// This private function builds the assembly file into memory after compiling the code
        /// </summary>
        /// <param name="code">C# code to be compiled</param>
        /// <returns>the compiled assembly object</returns>
        private Assembly BuildAssembly(string code) {
            Microsoft.CSharp.CSharpCodeProvider provider = new CSharpCodeProvider();

            CompilerResults results = provider.CompileAssemblyFromSource(compilerparams, code);

            if (results.Errors.HasErrors)
            {
                StringBuilder errors = new StringBuilder("Compiler Errors :\r\n");
                foreach (CompilerError error in results.Errors)
                {
                    errors.AppendFormat("Line {0},{1}\t: {2}\n", error.Line, error.Column, error.ErrorText);
                }
                throw new Exception(errors.ToString());
            }
            else
            {
                return results.CompiledAssembly;
            }
        }
    }
}


Step 3: call the code above

Invocation.CodeCompiler cc = new Invocation.CodeCompiler();
// We need to add the necessary references !!!!!
cc.addReference(typeof(System.Text.RegularExpressions.Regex).Assembly.Location);
string result = (string)cc.ExecuteCode(
    createRegexClassInMemory(
        input,
        expr,
        lambda,
        regexOptions: RegexOptions.IgnoreCase),
    GetType().Namespace,
    "RegexLambdaClass",
    "RegexLambda", true);



Note that if you don’t need lambda expressions, you could add the parameters as proper ‘method’ parameters. The build-up and execution code could then be as follows:

private string createRegexClassInMemory() {
    StringBuilder classbuilder = new StringBuilder("using System;”);
    classbuilder.Append(“using System.Text;");
    classbuilder.Append(“using System.Text.RegularExpressions;");

    // Note that we need to double escape 'escapes' and double accolade 'accolades'
    classbuilder.AppendFormat("namespace {0}{{", GetType().Namespace);
    classbuilder.Append(@"
        public class RegexLambdaClass{
        public static string RegexNoLambda(string input, string expr, MatchEvaluator me, RegexOptions regexOptions = RegexOptions.None){");
    classbuilder.Append("return Regex.Replace(input, expr, me, regexOptions);"); classbuilder.Append("}}}");
    return classbuilder.ToString();
}

Invocation.CodeCompiler cc = new Invocation.CodeCompiler();
// We need to add the necessary references !!!!!
cc.addReference(typeof(System.Text.RegularExpressions.Regex).Assembly.Location);
string result = (string)cc.ExecuteCode(
    createRegexClassInMemory(),
    GetType().Namespace,
    "RegexLambdaClass",
    "RegexLambda", true, 
    input, expr, new MatchEvaluator(delegate), RegexOptions.IgnoreCase);

No comments: