Expression Trees: Part 4 – Simple mapping inversion
Solution ·This is part of a series of blog post about expression trees in .NET 4.0. The past blog entries are:
In this article, I’ll cover simple mapping inversion using Expression Trees (ET). By simple I mean that we’ll map two objects properties / fields directly: there won’t be any complex properties.
Let’s say we have the following two classes:
public class Individual
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DateOfBirth { get; set; }
public decimal Salary { get; set; }
}public class PersonTable
{
public string FIRST_NAME { get; set; }
public string LAST_NAME { get; set; }
public DateTime DATE_OF_BIRTH { get; set; }
public decimal SALARY { get; set; }
}
We can easily map one class to the other:
Expression<Func<PersonTable, Individual>> map = p => new Individual
{
FirstName = p.FIRST_NAME,
LastName = p.LAST_NAME,
DateOfBirth = p.DATE_OF_BIRTH,
Salary = p.SALARY
};
This expression maps a PersonTable object to an Individual object. What we would like to do is revert this mapping in order to get a mapping from Individual object to an PersonTable object.
The first thing we would like to do is to extract the different field / property maps:
private static IEnumerable<Tuple<MemberInfo, MemberInfo>>
ExtractMemberMaps<S, D>(Expression<Func<S, D>> map)
{ // Validate the map isn't a null reference
if (map == null)
{
throw new ArgumentNullException("map");
}var initExp = map.Body as MemberInitExpression;
// Validate the body of the expression constist in a
// member initialization with a default (no arguments) constructor
if (initExp == null || (initExp.NewExpression.Arguments.Count != 0))
{
throw new ApplicationException("Not a member init expression with empty constructor");
}var mappingPairs =
from binding in initExp.Bindings.OfType<MemberAssignment>()
let memberAssignment = binding.Expression as MemberExpression
// Make sure we have an assignment
where memberAssignment != null
// Make sure we assign to a property or a field
&& (memberAssignment.Member.MemberType == MemberTypes.Property
|| memberAssignment.Member.MemberType == MemberTypes.Field)
select new Tuple<MemberInfo, MemberInfo>(memberAssignment.Member, binding.Member);// Materialize the enumeration so that it doesn't get recomputed many times
return mappingPairs.ToList();
}
We can then check for each property in the source object where it maps to:
Expression<Func<PersonTable, Individual>> map = p => new Individual
{
FirstName = p.FIRST_NAME,
LastName = p.LAST_NAME,
DateOfBirth = p.DATE_OF_BIRTH,
Salary = p.SALARY
};var sourceToDestinationMaps = ExtractMemberMaps(map);
Console.WriteLine(MapMember<PersonTable, string>(sourceToDestinationMaps, p => p.FIRST_NAME));
Console.WriteLine(MapMember<PersonTable, string>(sourceToDestinationMaps, p => p.LAST_NAME));
Console.WriteLine(MapMember<PersonTable, DateTime>(sourceToDestinationMaps, p => p.DATE_OF_BIRTH));
Console.WriteLine(MapMember<PersonTable, decimal>(sourceToDestinationMaps, p => p.SALARY));
This outputs:
- System.String FirstName
- System.String LastName
- System.DateTime DateOfBirth
- System.Decimal Salary
So we do have individual member element maps. We now have to revert those maps and validate there are no duplicates:
private static IEnumerable<Tuple<MemberInfo, MemberInfo>>
InvertMaps(IEnumerable<Tuple<MemberInfo, MemberInfo>> maps)
{ // Inverts
var invertedMaps =
(from map in maps
select new Tuple<MemberInfo, MemberInfo>(map.Item2, map.Item1)).ToList();// Validate uniqueness of mapping
var count = (from map in invertedMaps
select map.Item1).Distinct().Count();if (invertedMaps.Count != count)
{
throw new ApplicationException(string.Format(
"There are {0} duplicate of destinations",
invertedMaps.Count - count));
}return invertedMaps;
}
The only way we could fail the validation is if we had some destination members mapping to the same source member, for instance:
Expression<Func<PersonTable, Individual>> map = p => new Individual
{
FirstName = p.FIRST_NAME,
LastName = p.FIRST_NAME
};
Of course that type of mapping would be invalid for inversion since we wouldn’t be able to determine how to map from p.FIRST_NAME.
We can now revert an entire mapping by simply constructing the inverse map:
private static Expression<Func<D, S>> InvertMapping<S, D>(Expression<Func<S, D>> map)
{ // Fetch the maps of each field / property
var sourceToDestMaps = ExtractMemberMaps(map);
// Invert the maps
var destToSourceMaps = InvertMaps(sourceToDestMaps);
// Creates a variable for the destination type
var destVar = Expression.Variable(typeof(D), "d");
// New the source type, i.e. new S()
var newSource = Expression.New(typeof(S));
// List the bindings i.e. p1 = m.P1
var bindings = from m in destToSourceMaps
let destinationMember = m.Item1
let sourceMember = m.Item2
let assignation = Expression.MakeMemberAccess(destVar, destinationMember)
select Expression.Bind(sourceMember, assignation);
// Init expression, i.e. new S{p1=m.P1,p2=m.P2}
var initSource = Expression.MemberInit(newSource, bindings);
// Creates the lambda expression, i.e. m => new S{p1=m.P1,p2=m.P2}
var initLambda = Expression.Lambda<Func<D, S>>(initSource, destVar);return initLambda;
}
Putting it all together:
Expression<Func<PersonTable, Individual>> map = p => new Individual
{
FirstName = p.FIRST_NAME,
LastName = p.LAST_NAME,
DateOfBirth = p.DATE_OF_BIRTH,
Salary = p.SALARY
};
var invertMap = InvertMapping(map);
var mapDel = map.Compile();
var invertMapDel = invertMap.Compile();
var person = new PersonTable
{
FIRST_NAME = "John",
LAST_NAME = "Doe",
DATE_OF_BIRTH = DateTime.Now.Subtract(TimeSpan.FromDays(100000)),
SALARY = 25000
};
// Map from PersonTable to Individual
var mapped = mapDel(person);
// Map from Individual back to PersonTable
var unmapped = invertMapDel(mapped);Console.WriteLine(invertMap);
Here we successfully mapped the object from PersonTable to Individual back to PersonTable representation.
The outputted invert map is:
d => new PersonTable()
{
FIRST_NAME = d.FirstName,
LAST_NAME = d.LastName,
DATE_OF_BIRTH = d.DateOfBirth,
SALARY = d.Salary
}
We therefore have something pretty powerful here: we enter a mapping expression and get the functioning reverse mapping. This gets quite some of the declarative strength of expression trees.
2 responses