CodeDom Calculator - Evaluating C# Math Expressions Dynamically

Introduction

CodeDom and Reflection give you the ability to dynamically build C# Code into a string, compile it, and run it all inside your program. This very powerful feature in .NET allows us to create the CodeDom calculator, a calculator that evaluates expressions (and even lines of C# code) inside a Windows Form. We primarily use the System.Math class to do the calculations, but we've coded the CodeDom calculator in such a way so that we don't need to apply the Math. prefix before our functions. We'll show you in a minute how this is done.  

 example_boolean.bmp
Figure 1 - CodeDom Calculator in Action 

 

Usage

The CodeDom Calculator can be used in one of two ways:  a)  just enter some math expression you want to evaluate using C# Syntax.  b) write a block of code in C# to evaluate something more complex. The first method (method a)  only requires you to type in the math expression as shown in figure 2.

example_string.bmp

Figure 2 - Evaluating a long function in the CodeDom Calculator

In method b, we do something a bit different. At the top line you place the word answer terminated with a semicolon. After that you write any C# code you wish.  At the end of your code fragment, remember to assign the final answer to the variable answer. You may still leave off the Math class prefix when writing this code.  Figure 3 is an example of summing numbers from 1 to 10 using C# in CodDom.

figure1.jpg

Figure 3 - Summing numbers from 1 to 10 using code Dom

Creating and Running the Calculator Class

The three steps to evaluating the expression are:  1) Create  C# code around the function using CodeDom 2) compile the code into an assembly using the CodeDom Compiler 3) Create an instance of the Calculator class 4) Call the Calculate method on the Calculator Class to obtain the answer. Figure 2 shows the CodeDom class we wish to generate. The Calculate method will contain the expression we typed into our CodeDom calculator

figure4.jpg

Figure 4 - Calculator class in UML Reverse Engineered using WithClass

The assembly that is actually generated by CodeDom for figure 3 is shown in the listing below. We will talk more about how we generated this class with all the cool methods in CodeDom in the next section, but as you can see, our evaluation code was just slapped right into the Calculate method. The reason we place answer; at the top line is so we can just force a dummy line at the top of the Calculate method for large blocks of code (the dummy line being Answer = answer;)  If we had just put in a simple evaluation expression, such as 1 + 1, this same line becomes a Answer = 1 + 1; inside our code.

Listing 1 - CodeDom generated code for the Calculator

  1. namespace ExpressionEvaluator   
  2.  {  
  3.     using System;  
  4.     using System.Windows.Forms;  
  5.     public class Calculator   
  6.     {          
  7.         private double answer;       
  8.         /// Default Constructor for class  
  9.         public Calculator()   
  10.         {  
  11.             //TODO: implement default constructor  
  12.         }          
  13.         // The Answer property is the returned result  
  14.         public virtual double Answer   
  15.         {  
  16.             get   
  17.                {  
  18.                 return this.answer;  
  19.                }  
  20.   
  21.             set   
  22.               {  
  23.                 this.answer = value;  
  24.               }  
  25.         }  
  26.         /// Calculate an expression  
  27.         public virtual double Calculate()   
  28.         {  
  29.           Answer = answer;  
  30.           for (int i = 1; i <= 10; i++)  
  31.               answer = answer + i;  
  32.             return this.Answer;  
  33.         }  
  34.     }  
  35. }  
The Code

Upon clicking the Calculate button, the code is generated, compiled and run. Listing 2 shows the calculate event handler that executes all of these steps in sequence. Although the details aren't shown here, all of the steps are contained in the methods: BuildClass, CompileAssembly, and RunCode.

Listing 2 - Event Handler for calculating the Math Expression

  1. private void btnCalculate_Click(object sender, System.EventArgs e)  
  2. {  
  3.     // Blank out result fields and compile result fields    
  4.     InitializeFields();  
  5.     // change evaluation string to pick up Math class members    
  6.     tring expression = RefineEvaluationString(txtCalculate.Text);  
  7.     // build the class using codedom    
  8.     BuildClass(expression);  
  9.     // compile the class into an in-memory assembly.    
  10.     // if it doesn't compile, show errors in the window    
  11.     CompilerResults results = CompileAssembly();  
  12.     // write out the source code for debugging purposes    
  13.     Console.WriteLine("...........................\r\n");  
  14.     Console.WriteLine(_source.ToString());  
  15.     // if the code compiled okay,    
  16.     // run the code using the new assembly (which is inside the results)    
  17.     if (results != null && results.CompiledAssembly != null)  
  18.     {  
  19.         // run the evaluation function    
  20.         RunCode(results);  
  21.     }  
  22. }  
