Building a Template Engine - Part 2

In part 1, I start to build a very simple template engine using regular expressions. In this article I want to add another trick which can be helpful for creating dynamic content programmatically - the use of the using System.CodeDom.Compiler namespace, and run-time compilation of code. In a project I did a couple of years ago, we wanted the ability for users to store parameters based on some 'expression'. This expression would then be parsed to produce text on a report. Typically, the expression would be to generate today's date or some date based on today (eg end of last month, start of last month etc). Rather than building a pre-defined list of suitable placeholders for these variables, I decided to use the power of the System.CodeDom.Compiler classes, so that the user could type in a .NET 'snippet' expression, and this expression would be compiled during execution of the program, and then evaluated, to return the required text value. Therefore, users would be able to enter in strings like:

DateTime.Now.ToString()

First, I added a few class variables to the Templater class:

1 2 3private CodeDomProvider codeDomProvider; private List<string> usingNamespaces = new List<string>(); private List<string> references = new List<string>();
Next, a constructor to enable a codeDomProvider to be passed in to the Templater class. This is so that either C# or VB.NET can be specified.
1 2 3 4 5 6 7 8 9 10 11 12public Templater(CodeDomProvider codeDomProvider) { this.codeDomProvider = codeDomProvider; // default namespaces usingNamespaces.Add("System"); usingNamespaces.Add("System.Xml"); usingNamespaces.Add("System.Data"); usingNamespaces.Add("System.Windows.Forms"); usingNamespaces.Add("System.Drawing"); usingNamespaces.Add("System.Collections.Generic"); }
The next private method is used to generate the code text to compile:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25private string GetCode(string code, Dictionary<string, object> model) { StringBuilder sb = new StringBuilder(""); foreach (var m in model.Values) { usingNamespaces.Add(m.GetType().Namespace); } foreach (var n in usingNamespaces) { sb.Append("using " + n + ";\n"); } sb.Append("namespace Templater\n"); sb.Append("{\n"); sb.Append("public class Template { \n"); sb.Append("public object Eval(Dictionary<string, object> Model){\n"); sb.Append("return " + code + "; \n"); sb.Append("} \n"); sb.Append("} \n"); sb.Append("}\n"); return sb.ToString(); }
Next, we need a method to get the full code, compile it, execute it, and get the return value. This is all done in the Eval() method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43private object Eval(string code, Dictionary<string, object> model) { code = code.Substring(2, code.Length - 4); AppDomain ad = AppDomain.CreateDomain("TemplaterAppDomain"); ICodeCompiler icc = codeDomProvider.CreateCompiler(); // add useful references CompilerParameters cp = new CompilerParameters(); cp.ReferencedAssemblies.Add("mscorlib.dll"); cp.ReferencedAssemblies.Add("system.dll"); cp.ReferencedAssemblies.Add("system.xml.dll"); cp.ReferencedAssemblies.Add("system.data.dll"); cp.ReferencedAssemblies.Add("system.windows.forms.dll"); cp.ReferencedAssemblies.Add("system.drawing.dll"); foreach (var m in model.Values) { cp.ReferencedAssemblies.Add(m.GetType().Assembly.GetLoadedModules().First().Name); } cp.CompilerOptions = "/t:library"; cp.GenerateInMemory = false; cp.OutputAssembly = "Templater.dll"; CompilerResults cr = icc.CompileAssemblyFromSource(cp, GetCode(code,model)); if (cr.Errors.Count > 0) { // error handling here... return null; } System.Reflection.Assembly a = cr.CompiledAssembly; ad.Load("Templater"); object o = a.CreateInstance("Templater.Template"); Type t = o.GetType(); MethodInfo mi = t.GetMethod("Eval"); object s = mi.Invoke(o, new object[] {model}); AppDomain.Unload(ad); return s; }
Finally, we can make a small modification to the template method, to enable code to be evaluated. I'm using the delimiters '<%' and '%>' to denote code which is evaluated in this way:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37public string Transform(string template, Dictionary<string, object> variables) { // for loops var fl = new Regex(@"#foreach\(\$(?<iterator>[^)]*)\sin\s\$(?<enumerable>[^)]*)\) (?<code>[\$\w\s\<\>\/.]*) #end"); template = fl.Replace(template, m => { var iterator = m.Groups["iterator"].Value; var enumerable = m.Groups["enumerable"].Value; var code = m.Groups["code"].Value; // get enumerable object IEnumerable enumer = GetDictionaryElement(variables, enumerable) as IEnumerable; if (enumer != null) { string loopedText = ""; foreach (var iter in enumer) { Dictionary<string, object> iterationVariables = new Dictionary<string, object>(); iterationVariables = GetProperties(iter, iterationVariables, iterator, true); loopedText += Transform(code, iterationVariables); } return loopedText; } else return iterator; // leave text unchanged }); // simple expansion of variables var r = new Regex(@"\$(?<name>([\w/_\.]*)\b)"); template = SimpleReplace(template, r, variables); // any code to evaluate ??? template = Regex.Replace(template,"<%.*%>", match=>(string)Eval(match.ToString(), model)); return template; }
Looks like this template engine could be useful. I'll try to build on this over the next few weeks...
David Barone, 5 August, 2009, 9:21 pm
Last Modified: 5 August, 2009, 9:21 pm

Comments:

Comments are closed for this article.