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
3 | private 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
12 | public 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
25 | private 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
43 | private 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
37 | public 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...
Comments: