Search code examples
linqmappingmap-projections

Using LINQ to map dynamically (or construct projections)


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.


Solution

  • 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}