Replace parameter type in lambda expression
Asked Answered
D

4

13

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

Duff answered 11/7, 2016 at 21:28 Comment(0)
F
18

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);
    }
}
Firehouse answered 13/7, 2016 at 7:47 Comment(3)
I have tweeked the VisitMember override to use MakeMemberAccess: dotnetfiddle.net/HlZgPXDuff
@Eli Arbel thanks to your code I'm able to convert a Expression<Func<T, bool>> in Expression<Func<object, bool>> 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? ThanksKidder
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.Hobnob
C
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.

Cleavland answered 11/7, 2016 at 21:41 Comment(1)
Do you have an example for the scenario above?Duff
I
0

The Eli's answer is great.

But in my case, I have a lambda which have another lambda inside it. So, it sets the '_parameter' twice, overriding the old.

ex:

Expression<Func<IObjectWithCompanyUnits, bool>> expr = e => e.CompanyUnits.Any(cu => cu.ID == GetCurrentCompanyUnitId());

The Visitor breaks the expression above.

So I tweaked the original answer to my case:

public class ParameterTypeVisitor<TSource, TTarget> : ExpressionVisitor
{
    private Dictionary<int, ReadOnlyCollection<ParameterExpression>> _parameters = new();

    private int currentLambdaIndex = -1;

    protected override Expression VisitParameter(ParameterExpression node)
    {
        var prms = _parameters.Count > currentLambdaIndex ? _parameters[currentLambdaIndex] : null;

        var p = prms?.FirstOrDefault(p => p.Name == node.Name);
        if (p != null)
        {
            return p;
        }
        else
        {
            if (node.Type == typeof(TSource))
            {
                return Expression.Parameter(typeof(TTarget), node.Name);
            }
            else
            {
                return node;
            }
        }
    }

    protected override Expression VisitLambda<T>(Expression<T> node)
    {
        currentLambdaIndex++;
        try
        {
            _parameters[currentLambdaIndex] = VisitAndConvert<ParameterExpression>(node.Parameters, "VisitLambda");
            return Expression.Lambda(Visit(node.Body), _parameters[currentLambdaIndex]);
        }
        finally
        {
            currentLambdaIndex--;
        }
    }

    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);
    }
}
Irreplaceable answered 3/11, 2023 at 20:41 Comment(0)
S
0

Eli's answer is great, but will throw

System.InvalidCastException: Unable to cast object of type
'System.Linq.Expressions.Expression1`1[System.Func`2[...,<>f__AnonymousType0`1[...]]]'
to type
'System.Linq.Expressions.Expression`1[System.Func`2[...,System.Object]]'.

when the expression body is returning anonymous type like sharplab.io:

Expression<Func<C, object?>> expr = c => new {c.A};
Console.WriteLine((Expression<Func<C, object?>>)expr);
var visitor = new ReplaceParameterTypeVisitor<C, C>();
Console.WriteLine((Expression<Func<C, object?>>)visitor.Visit(expr));

public class C
{
    public int A { get; }
};

which is used widely in EF Core as selectors.

You may notice the first casting is working and marked as redundant since a delegate Func<in T, out TResult> that returning the top type: object is compatible with returning any types other aka convariant, but wrapping it into a class Expression<Func<in T, out TResult>> will losing the covariant of return type: Variance in Expression<Func<T,bool>>

So we will have to manually casting the generated anonymous type to object like sharplab.io:

    protected override Expression VisitLambda<T>(Expression<T> node)
    {
        _parameters = VisitAndConvert<ParameterExpression>(node.Parameters, "VisitLambda");
-        return Expression.Lambda(Visit(node.Body), _parameters);
+        return Expression.Lambda(Visit(node.Body.Type.IsAnonymous()
+            // https://mcmap.net/q/865461/-replace-parameter-type-in-lambda-expression/78560844#78560844
+            ? Expression.Convert(node.Body, typeof(object))
+            : node.Body), _parameters);
    }

with the extension method Type.IsAnonymous() that copied from How To Test if a Type is Anonymous?

public static class Extensions {
    public static bool IsAnonymous(this Type type)
    { // https://mcmap.net/q/267734/-how-to-test-if-a-type-is-anonymous-duplicate
        if (type == null)
            throw new ArgumentNullException("type");

        return Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false)
            && type.IsGenericType && type.Name.Contains("AnonymousType")
            && (type.Name.StartsWith("<>") || type.Name.StartsWith("VB$"))
            && type.Attributes.HasFlag(TypeAttributes.NotPublic);
    }
}

And if you are using LinqToDB they've already provided it: https://github.com/linq2db/linq2db/blob/0cb767639517d54023165780ddcdf2492268b794/Source/LinqToDB/Extensions/ReflectionExtensions.cs#L1045

Shawnee answered 31/5 at 15:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.