So what does a CodeDom generation look like?  If you look at the classes in CodeDom carefully, they almost look like a language grammar break down.  Each constructor uses another CodeDom object to construct it and builds a composite of grammar snippets. Table 1 shows the classes we use in this project to construct our assembly and their individual purpose.

CodeDom Object Purpose
CSharpCodeProvider Provider for generating C# Code
CodeNamespace Class for constructing namespace generation
CodeNamespaceImport Generates using statements
CodeTypeDeclaration Generates class structure
CodeConstructor Generates constructor
CodeTypeReference Generates reference for a type
CodeCommentStatement Generates a C# Comment
CodeAssignStatement Generates assignment statement
CodeFieldReferenceExpression Generates a field reference
CodeThisReferenceExpression Generates a this pointer
CodeSnippetExpression Generates any literal string you specify into the code (used to place our evaluation string)
CodeMemberMethod Generates a new method

Table 1 - CodeDom classes used to build the Calculator

Let's look at our CodeDom method for generating code shown in listing 3. As you can see its easier to get your head around code generation with CodeDom, because it breaks down the generation into simple pieces. First we create the generator and in this case we are generating C#, so we create a C# Generator. Then we begin to create and assemble the pieces. First we create the namespace, then we add to it the different import libraries we want to include. Next we create the class. We add to the class a constructor, a property and a method.  In the method we add statements for the method. Inside these statements, we stick the expression that we typed into the text box to evaluate. The expression we typed in is used in the CodeSnippetExpression constructor so we can generate the code directly from our evaluation string. The expression also uses the constructor of the CodeAssignStatement so we can assign it to the Answer property. When we are finished assembling the composite pieces of the CodeDom hierarchy, we just call GenerateCodeFromNamespace with the CodeDom generator on our assembled namespace. This gets streamed out to our StringWriter and assigned internally to a StringBuilder class where we can extract the whole assembly code from a string.

Listing 3 - Building the Calculator class using CodeDom classes

  1. /// <summary>  
  2. /// Main driving routine for building a class  
  3. /// </summary>  
  4. void BuildClass(string expression)  
  5. {  
  6.     // need a string to put the code into  
  7.     _source = new StringBuilder();  
  8.     StringWriter sw = new StringWriter(_source);  
  9.     //Declare your provider and generator  
  10.     CSharpCodeProvider codeProvider = new CSharpCodeProvider();  
  11.     ICodeGenerator generator = codeProvider.CreateGenerator(sw);  
  12.     CodeGeneratorOptions codeOpts = new CodeGeneratorOptions();  
  13.     CodeNamespace myNamespace = new CodeNamespace("ExpressionEvaluator");  
  14.     myNamespace.Imports.Add(new CodeNamespaceImport("System"));  
  15.     myNamespace.Imports.Add(new CodeNamespaceImport("System.Windows.Forms"));  
  16.     //Build the class declaration and member variables                  
  17.     CodeTypeDeclaration classDeclaration = new CodeTypeDeclaration();  
  18.     classDeclaration.IsClass = true;  
  19.     classDeclaration.Name = "Calculator";  
  20.     classDeclaration.Attributes = MemberAttributes.Public;  
  21.     classDeclaration.Members.Add(FieldVariable("answer"typeof(double), MemberAttributes.Private));  
  22.     //default constructor  
  23.     CodeConstructor defaultConstructor = new CodeConstructor();  
  24.     defaultConstructor.Attributes = MemberAttributes.Public;  
  25.     defaultConstructor.Comments.Add(new CodeCommentStatement("Default Constructor for class"true));  
  26.     defaultConstructor.Statements.Add(new CodeSnippetStatement("//TODO: implement default constructor"));  
  27.     classDeclaration.Members.Add(defaultConstructor);  
  28.     //home brewed method that uses CodeDom to make a property  
  29.     classDeclaration.Members.Add(this.MakeProperty("Answer""answer"typeof(double)));  
  30.     //Our Calculate Method  
  31.     CodeMemberMethod myMethod = new CodeMemberMethod();  
  32.     myMethod.Name = "Calculate";  
  33.     myMethod.ReturnType = new CodeTypeReference(typeof(double));  
  34.     myMethod.Comments.Add(new CodeCommentStatement("Calculate an expression"true));  
  35.     myMethod.Attributes = MemberAttributes.Public;  
  36.     myMethod.Statements.Add(new CodeAssignStatement(new CodeSnippetExpression("Answer"),  
  37.           new CodeSnippetExpression(expression)));  
  38.     //            Include the generation below if you want your answer to pop up in a message box  
  39.     //            myMethod.Statements.Add(new CodeSnippetExpression("MessageBox.Show(String.Format(\"Answer = {0}\", Answer))"));  
  40.     //  return answer  
  41.     myMethod.Statements.Add(new CodeMethodReturnStatement(new CodeFieldReferenceExpression(  
  42.           new CodeThisReferenceExpression(), "Answer")));  
  43.     classDeclaration.Members.Add(myMethod);  
  44.     //write code  
  45.     myNamespace.Types.Add(classDeclaration);  
  46.     generator.GenerateCodeFromNamespace(myNamespace, sw, codeOpts);  
  47.     // cleanup  
  48.     sw.Flush();  
  49.     sw.Close();  
  50. }  
