I needed to create a simple dynamic parser to create a class based on an array of objects into a class. This would be used to read a data from a DatReader. My first thought was to use reflection but having a little bit of spare time, I decided to make a sample of doing this using CodeDOM.
My data class:
namespace CodeDomTest
{
public class DataClass
{
private int _FirstInt;
private long _SecondInt;
private string _FirstString;
private string _SecondString;
public int FirstInt
{
get { return _FirstInt; } set { _FirstInt = value; }
}
public long SecondInt
{
get { return _SecondInt; } set { _SecondInt = value; }
}
public string FirstString
{
get { return _FirstString; } set { _FirstString = value; }
}
public string SecondString
{
get { return _SecondString; } set { _SecondString = value; }
}
}
}
And the Interface that the parser would have to implement:
namespace CodeDomTest
{
public interface IParser
{
DataClass Parse(object[] s);
}
}
I started working backwards, and wrote a simple parser myself:
class StringParser : IParser
{
public DataClass Parse(object[] s)
{
DataClass _data = new DataClass();
_data.FirstInt = System.Convert.ToInt32(s[0]);
_data.FirstString = System.Convert.ToString(s[1]);
_data.SecondInt = System.Convert.ToInt32(s[2]);
_data.SecondString = System.Convert.ToString(s[3]);
return _data;
}
}
Now, all I needed is for the CodeDom to generate the above code, and I am done. Easy...
using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using Microsoft.CSharp;
namespace CodeDomTest
{
class DataParserGenerator
{
private static Dictionary<Type, IParser> ParserCache = new Dictionary<Type, IParser>();
private static object SyncLock = new object();
public static IParser GetCachedParser(Type ReturnType, string[] ParseMap)
{
lock (SyncLock)
{
if (ParserCache.ContainsKey(ReturnType))
return ParserCache[ReturnType];
}
IParser obj = GetParser(ReturnType, ParseMap);
lock (SyncLock)
{
if (ParserCache.ContainsKey(ReturnType))
return ParserCache[ReturnType];
ParserCache.Add(ReturnType, obj);
return obj;
}
}
public static IParser GetParser(Type ReturnType, string[] ParseMap)
{
CodeCompileUnit targetUnit;
CodeTypeDeclaration targetClass;
targetUnit = new CodeCompileUnit();
CodeNamespace samples = new CodeNamespace(typeof(DataParserGenerator).Namespace.ToString());
samples.Imports.Add(new CodeNamespaceImport("System"));
targetClass = new CodeTypeDeclaration("Parser_Generated_" + ReturnType.Name);
targetClass.IsClass = true;
targetClass.TypeAttributes =
TypeAttributes.Public | TypeAttributes.Sealed;
targetClass.BaseTypes.Add(new CodeTypeReference(typeof (IParser)));
samples.Types.Add(targetClass);
targetUnit.Namespaces.Add(samples);
AddParseMethod(targetClass, ReturnType, ParseMap);
IParser obj = (IParser) ReturnCompiledClass(targetUnit, samples.Name + "." + targetClass.Name);
return obj;
}
private static void AddParseMethod(CodeTypeDeclaration targetClass, Type ReturnType, string[] ParseMap)
{
CodeMemberMethod method = new CodeMemberMethod();
method.Name = "Parse";
method.Attributes = MemberAttributes.Public | MemberAttributes.Final;
method.Parameters.Add(new CodeParameterDeclarationExpression(typeof(object[]), "param"));
method.ReturnType = new CodeTypeReference(ReturnType);
method.Statements.Add(
new CodeVariableDeclarationStatement(ReturnType, "_data",
new CodeObjectCreateExpression(ReturnType)));
for (int i = 0; i < ParseMap.Length; i++)
{
PropertyInfo prop;
//Only support properties for now.
prop = ReturnType.GetProperty(ParseMap[i]);
if (prop == null)
continue;
CodeArrayIndexerExpression mapIndexer = new CodeArrayIndexerExpression(
new CodeVariableReferenceExpression("param"),
new CodePrimitiveExpression(i));
//If System.Convert has a conversion method, use it. Otherwise, just cast
if (typeof(System.Convert).GetMethod("To" + prop.PropertyType.Name, new Type[] { typeof(object) }) != null)
{
CodeTypeReferenceExpression ctr = new CodeTypeReferenceExpression(typeof(System.Convert));
CodeMethodInvokeExpression cme =
new CodeMethodInvokeExpression();
cme.Method = new CodeMethodReferenceExpression(ctr, "To" + ReturnType.GetProperty(ParseMap[i]).PropertyType.Name);
cme.Parameters.Add(mapIndexer);
method.Statements.Add(new CodeAssignStatement(
new CodeVariableReferenceExpression("_data." + ParseMap[i]),
cme
));
}
else
{
method.Statements.Add(new CodeAssignStatement(
new CodeVariableReferenceExpression("_data." + ParseMap[i]),
new CodeCastExpression(prop.PropertyType,
mapIndexer)));
}
}
method.Statements.Add(new CodeMethodReturnStatement(new CodeArgumentReferenceExpression("_data")));
targetClass.Members.Add(method);
}
private static object ReturnCompiledClass(CodeCompileUnit targetUnit, string targetClassName)
{
CSharpCodeProvider provider = new CSharpCodeProvider();
// Build the parameters for source compilation.
CompilerParameters cp = new CompilerParameters();
// Generate a class library.
cp.GenerateExecutable = false;
// Do not save the assembly as a physical file.
cp.GenerateInMemory = true;
// Need to referenc current assembly because it containts definitions of IParse and DataClass
cp.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().Location);
#if DEBUG
StringBuilder sb = new StringBuilder();
StringWriter sw = new StringWriter(sb);
provider.GenerateCodeFromCompileUnit(targetUnit, sw, null);
sw.Close();
System.Diagnostics.Debug.WriteLine(sb.ToString());
#endif
CompilerResults cr = provider.CompileAssemblyFromDom(cp, targetUnit);
if (cr.Errors.Count == 0)
{
return cr.CompiledAssembly.CreateInstance(targetClassName);
}
else
return null;
}
}
}
Here's a sample way of calling the parser:
IParser dcParser;
string[] s ={"1", "2", "3", "4"};
string[] ParseMap = new string[] { "FirstInt", "FirstString", "SecondInt", "SecondString" };
dcParser= DataParserGenerator.GetParser(typeof (DataClass), ParseMap);
DataClass dc = dcParser.Parse(s);
And Here's the generated code (from the debug output window)
namespace CodeDomTest {
using System;
public sealed class Parser_Generated_DataClass : CodeDomTest.IParser {
public CodeDomTest.DataClass Parse(object[] param) {
CodeDomTest.DataClass _data = new CodeDomTest.DataClass();
_data.FirstInt = System.Convert.ToInt32(param[0]);
_data.FirstString = System.Convert.ToString(param[1]);
_data.SecondInt = System.Convert.ToInt64(param[2]);
_data.SecondString = System.Convert.ToString(param[3]);
return _data;
}
}
}
That's all... I told you it's easy.
Using CodeDOM to generate parsing classes