views:

56

answers:

1

I know I can map two object types with LINQ using a projection as so:

var destModel = from m in sourceModel
               select new DestModelType {A = m.A, C = m.C, E = m.E}

where

class SourceModelType
{
    string A {get; set;}
    string B {get; set;}
    string C {get; set;}
    string D {get; set;}
    string E {get; set;}
}

class DestModelType
{
    string A {get; set;}
    string C {get; set;}
    string E {get; set;}
}

But what if I want to make something like a generic to do this, where I don't know specifically the two types I am dealing with. So it would walk the "Dest" type and match with the matching "Source" types.. is this possible? Also, to achieve deferred execution, I would want it just to return an IQueryable.

For example:

public IQueryable<TDest> ProjectionMap<TSource, TDest>(IQueryable<TSource> sourceModel)
{
   // dynamically build the LINQ projection based on the properties in TDest

   // return the IQueryable containing the constructed projection
}

I know this is challenging, but I hope not impossible, because it will save me a bunch of explicit mapping work between models and viewmodels.

+2  A: 

You have to generate an expression tree, but a simple one, so it's not so hard...

void Main()
{
    var source = new[]
    {
        new SourceModelType { A = "hello", B = "world", C = "foo", D = "bar", E = "Baz" },
        new SourceModelType { A = "The", B = "answer", C = "is", D = "42", E = "!" }
    };

    var dest = ProjectionMap<SourceModelType, DestModelType>(source.AsQueryable());
    dest.Dump();
}

public static IQueryable<TDest> ProjectionMap<TSource, TDest>(IQueryable<TSource> sourceModel)
    where TDest : new()
{
    var sourceProperties = typeof(TSource).GetProperties().Where(p => p.CanRead);
    var destProperties =   typeof(TDest).GetProperties().Where(p => p.CanWrite);
    var propertyMap = from d in destProperties
                      join s in sourceProperties on new { d.Name, d.PropertyType } equals new { s.Name, s.PropertyType }
                      select new { Source = s, Dest = d };
    var itemParam = Expression.Parameter(typeof(TSource), "item");
    var memberBindings = propertyMap.Select(p => (MemberBinding)Expression.Bind(p.Dest, Expression.Property(itemParam, p.Source)));
    var newExpression = Expression.New(typeof(TDest));
    var memberInitExpression = Expression.MemberInit(newExpression, memberBindings);
    var projection = Expression.Lambda<Func<TSource, TDest>>(memberInitExpression, itemParam);
    projection.Dump();
    return sourceModel.Select(projection);
}

(tested in LinqPad, hence the Dumps)

The generated projection expression looks like that :

item => new DestModelType() {A = item.A, C = item.C, E = item.E}
Thomas Levesque
Thank you for this solution. I am deep into understanding how it works. If I wanted to have it drill into complex objects, I would have to alter the propertyMap, correct?
CodeGrue
If you want to understand how the expression is constructed, I suggest you use LinqPad; it allows you to easily inspect each node of an expression. As for your question, I'm not sure I understand what you mean... if you only know the source and destination types, you can't really do anything more complex than copying properties with the same name.
Thomas Levesque
What if you wanted to incorporate complex objects so thatitem => new DestModelType() {A = item.A.X, C = item.C, E = item.E}. This could be through a property attribute designating what to map to.
CodeGrue
I think that it would be much harder, but still doable... you would have to change `propertyMap` and `memberBindings` according to your own logic
Thomas Levesque