Compiling

Compiling is broken down into 3 pieces: Creating the CodeDom compiler, creating the compile parameters, and compiling the code into the assembly as shown in listing 4.

Listing 4  - Compiling the Assembly with CodeDom

  1. /// <summary>  
  2. /// Compiles the c# into an assembly if there are no syntax errors  
  3. /// </summary>  
  4. /// <returns></returns>  
  5. private CompilerResults CompileAssembly()  
  6. {  
  7.     // create a compiler  
  8.     ICodeCompiler compiler = CreateCompiler();  
  9.     // get all the compiler parameters  
  10.     CompilerParameters parms = CreateCompilerParameters();  
  11.     // compile the code into an assembly  
  12.     CompilerResults results = CompileCode(compiler, parms, _source.ToString());  
  13.     return results;  
  14. }  
The CreateCompiler code simply constructs a C# CodeDom provider object and creates a compiler object from it. 

Listing 5 - Creating the C# Compiler Object

  1. ICodeCompiler CreateCompiler()  
  2. {  
  3.     //Create an instance of the C# compiler     
  4.     CodeDomProvider codeProvider = null;  
  5.     codeProvider = new CSharpCodeProvider();  
  6.     ICodeCompiler compiler = codeProvider.CreateCompiler();  
  7.     return compiler;  
  8. }  

We also need to put together compiler parameters as shown in listing 6. Since we are generating an in-memory dll class library, we need to set the appropriate compiler options to make this happen. Also we can use the parameters to add any reference libraries we want to bring into the picture (namely the System library which contains the System.Math class).

Listing 6 - Creating parameters for the compiler

  1.  /// <summary>  
  2. /// Creawte parameters for compiling  
  3. /// </summary>  
  4. /// <returns></returns>  
  5. CompilerParameters  CreateCompilerParameters()  
  6. {  
  7.     //add compiler parameters and assembly references  
  8.     CompilerParameters compilerParams = new CompilerParameters();  
  9.     compilerParams.CompilerOptions = "/target:library /optimize";  
  10.     compilerParams.GenerateExecutable = false;  
  11.     compilerParams.GenerateInMemory = true;  
  12.     compilerParams.IncludeDebugInformation = false;  
  13.     compilerParams.ReferencedAssemblies.Add("mscorlib.dll");  
  14.     compilerParams.ReferencedAssemblies.Add("System.dll");  
  15.     compilerParams.ReferencedAssemblies.Add("System.Windows.Forms.dll");  
  16.     return compilerParams;  
  17. }  
