Click here to Skip to main content
15,868,164 members
Articles / Programming Languages / C#
Article

.Net Expression Evaluator using DynamicMethod

Rate me:
Please Sign up or sign in to vote.
4.97/5 (23 votes)
13 Mar 20076 min read 119.9K   2.1K   102   20
Evaluating Dynamic expressions by compiling C# code to IL, then creating a DynamicMethod from the IL.

.Net Expression Evaluator using DynamicMethod

Introduction

There are already quite a few articles about dynamic expression evaluation using .NET. What I want to show here is a new twist on the idea by using the DynamicMethod class in the System.Reflection.Emit namespace. In my research I've found two main ways to tackle runtime expression evaluation: compile .NET code using the System.CodeDom namespace or write a parser. There are pros and cons with each method but I knew I didn't want to have to write a parser since .NET can already parse C# and will do a much better job at it than I can.

The good thing about compiled code is it's fast. However, there are several problems with it. It seems to take quite a bit of time to compile compared to parsing. So if the expression is constantly changing then it won't be a good performer. But if the expression is something to be compiled once and called many times then its performance can't be beat. So this is the problem I'm trying to solve; given a set of expressions I need to execute them quickly for a given set of values or row of data. Then be able to do the same for a data set by repeating for each row in the set.

Background

Some useful articles/sites I've found:

Parsing the IL of a Method Body
Turn MethodInfo to DynamicMethod
The expression evaluator revisited (Eval function in 100% managed .net)
Evaluating Mathematical Expressions by Compiling C# Code at Runtime
A Math Expression Evaluator

It would be a good idea to look at some of these to get an idea of how this article differs or on what concepts it's based.

So a problem with CodeDom is that compiling causes the resultant assembly to be loaded into the current AppDomain. Since in .NET you can't unload an assembly you would end up with a lot of assemblies since a new one is created for each new expression. To keep the generated assembly from being loaded right away another AppDomain can be created where code the is generated. But when we want to execute the expression the assembly still has to be loaded. So what can we do to get around this problem? Enter System.Reflection.Emit.DynamicMethod class to save the day.

By now I'm sure you just looked in MSDN to find out more about it. DynamicMethod allows a method to be created at runtime and have the speed of compiled code. This is great but just one problem: you need to write the IL code for it.

I knew I didn't want to write a parser and I sure didn't want to spend a lot of time with writing IL code. The bright idea comes from the Turn MethodInfo to DynamicMethod article linked above. Basically reflection can be used to get the IL code of a compiled method and generate a DynamicMethod from it. Perfect! Now we can write .NET code, compile it, get the IL code and create the DynamicMethod we need.

Using the code

To turn a MethodInfo into a DynamicMethod I needed to know what method was being used. I know I wanted access to functions in the same class as the method, so I didn't need to prefix with a class name. I wanted to provide a set of documented functions to the user without them having to know a lot of .NET. So I created an object with some functions on it and called it FunctionClass.

C#
public class FunctionClass
{
    //some instance data, could be another object
    //maybe a datarow or custom data object
    public double X;
    public double Y;
    public double Z;

    //method that can be called
    public double Round(double number)
    {
        return Math.Round(number);
    }
}

CodeDom Compiler

So to compile a C# expression that will have access to the methods on this class I'm going to make the compiled method on a class that inherits from FunctionClass. This way it will have access to all protected and public methods from the base class and call them without any type prefix. The assumption I've made is that an expression is one line of code so it looks like return [user code line];. To keep it type safe I'm also going to require that the user pick a return type for the expression; double, string, DateTime, etc. So below is the code for the compiler class.

