Search code examples
c#linqexpressionlinq-to-entitieslinq-expressions

How add MemberInitExpression into Bindings other Lambda MemberInitExpression


I have the following classes:

class Source {
    public int Id { get; set; }
    public string Name { get; set; }
    public SourceItem Item { get; set; }
}

class SourceItem {
    public Guid Id { get; set; }
    public decimal Price { get; set; }
}

class Dest {
    public int Id { get; set; }
    public string Name { get; set; }
    public DestItem Item { get; set; }
}

class DestItem{
    public Guid Id { get; set; }
    public decimal Price { get; set; }
}

And I built the following lambda expressions:

Expression<Func<Source, Dest>> projectionSource = source => new Dest{
    Id = source.Id,
    Name = source.Name
};

Expression<Func<SourceItem, DestItem>> projectionItem = item => new DestItem
{
    Id = item.Id,
    Price = item.Price
};

Expression<Func<Source, SourceItem>> sourceMember = source => source.Item;
Expression<Func<Dest, DestItem>> destMember = dest => dest.Item;

How do I add new element bindings to the list of element bindings of the original projection expression using the memberInitExpression expression from the projectionItem lambda expression? Lambda is needed at the output:

source => new Dest() // part of projectionSource expression
{
   Id = source.Id,
   Name = source.Name,
   Item = new DestItem() // part of projectionItem expression
   {
      Id = source.Item.Id,
      Price = source.Item.Price
   }
}

I couldn't create a MemberAccess from the lambda destMember and projectionItem and add it to the Bindings List projectionSource.


Solution

  • You don't add a member to the bindings list, you build a new expression.

    With following helper to replace parameters (if you have EF Core 3+ referenced you can use it's ReplacingExpressionVisitor instead):

    public static class ExpressionExt
    {
        public static Expression ReplaceParameter(this Expression expression, ParameterExpression source, Expression target)
        {
            return new ParameterReplacingVisitor { Source = source, Target = target }.Visit(expression);
        }
    
        class ParameterReplacingVisitor : ExpressionVisitor
        {
            public ParameterExpression Source;
            public Expression Target;
            protected override Expression VisitParameter(ParameterExpression node)
            {
                return node == Source ? Target : base.VisitParameter(node);
            }
        }
    }
    

    You can do something like the following:

    // get the body
    var projectionSourceBody = projectionSource.Body as MemberInitExpression;
    
    // get the "nested" Item members to map:
    var sourceMemberE = sourceMember.Body as MemberExpression;
    var destMemberE = destMember.Body as MemberExpression;
    
    // replace the "nested" projection expression parameter with source.Item
    var projectItemE = projectionItem.Body.ReplaceParameter(
        projectionItem.Parameters[0],
        Expression.MakeMemberAccess(projectionSource.Parameters[0], sourceMemberE.Member));
    
    // generate the Item = new DestItem {...} member binding for init expression
    var assignItem = Expression.Bind(destMemberE.Member, projectItemE);
    
    // rebuild member init expression with new binding list
    // using C# 12 collection expressions here
    // can use projectionSourceBody.Bindings.Append(assignItem) instead
    var newInitE = Expression.MemberInit(
      projectionSourceBody.NewExpression, 
      [.. projectionSourceBody.Bindings, assignItem]); 
    
    // create new lambda
    var newProjectionSource = Expression.Lambda<Func<Source, Dest>>(newInitE, projectionSource.Parameters);
    newProjectionSource.Compile(); // compile to at least verify we did something compilable
    

    Note that possibly it is worth investigating existing mapper libraries which support such stuff out of the box so you don't need to reinvent the wheel. For example popular AutoMapper "understands" "nested" mappings and supports building expressions to be consumed by LINQ providers - see the Queryable Extensions doc (search for ProjectTo and CreateProjection samples).