Finally we need to compile are code. This is accomplished with the method  CompileAssemblyFromSource as shown in listing 7. This method takes the parameters set in listing 5 and the source code of the assembly as a string and compiles the code into an assembly. A reference to the assembly will be assigned in the Compiler results. If there are any errors in the compilation, we write them out to the bottom text box, and set the compiler results to null so we know not to try and run the assembly.

Listing 7 - Compiling the Generated Code into an Assembly using the Compiler Parameters

  1. private CompilerResults CompileCode(ICodeCompiler compiler, CompilerParameters parms, string source)  
  2. {  
  3.                 //actually compile the code  
  4.     CompilerResults results = compiler.CompileAssemblyFromSource(parms, source);   
  5.     //Do we have any compiler errors?  
  6.     if (results.Errors.Count > 0)  
  7.     {  
  8.       foreach (CompilerError error in results.Errors)  
  9.          WriteLine("Compile Error:"+error.ErrorText);   
  10.         return null;  
  11.     }     
  12.     return results;  
  13. }  
Running the Code

Assuming there are no errors after compiling, we can run the assembly. Running the assembly is not accomplished through CodeDom, but rather through Reflection. Reflection allows us to create a Calculator object from our newly created in-memory assembly and run the Calculate method contained within. Listing 8 shows how we run the Calculate method. First we get a reference to the executingAssembly from our results. Then we call CreateInstance on the newly created Calculator class to construct an instance of the class. From our assembly we loop through each class contained within the assembly (in this case only one class, Calculator) and get the class definition. Then we loop through each member in the class and look for the Calculate method.  Once we've obtained the Calculate method, we simply call Invoke on the Calculate method through the object created in CreateInstance. This will execute Calculate on our CodeDom generated assembly and return a resulting double value. The answer is then placed in the result text box.

Listing 8 - Running the Calculate method of the generated Assembly through Reflection

  1. private void RunCode(CompilerResults results)  
  2. {  
  3.       Assembly executingAssembly = results.CompiledAssembly;  
  4.        try  
  5.           {  
  6.             //can't call the entry method if the assembly is null  
  7.             if (executingAssembly != null)  
  8.              {  
  9.                object assemblyInstance = executingAssembly.CreateInstance("ExpressionEvaluator.Calculator");  
  10.               //Use reflection to call the static Main function          
  11.              Module[] modules = executingAssembly.GetModules(false);  
  12.               Type[] types = modules[0].GetTypes();   
  13.                       //loop through each class and each method that was defined  
  14.                      //  and look for the first occurrence of the Calculate method  
  15.                foreach (Type type in types)  
  16.                  {  
  17.                       MethodInfo[] mis = type.GetMethods();  
  18.                       foreach (MethodInfo mi in mis)  
  19.                        {  
  20.                           if (mi.Name == "Calculate")  
  21.                           {  
  22.                              // execute the Calculate method, and retrieve the answer  
  23.                              object result = mi.Invoke(assemblyInstance, null);  
  24.                              // place the answer on the win form in the result text box  
  25.                              txtResult.Text =  result.ToString();  
  26.                           }  
  27.                        }  
  28.                   }  
  29.              }  
  30.           }  
  31.           catch (Exception ex)  
  32.           {  
  33.                 Console.WriteLine("Error:  An exception occurred while executing the script", ex);                  
  34.           }  
  35. }  
Making the Input Compileable

Once of the nice things about accepting code dynamically while the program is running, is that you can decide what is acceptable input before compiling, and then tweak the code input so that it will compile. Another words, the input doesn't initially have to be C#, it just needs to be C# at the point we need to compile it. We decided that the CodeDom calculator would be easier to use if the person typing in the expression didn't have to type the prefix Math before every math function in the System.Math library. By pre-parsing the entered evaluation string, and inserting the Math prefix anywhere it is needed (before compiling), We can make the slightly easier evaluation expression compileable. Also, we felt that the user shouldn't have to worry about case when using the math library, so we handle this situation as well. First we use reflection to create a map of all members of the Math library. Then we use regular expressions to match all words contained in our evaluation strings and see if any of these map to members of the math library. If they do, we append the Math prefix to them before we compile. Listing 9 shows how we can create a map of all members of the Math class through reflection.