C#
/// <summary>
/// Expression compiler using the C# language
/// </summary>
public class CSharpExpressionCompiler : BaseExpressionCompiler, 
    IExpressionCompiler
{

    /// <summary>
    /// Compiles the expression into an assembly and returns the method code 
    /// for it.
    /// It should compile the method into a class that inherits from the 
    /// functionType.
    /// </summary>
    /// <param name="expression">expression to be 
    /// compiled</param>
    /// <param name="functionType">Type of the function class 
    /// to use</param>
    /// <param name="returnType">Return type of the method to 
    /// create</param>
    /// <returns>DynamicMethodState - A serialized version of the 
    /// method code</returns>
    public DynamicMethodState CompileExpression(string expression, 
        Type functionType, Type returnType)
    {
        DynamicMethodState methodState;

        //use CodeDom to compile using C#
        CodeDomProvider codeProvider = 
            CodeDomProvider.CreateProvider("CSharp");

        CompilerParameters loParameters = new CompilerParameters();

        //add assemblies
        loParameters.ReferencedAssemblies.Add("System.dll");
        loParameters.ReferencedAssemblies.Add(functionType.Assembly.Location);
        //don't generate assembly on disk and treat warnings as errors
        loParameters.GenerateInMemory = true;
        loParameters.TreatWarningsAsErrors = true;

        //set namespace of dynamic class
        string dynamicNamespace = "ExpressionEval.Functions.Dynamic";

        //set source for inherited class - need to change to use CodeDom 
        //objects instead
        string source = @"
using System;
using {5};

namespace {6}
{{
public class {0} : {1}
{{
    public {2} {3}()
    {{
        return {4};
    }}
}}
}}
";

        //set source code replacements
        string className = "Class_" + Guid.NewGuid().ToString("N");
        string methodName = "Method_" + Guid.NewGuid().ToString("N");
        string returnTypeName = returnType.FullName;

        //check for generic type for return
        ....

        //format codestring with replacements
        string codeString = string.Format(source, className, 
            functionType.FullName, returnTypeName, 
        methodName, expression, functionType.Namespace, dynamicNamespace);

        //compile the code
        CompilerResults results = 
            codeProvider.CompileAssemblyFromSource(loParameters, codeString);

        if (results.Errors.Count > 0)
        {
            //throw an exception for any errors
            throw new CompileException(results.Errors);
        }
        else
        {
            //get the type that was compiled
            Type dynamicType = results.CompiledAssembly.GetType(
                dynamicNamespace + "." + className);

            //get the MethodInfo for the compiled expression
            MethodInfo dynamicMethod = dynamicType.GetMethod(methodName);

            //get the compiled expression as serializable object
            methodState = GetMethodState(dynamicMethod);

        }

        return methodState;
    }
}

The BaseExpressionCompiler provides the GetMethodState method that converts a MethodInfo into a DynamicMethodState. DynamicMethodState is the class that provides a serializable form of a method's IL and metadata token offsets. Once I have that I can throw away the assembly that was created.

C#
/// <summary>
/// A base expression compiler.  MarshalByRef so it can be used across 
/// AppDomains.
/// </summary>
public abstract class BaseExpressionCompiler : MarshalByRefObject
{
    /// <summary>
    /// Converts a MethodInfo into a serialized version of it.
    /// </summary>
    /// <param name="dynamicMethod">The method for which to 
    /// create a DynamicMethod for</param>
    /// <returns>DynamicMethodState - serialized version of a 
    /// method.</returns>
    protected DynamicMethodState GetMethodState(MethodInfo dynamicMethod)
    {
        DynamicMethodState methodState = new DynamicMethodState();

        //IL info from method
        MethodBody methodIlCode = dynamicMethod.GetMethodBody();

        //get code bytes and other method properties
        methodState.CodeBytes = methodIlCode.GetILAsByteArray();
        methodState.InitLocals = methodIlCode.InitLocals;
        methodState.MaxStackSize = methodIlCode.MaxStackSize;

        //get any local variable information
        IDictionary<int, LocalVariable> locals = new SortedList<int, 
            LocalVariable>();

        foreach (LocalVariableInfo localInfo in methodIlCode.LocalVariables)
        {
            locals.Add(localInfo.LocalIndex, new 
                LocalVariable(localInfo.IsPinned, 
                localInfo.LocalType.TypeHandle));
        }

        methodState.LocalVariables = locals;

        TokenOffset tokenOffset = new TokenOffset();

        //get metadata token offsets
        IlReader reader = new IlReader(methodState.CodeBytes, 
            dynamicMethod.Module);

        tokenOffset.Fields = reader.Fields;
        tokenOffset.Methods = reader.Methods;
        tokenOffset.Types = reader.Types;
        tokenOffset.LiteralStrings = reader.LiteralStrings;

        methodState.TokenOffset = tokenOffset;

        return methodState;
    }
}

