11

I am trying to replace the parameter type in a lambda expression from one type to another.

I have found other answers on stackoverflow i.e. this one but I have had no luck with them.

Imagine for a second you have a domain object and a repository from which you can retrieve the domain object.

however the repository has to deal with its own Data transfer objects and then map and return domain objects:

ColourDto.cs

public class DtoColour {

    public DtoColour(string name)
    {
        Name = name;
    }

    public string Name { get; set; }
}

DomainColour.cs

public class DomainColour {

    public DomainColour(string name)
    {
        Name = name;
    }

    public string Name { get; set; }
}

Repository.cs

public class ColourRepository {
    ...
    public IEnumerable<DomainColour> GetWhere(Expression<Func<DomainColour, bool>> predicate)
    {
        // Context.Colours is of type ColourDto
        return Context.Colours.Where(predicate).Map().ToList();
    }
}

As you can see this will not work as the predicate is for the domain model and the Collection inside the repository is a collection of Data transfer objects.

I have tried to use an ExpressionVisitor to do this but cannot figure out how to just change the type of the ParameterExpression without an exception being thrown for example:

Test scenario

public class ColourRepository {
    ...
    public IEnumerable<DomainColour> GetWhere(Expression<Func<DomainColour, bool>> predicate)
    {
        var visitor = new MyExpressionVisitor();
        var newPredicate = visitor.Visit(predicate) as Expression<Func<ColourDto, bool>>;
        return Context.Colours.Where(newPredicate.Complie()).Map().ToList();
    }
}


public class MyExpressionVisitor : ExpressionVisitor
{
    protected override Expression VisitParameter(ParameterExpression node)
    {
        return Expression.Parameter(typeof(ColourDto), node.Name);
    }
}

finally here is the exception:

System.ArgumentException : Property 'System.String Name' is not defined for type 'ColourDto'

Hope someone can help.

EDIT: Here is a dotnetfiddle

still doesnt work.

Edit: Here is a working dotnetfiddle

Thanks Eli Arbel

Community
  • 1
  • 1
Jake Aitchison
  • 1,079
  • 6
  • 20

2 Answers2

15

You need to do a few things for this to work:

  • Replace parameter instance both at the Expression.Lambda and anywhere they appear in the body - and use the same instance for both.
  • Change the lambda's delegate type.
  • Replace the property expressions.

Here's the code, with added generics:

public static Func<TTarget, bool> Convert<TSource, TTarget>(Expression<Func<TSource, bool>> root)
{
    var visitor = new ParameterTypeVisitor<TSource, TTarget>();
    var expression = (Expression<Func<TTarget, bool>>)visitor.Visit(root);
    return expression.Compile();
}

public class ParameterTypeVisitor<TSource, TTarget> : ExpressionVisitor
{
    private ReadOnlyCollection<ParameterExpression> _parameters;

    protected override Expression VisitParameter(ParameterExpression node)
    {
        return _parameters?.FirstOrDefault(p => p.Name == node.Name) ?? 
            (node.Type == typeof(TSource) ? Expression.Parameter(typeof(TTarget), node.Name) : node);
    }

    protected override Expression VisitLambda<T>(Expression<T> node)
    {
        _parameters = VisitAndConvert<ParameterExpression>(node.Parameters, "VisitLambda");
        return Expression.Lambda(Visit(node.Body), _parameters);
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        if (node.Member.DeclaringType == typeof(TSource))
        {
            return Expression.Property(Visit(node.Expression), node.Member.Name);
        }
        return base.VisitMember(node);
    }
}
Eli Arbel
  • 22,391
  • 3
  • 45
  • 71
  • 2
    I have tweeked the VisitMember override to use MakeMemberAccess: https://dotnetfiddle.net/HlZgPX – Jake Aitchison Jul 13 '16 at 08:52
  • @Eli Arbel thanks to your code I'm able to convert a Expression> in Expression> but if my source expression is "x => x.MyProp == 2" I get an runtime error "ArgumentException: Instance property 'MyProp' is not defined for type 'System.Object' (Parameter 'propertyName')"; how can I add a cast during conversion? Thanks – gidanmx2 Oct 07 '20 at 07:11
  • Ok, maybe I'm dumb but I thought that one could use different instances of a `ParameterExpression` as long as it had the same name and type, and I had problems with it. So it is very important to use **the same** instance of the `ParameterExpression` we want to replace everywhere in the visitor, thanks for your answer. – Xriuk Oct 27 '22 at 13:23
1

Properties are defined separately for each type.

That error happens because you can't get the value of a property defined by DomainColour from a value of type ColourDto.

You need to visit every MemberExpression that uses the parameter and return a new MemberExpression that uses that property from the new type.

SLaks
  • 868,454
  • 176
  • 1,908
  • 1,964