Listing 9 - Gathering Members of the Math class through reflection

  1. ArrayList _mathMembers = new ArrayList();  
  2. Hashtable _mathMembersMap = new Hashtable();   
  3. void GetMathMemberNames()  
  4. {  
  5.     // get a reflected assembly of the System assembly  
  6.     Assembly systemAssembly = Assembly.GetAssembly(typeof(System.Math));  
  7.     try  
  8.     {  
  9.         //cant call the entry method if the assembly is null  
  10.         if (systemAssembly != null)  
  11.         {  
  12.             //Use reflection to get a reference to the Math class   
  13.             Module[] modules = systemAssembly.GetModules(false);  
  14.             Type[] types = modules[0].GetTypes();  
  15.              //loop through each class that was defined and look for the first occurrance of the Math class  
  16.             foreach (Type type in types)  
  17.             {  
  18.                 if (type.Name == "Math")  
  19.                 {  
  20.                     // get all of the members of the math class and map them to the same member  
  21.                     // name in uppercase  
  22.                     MemberInfo[] mis = type.GetMembers();  
  23.                     foreach (MemberInfo mi in mis)  
  24.                     {  
  25.                         _mathMembers.Add(mi.Name);  
  26.                         _mathMembersMap[mi.Name.ToUpper()] = mi.Name;  
  27.                     }  
  28.                 }  
  29.             }  
  30.         }  
  31.     }  
  32.     catch (Exception ex)  
  33.     {  
  34.         Console.WriteLine("Error:  An exception occurred while executing the script", ex);  
  35.     }  
  36. }  
Listing 10 shows how we use the map of the Math members to determine where we should place the Math prefix in our code to make it compileable. By using the regular expression class, we can find all the alphabetic words in our evaluation expression. We can then check all those words against the math member map. Any word that is the same name as  a System.Math member will be replaced with the Math prefix and the member name contained in the map. Because the math member map key is upper case and the value in the math member is the proper case, the person typing the evaluation string need not worry about case when typing. Another words, no matter what case the user types in, the evaluation words will be replaced with the proper case previously read in through Reflection of the System.Math library.

Listing 10 - Tweaking the evaluation expression by adding the static Math class prefix

  1. /// <summary>  
  2. /// Need to change eval string to use .NET Math library  
  3. /// </summary>  
  4. /// <param name="eval">evaluation expression</param>  
  5. /// <returns></returns>  
  6. string RefineEvaluationString(string eval)  
  7. {  
  8.     // look for regular expressions with only letters  
  9.     Regex regularExpression = new Regex("[a-zA-Z_]+");  
  10.     // track all functions and constants in the evaluation expression we already replaced  
  11.     ArrayList replacelist = new ArrayList();  
  12.     // find all alpha words inside the evaluation function that are possible functions  
  13.     MatchCollection matches = regularExpression.Matches(eval);  
  14.     foreach (Match m in matches)  
  15.     {  
  16.         // if the word is found in the math member map, add a Math prefix to it  
  17.         bool isContainedInMathLibrary = _mathMembersMap[m.Value.ToUpper()] != null;  
  18.         if (replacelist.Contains(m.Value) == false && isContainedInMathLibrary)  
  19.         {  
  20.             eval = eval.Replace(m.Value, "Math." + _mathMembersMap[m.Value.ToUpper()]);  
  21.         }  
  22.         // we matched it already, so don't allow us to replace it again  
  23.         replacelist.Add(m.Value);  
  24.     }  
  25.     // return the modified evaluation string  
  26.     return eval;  
  27. }  
Conclusion

CodeDom opens up a world of possible dynamic coding that we can conjure on the fly. Code that writes itself may not always bring to mind stories only found in science fiction. Also, there are other practical uses of dynamic code generation such as Aspect Oriented Programming, dynamic state machines, and powerful script engines. I think we will see many more uses for this potent technique. In the meantime, keep your eyes open for the next generation of the .NET framework while coding in C#.


Similar Articles