The compiler class inherits from MarshalByRefObject because it needs to be used across an AppDomain as explained earlier. Now that there is a compiler to get a DynamicMethodState we then need to create the DynamicMethod object from it.

DynamicMethod Delegate

The ExpressionDelegateFactory calls to the compiler across an AppDomain it creates to compile an expression. It then creates a delegate from the DynamicMethodState returned by the compiler in the form of R ExecuteExpression<R,C>(C functionClass) where R is the return type and C is the function class type.

C#
/// <summary>
/// Implements a Delegate Factory for compiled expressions
/// </summary>
public class ExpressionDelegateFactory : IExpressionDelegateFactory
{
    private ExpressionLanguage m_language;

    /// <summary>
    /// Delegate Factory for a Language
    /// </summary>
    /// <param name="language"></param>
    public ExpressionDelegateFactory(ExpressionLanguage language)
    {
        m_language = language;
    }

    /// <summary>
    /// Compiles an expression and returns a delegate to the compiled code.
    /// </summary>
    /// <typeparam name="R">The return type of the 
    /// expression</typeparam>
    /// <typeparam name="C">The type of the function 
    /// class</typeparam>
    /// <param name="expression">Expression to 
    /// evaluate</param>
    /// <returns>ExecuteExpression<R, C> - a delegate that calls  
    /// the compiled expression</returns>
    public ExecuteExpression<R, C> CreateExpressionDelegate<R, 
        C>(string expression)
    {
        ExecuteExpression<R, C> expressionDelegate = null;
        DynamicMethodState methodState;

        //create the compiled expression
        methodState = CreateExpressionMethodState<R, C>(expression);

        if (methodState != null && methodState.CodeBytes != null)
        {
            //get a dynamic method delegate from the method state
            expressionDelegate = CreateExpressionDelegate<R, 
                C>(methodState);
        }

        return expressionDelegate;
    }

    /// <summary>
    /// Compiles an expression and returns a DynamicMethodState
    /// </summary>
    /// <typeparam name="R">The return type of the 
    /// expression</typeparam>
    /// <typeparam name="C">The type of the function 
    /// class</typeparam>
    /// <param name="expression">Expression to 
    /// evaluate</param>
    /// <returns>DynamicMethodState - serialized version of the 
    /// compiled expression</returns>
    public DynamicMethodState CreateExpressionMethodState<R, 
        C>(string expression)
    {
        DynamicMethodState methodState;
        IExpressionCompiler compiler;

        //create an AppDomain
        AppDomainSetup loSetup = new AppDomainSetup();
        loSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
        AppDomain loAppDomain = 
            AppDomain.CreateDomain("CompilerDomain", null, loSetup);

        //get the compiler to use based on the language
        string className = null;
        switch(m_language)
        {
            case ExpressionLanguage.CSharp:
                className = "CSharpExpressionCompiler";
                break;
            case ExpressionLanguage.VisualBasic:
                className = "VisualBasicExpressionCompiler";
                break;
            case ExpressionLanguage.JScript:
                className = "JScriptExpressionCompiler";
                break;
        }

        //create an instance of a compiler
        compiler=(IExpressionCompiler)loAppDomain.CreateInstanceFromAndUnwrap(
                    "ExpressionEval.ExpressionCompiler.dll", 
                    "ExpressionEval.ExpressionCompiler." + className);

        try
        {
            //compile the expression
            methodState = compiler.CompileExpression(expression, typeof(C), 
                typeof(R));
        }
        catch (CompileException e)
        {
            //catch any compile errors and throw an overall exception
            StringBuilder exceptionMessage = new StringBuilder();

            foreach (CompilerError error in e.CompileErrors)
            {
                exceptionMessage.Append("Error# ").Append(error.ErrorNumber);
                exceptionMessage.Append(", column ").Append(error.Column);
                exceptionMessage.Append(", ").Append(error.ErrorText);
                exceptionMessage.Append(Environment.NewLine);
            }

            throw new ApplicationException(exceptionMessage.ToString());
        }
        finally
        {
            //unload the AppDomain
            AppDomain.Unload(loAppDomain);
        }

        //if for some reason the code byte were not sent then return null
        if (methodState != null && methodState.CodeBytes == null)
        {
            methodState = null;
        }

        return methodState;
    }

