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.
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.
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.
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
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
- namespace ExpressionEvaluator
- {
- using System;
- using System.Windows.Forms;
- public class Calculator
- {
- private double answer;
-
- public Calculator()
- {
-
- }
-
- public virtual double Answer
- {
- get
- {
- return this.answer;
- }
-
- set
- {
- this.answer = value;
- }
- }
-
- public virtual double Calculate()
- {
- Answer = answer;
- for (int i = 1; i <= 10; i++)
- answer = answer + i;
- return this.Answer;
- }
- }
- }
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
- private void btnCalculate_Click(object sender, System.EventArgs e)
- {
-
- InitializeFields();
-
- tring expression = RefineEvaluationString(txtCalculate.Text);
-
- BuildClass(expression);
-
-
- CompilerResults results = CompileAssembly();
-
- Console.WriteLine("...........................\r\n");
- Console.WriteLine(_source.ToString());
-
-
- if (results != null && results.CompiledAssembly != null)
- {
-
- RunCode(results);
- }
- }
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
-
-
-
- void BuildClass(string expression)
- {
-
- _source = new StringBuilder();
- StringWriter sw = new StringWriter(_source);
-
- CSharpCodeProvider codeProvider = new CSharpCodeProvider();
- ICodeGenerator generator = codeProvider.CreateGenerator(sw);
- CodeGeneratorOptions codeOpts = new CodeGeneratorOptions();
- CodeNamespace myNamespace = new CodeNamespace("ExpressionEvaluator");
- myNamespace.Imports.Add(new CodeNamespaceImport("System"));
- myNamespace.Imports.Add(new CodeNamespaceImport("System.Windows.Forms"));
-
- CodeTypeDeclaration classDeclaration = new CodeTypeDeclaration();
- classDeclaration.IsClass = true;
- classDeclaration.Name = "Calculator";
- classDeclaration.Attributes = MemberAttributes.Public;
- classDeclaration.Members.Add(FieldVariable("answer", typeof(double), MemberAttributes.Private));
-
- CodeConstructor defaultConstructor = new CodeConstructor();
- defaultConstructor.Attributes = MemberAttributes.Public;
- defaultConstructor.Comments.Add(new CodeCommentStatement("Default Constructor for class", true));
- defaultConstructor.Statements.Add(new CodeSnippetStatement("//TODO: implement default constructor"));
- classDeclaration.Members.Add(defaultConstructor);
-
- classDeclaration.Members.Add(this.MakeProperty("Answer", "answer", typeof(double)));
-
- CodeMemberMethod myMethod = new CodeMemberMethod();
- myMethod.Name = "Calculate";
- myMethod.ReturnType = new CodeTypeReference(typeof(double));
- myMethod.Comments.Add(new CodeCommentStatement("Calculate an expression", true));
- myMethod.Attributes = MemberAttributes.Public;
- myMethod.Statements.Add(new CodeAssignStatement(new CodeSnippetExpression("Answer"),
- new CodeSnippetExpression(expression)));
-
-
-
- myMethod.Statements.Add(new CodeMethodReturnStatement(new CodeFieldReferenceExpression(
- new CodeThisReferenceExpression(), "Answer")));
- classDeclaration.Members.Add(myMethod);
-
- myNamespace.Types.Add(classDeclaration);
- generator.GenerateCodeFromNamespace(myNamespace, sw, codeOpts);
-
- sw.Flush();
- sw.Close();
- }
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
-
-
-
-
- private CompilerResults CompileAssembly()
- {
-
- ICodeCompiler compiler = CreateCompiler();
-
- CompilerParameters parms = CreateCompilerParameters();
-
- CompilerResults results = CompileCode(compiler, parms, _source.ToString());
- return results;
- }
The CreateCompiler code simply constructs a C# CodeDom provider object and creates a compiler object from it.
Listing 5 - Creating the C# Compiler Object
- ICodeCompiler CreateCompiler()
- {
-
- CodeDomProvider codeProvider = null;
- codeProvider = new CSharpCodeProvider();
- ICodeCompiler compiler = codeProvider.CreateCompiler();
- return compiler;
- }
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
-
-
-
-
- CompilerParameters CreateCompilerParameters()
- {
-
- CompilerParameters compilerParams = new CompilerParameters();
- compilerParams.CompilerOptions = "/target:library /optimize";
- compilerParams.GenerateExecutable = false;
- compilerParams.GenerateInMemory = true;
- compilerParams.IncludeDebugInformation = false;
- compilerParams.ReferencedAssemblies.Add("mscorlib.dll");
- compilerParams.ReferencedAssemblies.Add("System.dll");
- compilerParams.ReferencedAssemblies.Add("System.Windows.Forms.dll");
- return compilerParams;
- }
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
- private CompilerResults CompileCode(ICodeCompiler compiler, CompilerParameters parms, string source)
- {
-
- CompilerResults results = compiler.CompileAssemblyFromSource(parms, source);
-
- if (results.Errors.Count > 0)
- {
- foreach (CompilerError error in results.Errors)
- WriteLine("Compile Error:"+error.ErrorText);
- return null;
- }
- return results;
- }
Running the CodeAssuming 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
- private void RunCode(CompilerResults results)
- {
- Assembly executingAssembly = results.CompiledAssembly;
- try
- {
-
- if (executingAssembly != null)
- {
- object assemblyInstance = executingAssembly.CreateInstance("ExpressionEvaluator.Calculator");
-
- Module[] modules = executingAssembly.GetModules(false);
- Type[] types = modules[0].GetTypes();
-
-
- foreach (Type type in types)
- {
- MethodInfo[] mis = type.GetMethods();
- foreach (MethodInfo mi in mis)
- {
- if (mi.Name == "Calculate")
- {
-
- object result = mi.Invoke(assemblyInstance, null);
-
- txtResult.Text = result.ToString();
- }
- }
- }
- }
- }
- catch (Exception ex)
- {
- Console.WriteLine("Error: An exception occurred while executing the script", ex);
- }
- }
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
- ArrayList _mathMembers = new ArrayList();
- Hashtable _mathMembersMap = new Hashtable();
- void GetMathMemberNames()
- {
-
- Assembly systemAssembly = Assembly.GetAssembly(typeof(System.Math));
- try
- {
-
- if (systemAssembly != null)
- {
-
- Module[] modules = systemAssembly.GetModules(false);
- Type[] types = modules[0].GetTypes();
-
- foreach (Type type in types)
- {
- if (type.Name == "Math")
- {
-
-
- MemberInfo[] mis = type.GetMembers();
- foreach (MemberInfo mi in mis)
- {
- _mathMembers.Add(mi.Name);
- _mathMembersMap[mi.Name.ToUpper()] = mi.Name;
- }
- }
- }
- }
- }
- catch (Exception ex)
- {
- Console.WriteLine("Error: An exception occurred while executing the script", ex);
- }
- }
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
-
-
-
-
-
- string RefineEvaluationString(string eval)
- {
-
- Regex regularExpression = new Regex("[a-zA-Z_]+");
-
- ArrayList replacelist = new ArrayList();
-
- MatchCollection matches = regularExpression.Matches(eval);
- foreach (Match m in matches)
- {
-
- bool isContainedInMathLibrary = _mathMembersMap[m.Value.ToUpper()] != null;
- if (replacelist.Contains(m.Value) == false && isContainedInMathLibrary)
- {
- eval = eval.Replace(m.Value, "Math." + _mathMembersMap[m.Value.ToUpper()]);
- }
-
- replacelist.Add(m.Value);
- }
-
- return eval;
- }
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#.