14

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

5 Answers 5

17

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);
    }
}
Sign up to request clarification or add additional context in comments.

3 Comments

I have tweeked the VisitMember override to use MakeMemberAccess: dotnetfiddle.net/HlZgPX
@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? Thanks
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.
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.

1 Comment

Do you have an example for the scenario above?
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);
    }
}

Comments

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://stackoverflow.com/questions/38316519/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://stackoverflow.com/questions/2483023/how-to-test-if-a-type-is-anonymous
        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

Comments

0

Significantly improved Eli's answer which now supports:

  • Nested lambdas as it does not have mutable state and therefore sideeffects (mentioned by Cesar)

  • Correctly substitutes parameters by reference equality

  • Variance (mentioned by n0099)

Working dotnetfiddle with some syntax sugar

public Expression<Func<TTarget, TResult>> ReplaceParameterType<TSource, TTarget, TResult>(Expression<Func<TSource, TResult>> expression)
{
    var oldParameter = expression.Parameters[0];
    var newParameter = Expression.Parameter(typeof(TTarget), oldParameter.Name);
    var visitor = new ParameterTypeVisitor(oldParameter, newParameter);
    var lambdaExpression = (LambdaExpression)visitor.Visit(expression);

    if (lambdaExpression.ReturnType != typeof(TResult)
        &&
          (lambdaExpression.ReturnType.IsSubclassOf(typeof(TResult))
            || typeof(TResult).IsAssignableFrom(lambdaExpression.ReturnType)
          )
       )
    {
        var convertedBody = Expression.Convert(lambdaExpression.Body, typeof(TResult));
        return Expression.Lambda<Func<TTarget, TResult>>(convertedBody, newParameter);
    }

    return (Expression<Func<TTarget, TResult>>)lambdaExpression;
}
internal class ParameterTypeVisitor : ExpressionVisitor
{
    private readonly ParameterExpression _oldParameter;
    private readonly ParameterExpression _newParameter;

    public ParameterTypeVisitor(ParameterExpression oldParameter, ParameterExpression newParameter)
    {
        _oldParameter = oldParameter;
        _newParameter = newParameter;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        return node == _oldParameter
            ? _newParameter
            : base.VisitParameter(node);
    }

    protected override Expression VisitLambda<T>(Expression<T> node)
    {
        var parameters = node.Parameters.Select(VisitParameter).Cast<ParameterExpression>();
        return Expression.Lambda(Visit(node.Body), parameters);
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        if (node.Expression is ParameterExpression && node.Expression == _oldParameter)
        {
            return Expression.Property(Visit(node.Expression), node.Member.Name);
        }
        return base.VisitMember(node);
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.