    /// <summary>
    /// Compiles a DynamicMethodState and returns a delegate.
    /// </summary>
    /// <typeparam name="R">The return type of the 
    /// expression</typeparam>
    /// <typeparam name="C">The type of the function 
    /// class</typeparam>
    /// <param name="methodState">The serialized version of a 
    /// method on the functionClass</param>
    /// <returns>ExecuteExpression<R, C> - a delegate that calls 
    /// the compiled expression</returns>
    public ExecuteExpression<R, C> CreateExpressionDelegate<R, 
        C>(DynamicMethodState methodState)
    {
        ExecuteExpression<R, C> expressionDelegate;

        //create a dynamic method
        DynamicMethod dynamicMethod = new DynamicMethod(
                        "_" + Guid.NewGuid().ToString("N"), typeof(R),
                         new Type[] { typeof(C) }, typeof(C));

        //get the IL writer for it
        DynamicILInfo dynamicInfo = dynamicMethod.GetDynamicILInfo();

        //set the properties gathered from the compiled expression
        dynamicMethod.InitLocals = methodState.InitLocals;

        //set local variables
        SignatureHelper locals = SignatureHelper.GetLocalVarSigHelper();
        foreach (int localIndex in methodState.LocalVariables.Keys)
        {
            LocalVariable localVar = methodState.LocalVariables[localIndex];
            locals.AddArgument(Type.GetTypeFromHandle(localVar.LocalType), 
                localVar.IsPinned);
        }

        dynamicInfo.SetLocalSignature(locals.GetSignature());

        //resolve any metadata tokens
        IlTokenResolver tokenResolver = new IlTokenResolver(
                        methodState.TokenOffset.Fields, 
                             methodState.TokenOffset.Methods, 
                        methodState.TokenOffset.Types, 
                            methodState.TokenOffset.LiteralStrings);
                        
        methodState.CodeBytes = tokenResolver.ResolveCodeTokens(
             methodState.CodeBytes, dynamicInfo);

        //set the IL code for the dynamic method
        dynamicInfo.SetCode(methodState.CodeBytes, methodState.MaxStackSize);

        //create a delegate for fast execution
        expressionDelegate = (ExecuteExpression<R, 
            C>)dynamicMethod.CreateDelegate(                                           
                typeof(ExecuteExpression<R, C>));

        return expressionDelegate;
    }

}

The 'magic' for DynamicMethod is that it behaves like a static method except it can also be used like an instance method. In IL code the first argument in an instance method is always the instance of the type to which the method belongs. So to trick this static method to behave like an instance, you make sure the first argument of the method is an instance of the type to which the method belongs. Read about this in MSDN because its the important concept that makes this code work.

