I'm writing a specific kind of object-mapper. Basically I want to convert from a DataTable
that has the fields a
, b
and c
to an object that has properties a
, b
and c
(the class of the object will be written by hand). There will be many different DataTables and many different classes that they will need to map to, so I want to make a generic mechanism that performs this data copying. Basically, I want the following function:
public T Map<T>(DataTable t) where T: new() { ... }
Now, I can do this with Reflection, but that's slow. This function will be at the core of the framework and will be used often. So I'm thinking about dynamic code generation. The first time this method is run on a specific T
it will perform the necessary reflection and emit an anonymous method that does all the appropriate mapping. The next time it will just run that code. This should be as performant as possible.
Just one problem - I've never emitted code at runtime. How do I do that? I looked at Expressions
, but they can only do, well, expressions, not a series of statements.
Then there's CodeDOM and CSharpCodeProvider
. That sort of works - I can generate C# code as a string, compile it on-the-fly and then get a reference. However it involves the C# compiler and generates a whole new in-memory assembly. Sounds a bit... heavyweight for one simple method.
Is there some simpler way? Something that generates a lightweight, anonymous method not attached to any assembly (or attached to an existing assembly)?
OK, since people asked for an example.
Here's a class, written by hand
class MyBusinessObject
{
public int a;
public string b { get; set; }
}
Here's a DataTable, prepared by hand (in real life, this will come from an external library):
DataTable t = new DataTable();
t.AddColumn("a", typeof(int));
t.AddColumn("b", typeof(string));
t.AddRow(42, "Meaning");
Here's the method that should be generated on the fly:
(DataRow drow, MyBusinessObject o) =>
{
o.a = (int)drow["a"];
o.b = (string)drow["b"];
}
I've omitted some other stuff I need for brevity, but that's the meat of the issue.
In C# 4.0, a new type is introduced that is known as a dynamic type. It is used to avoid the compile-time type checking. The compiler does not check the type of the dynamic type variable at compile time, instead of this, the compiler gets the type at the run time.
Specifically, dynamic code generation, a technique widely used in just-in-time (JIT) compilation and dynamic binary translation (DBT), generates and modifies code on the fly in order to promote performance or security.
The easiest way to generate code dynamically in .NET 3.5+ is by converting LINQ Expression Trees to executable code through the Compile
method of the LambdaExpression
class. .NET 4.0 has greatly expanded the possibilities, adding support for code structures beyond simple expressions of .NET 3.5, letting you construct fully functional methods. The resultant code gives you the same high performance as regularly compiled code would, assuming that your expression generator has applied the same kinds of optimizations a C# compiler would while generating the code.
Here is how you could generate the code from your snippet:
// nameToProperty is a dictionary with keys representing string parameters
// that you pass to drow's indexer, and values representing names of properties
// or fields of the target object.
private static Action<DataRow,T> MakeGetter<T>(IDictionary<string,string> nameToProperty) {
var sequence = new List<Expression>();
var drowParam = Expression.Parameter(typeof(DataRow));
var oParam = Expression.Parameter(typeof(T));
var indexer = typeof(DataRow)
.GetDefaultMembers()
.OfType<PropertyInfo>()
.Where(pinf => pinf.GetIndexParameters().Length == 1
&& pinf.GetIndexParameters()[0].ParameterType == typeof(string))
.Single();
foreach (var pair in nameToProperty) {
var indexExpr = Expression.Property(
drowParam
, indexer
, Expression.Constant(pair.Key));
sequence.Add(Expression.Assign(
Expression.PropertyOrField(pair.Value)
, indexExpr
));
}
return (Action<DataRow,T>)Expression.Lambda(
Expression.Block(sequence)
, drowParam
, oParam
).Compile();
}
With this method in place, you should be able to generate compiled Action
s that would do the assignments as needed.
I wouldn't be so quick to dismiss expressions. You can use expressions in one of several different ways to accomplish your goal.
If you are using .NET 4+, you the expression trees have been extended to support blocks of code. While you can't use this functionality with the lambda syntactic sugar, you can use the Expression.Block
method to create a code block.
Use a constructor that has a parameter for each field you are mapping. The generated code could mimic return new T(ExtractA(t), ExtractB(t), ...)
. In this case you would remove the where T : new()
constraint from Map<T>
and instead rely on your object model classes having a constructor that can be found using reflection with a parameter for each mapped property.
Use helper methods to execute a series of statements as if it were a single statement. The generated code could mimic return ApplyProperties(t, new T(), new[] { applyA, applyB, ... })
, where applyA
and applyB
are Action<DataTable, T>
delegates that were separately compiled from expressions designed to set a single specific property. The ApplyProperties
method is a helper method in your code like the following:
private T ApplyProperties<T>(DataTable t, T result, Action<DataTable, T>[] setters)
{
foreach (var action in setters)
{
action(t, result);
}
return result;
}
Sometimes a 3rd party library is the easiest way to do it, AutoMapper will do exactly what you want with just a few lines of code
//This just needs to be run once, maybe in a static constructor somewhere.
Mapper.CreateMap<IDataReader, MyBusinessObject>();
//This line does your mapping.
List<MyBusinessObject> myBusinessObject =
Mapper.Map<IDataReader, List<MyBusinessObject>>(myDataTable.CreateDataReader());
If your source data does not exactly match your business object all you need to do is add some setup info to CreateMap
.
class MyBusinessObject
{
public int Answer;
public string Question { get; set; }
}
//In some static constructor somewhere, this maps "a" to "Answer" and "b" to "Question".
Mapper.CreateMap<IDataReader, MyBusinessObject>()
.ForMember(dto => dto.Answer, opt => opt.MapFrom(rdr => rdr["a"]))
.ForMember(dto => dto.Question, opt => opt.MapFrom(rdr => rdr["b"]));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With