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; /// Default Constructor for class public Calculator() { //TODO: implement default constructor } // The Answer property is the returned result public virtual double Answer { get { return this.answer; }
set { this.answer = value; } } /// Calculate an expression 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) { // Blank out result fields and compile result fields InitializeFields(); // change evaluation string to pick up Math class members string expression = RefineEvaluationString(txtCalculate.Text); // build the class using codedom BuildClass(expression); // compile the class into an in-memory assembly. // if it doesn't compile, show errors in the window CompilerResults results = CompileAssembly(); // write out the source code for debugging purposes Console.WriteLine("...........................\r\n"); Console.WriteLine(_source.ToString()); // if the code compiled okay, // run the code using the new assembly (which is inside the results) if (results != null && results.CompiledAssembly != null) { // run the evaluation function 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
/// <summary> /// Main driving routine for building a class /// </summary> void BuildClass(string expression) { // need a string to put the code into _source = new StringBuilder(); StringWriter sw = new StringWriter(_source); //Declare your provider and generator 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")); //Build the class declaration and member variables CodeTypeDeclaration classDeclaration = new CodeTypeDeclaration(); classDeclaration.IsClass = true; classDeclaration.Name = "Calculator"; classDeclaration.Attributes = MemberAttributes.Public; classDeclaration.Members.Add(FieldVariable("answer", typeof(double), MemberAttributes.Private)); //default constructor 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); //home brewed method that uses CodeDom to make a property classDeclaration.Members.Add(this.MakeProperty("Answer", "answer", typeof(double))); //Our Calculate Method 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))); // Include the generation below if you want your answer to pop up in a message box // myMethod.Statements.Add(new CodeSnippetExpression("MessageBox.Show(String.Format(\"Answer = {0}\", Answer))")); // return answer myMethod.Statements.Add(new CodeMethodReturnStatement(new CodeFieldReferenceExpression( new CodeThisReferenceExpression(), "Answer"))); classDeclaration.Members.Add(myMethod); //write code myNamespace.Types.Add(classDeclaration); generator.GenerateCodeFromNamespace(myNamespace, sw, codeOpts); // cleanup 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
/// <summary> /// Compiles the c# into an assembly if there are no syntax errors /// </summary> /// <returns></returns> private CompilerResults CompileAssembly() { // create a compiler ICodeCompiler compiler = CreateCompiler(); // get all the compiler parameters CompilerParameters parms = CreateCompilerParameters(); // compile the code into an assembly 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() { //Create an instance of the C# compiler 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
/// <summary> /// Creawte parameters for compiling /// </summary> /// <returns></returns> CompilerParameters CreateCompilerParameters() { //add compiler parameters and assembly references 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) { //actually compile the code CompilerResults results = compiler.CompileAssemblyFromSource(parms, source); //Do we have any compiler errors? if (results.Errors.Count > 0) { foreach (CompilerError error in results.Errors) WriteLine("Compile Error:"+error.ErrorText); return null; } return results; } |
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
private void RunCode(CompilerResults results) { Assembly executingAssembly = results.CompiledAssembly; try { //can't call the entry method if the assembly is null if (executingAssembly != null) { object assemblyInstance = executingAssembly.CreateInstance("ExpressionEvaluator.Calculator"); //Use reflection to call the static Main function Module[] modules = executingAssembly.GetModules(false); Type[] types = modules[0].GetTypes(); //loop through each class and each method that was defined // and look for the first occurrence of the Calculate method foreach (Type type in types) { MethodInfo[] mis = type.GetMethods(); foreach (MethodInfo mi in mis) { if (mi.Name == "Calculate") { // execute the Calculate method, and retrieve the answer object result = mi.Invoke(assemblyInstance, null); // place the answer on the win form in the result text box 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() { // get a reflected assembly of the System assembly Assembly systemAssembly = Assembly.GetAssembly(typeof(System.Math)); try { //cant call the entry method if the assembly is null if (systemAssembly != null) { //Use reflection to get a reference to the Math class Module[] modules = systemAssembly.GetModules(false); Type[] types = modules[0].GetTypes(); //loop through each class that was defined and look for the first occurrance of the Math class foreach (Type type in types) { if (type.Name == "Math") { // get all of the members of the math class and map them to the same member // name in uppercase 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
/// <summary> /// Need to change eval string to use .NET Math library /// </summary> /// <param name="eval">evaluation expression</param> /// <returns></returns> string RefineEvaluationString(string eval) { // look for regular expressions with only letters Regex regularExpression = new Regex("[a-zA-Z_]+"); // track all functions and constants in the evaluation expression we already replaced ArrayList replacelist = new ArrayList(); // find all alpha words inside the evaluation function that are possible functions MatchCollection matches = regularExpression.Matches(eval); foreach (Match m in matches) { // if the word is found in the math member map, add a Math prefix to it bool isContainedInMathLibrary = _mathMembersMap[m.Value.ToUpper()] != null; if (replacelist.Contains(m.Value) == false && isContainedInMathLibrary) { eval = eval.Replace(m.Value, "Math." + _mathMembersMap[m.Value.ToUpper()]); } // we matched it already, so don't allow us to replace it again replacelist.Add(m.Value); } // return the modified evaluation string 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 yours eyes open for the next generation of the .NET framework while coding in C#.
评论