I also wanted to hide the implementation of creating a DynamicMethod from the code using the expression so I created an ExpressionEvaluator that wraps the calls to the ExpressionDelegateFactory. The code is separated into 5 different projects: MsilConversion (IL reading and token resolving), MethodState (shared dll between app domains, DynamicMethodState), ExpressionCompiler (langauge specific compilers), DynamicMethodGenerator (DynamicMethod delegate factory), ExpressionEvaluation (wrapper around delegate factory).

I created a Windows Forms test application aptly named TestApp to call the ExpressionEvaluation engine. It has a simple one-time Evaluate and then a Loop function that shows the performance that a 'compile once, run many times' operation can achieve.

Possibilities

There appear to be quite a few possibilities from here for implementation enhancements or changes. Different .NET languages, a language translator from custom language to .NET language, supporting multiple code lines instead of a one line expression. Maybe being even more ambitious and creating a parser/IL generator to skip the CodeDom step and write IL directly.

Feedback

I would appreciate any feedback you can give me on the code, concept, or the article itself. Also, I'm curious about your ideas for enhancements and if you implement this concept what was the result.

History

  • 3/13/2007 - First Published

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
United States United States
.Net Software Engineer in Kansas, USA trying to keep pace with technology.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Kanasz Robert1-Dec-11 2:48
professionalKanasz Robert1-Dec-11 2:48 
GeneralProblem with Generics [modified] Pin
Gummy356-Dec-08 11:32
Gummy356-Dec-08 11:32 
Generalproblem with arrays Pin
svab12312-Aug-08 23:12
svab12312-Aug-08 23:12 
GeneralRe: problem with arrays Pin
luikku13-Aug-08 3:28
luikku13-Aug-08 3:28 
GeneralRe: problem with arrays Pin
luikku1-Sep-08 1:04
luikku1-Sep-08 1:04 
GeneralRe: problem with arrays Pin
macsux6-Jun-11 10:18
macsux6-Jun-11 10:18 
GeneralCannot seem to pass arrays in Pin
tomhornyak3-Jul-08 9:56
tomhornyak3-Jul-08 9:56 
Generalgot a access violation Pin
stevebur13-Nov-07 12:09
stevebur13-Nov-07 12:09 
GeneralRe: got a access violation Pin
thefellow3j12-Jan-08 0:01
thefellow3j12-Jan-08 0:01 
GeneralASP.NET implementation Pin
Konstatin Shilov6-Sep-07 3:22
Konstatin Shilov6-Sep-07 3:22 
GeneralRequest for the permission of type 'System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' failed. Pin
Robson Félix14-Jun-07 11:32
Robson Félix14-Jun-07 11:32 
GeneralRe: Request for the permission of type 'System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' failed. Pin
Wilson, Drew14-Jun-07 16:21
Wilson, Drew14-Jun-07 16:21 
GeneralRe: Request for the permission of type 'System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' failed. Pin
Robson Félix15-Jun-07 5:00
Robson Félix15-Jun-07 5:00 
Generalperformance? .. Pin
chiao.vincent7-Jun-07 8:47
chiao.vincent7-Jun-07 8:47 
GeneralRe: performance? .. Pin
Wilson, Drew7-Jun-07 21:23
Wilson, Drew7-Jun-07 21:23 
GeneralRe: performance? .. Pin
Tim McCurdy9-Jun-08 7:07
Tim McCurdy9-Jun-08 7:07 
GeneralRe: performance? .. Pin
BinShao16-Oct-13 2:45
BinShao16-Oct-13 2:45 
GeneralFormatting generic arguments Pin
JoostB24-May-07 3:36
JoostB24-May-07 3:36 
GeneralNice toy, and something weird Pin
Marc Heiligers2-May-07 1:34
Marc Heiligers2-May-07 1:34 
GeneralRe: Nice toy, and something weird Pin
Wilson, Drew2-May-07 15:26
Wilson, Drew2-May-07 15:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.