Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to re-wrap a Linq Expression Tree

I have an Expression<Func<Entity, string>> that can be either a property or a nested property accessor

y => y.SearchColumn

or

y => y.SubItem.SubColumn

I am building up an expression tree dynamically and would like to get an InvokeExpression like this

x => x.SearchColumn.Contains("foo");
x => x.Sub.SearchColumn.Contains("foo");

I'm pretty sure I can unwrap the Expression body, then partially apply the x ParameterExpression to it but I can't figure out the magical incantations to make this happen

So I have something like

Expression<Func<Entity, string>> createContains(Expression<Func<Entity, string>> accessor) {
    var stringContains = typeof(String).GetMethod("Contains", new [] { typeof(String) });
    var pe = Expression.Parameter(typeof(T), "__x4326");
    return Expression.Lambda<Func<Entity, bool>>(
        Expression.Call(
            curryExpression(accessor.Body, pe),
            stringContains,
            Expression.Constant("foo")
        )
        , pe
    );              
}

    static Expression curryExpression(Expression from, ParameterExpression parameter) {
        // this doesn't handle the sub-property scenario
        return Expression.Property(parameter, ((MemberExpression) from).Member.Name);
        //I thought this would work but it does not
        //return Expression.Lambda<Func<Entity,string>>(from, parameter).Body;
    }

Edit: Here is the full thing I'm trying to do along with the error

like image 987
George Mauer Avatar asked Aug 31 '15 20:08

George Mauer


1 Answers

You need to do two things - first one you can use the same accessor.Body, but it will reference to incorrect Parameter, as you created a new one. Second one you need to write custom ExpressionVisitor that will replace all usage of original y ParameterExpression to a new created, so result expression will be compiled fine.

If you don't need to create new ParameterExpression, then you can just use the same accessor.Body and resent original ParameterExpression to a new tree.

So here is my test working copy with a new ParameterExpression - https://dotnetfiddle.net/uuPVAl

And the code itself

public class Program
{
    public static void Main(string[] args)
    {
        Expression<Func<Entity, string>> parent = (y) => y.SearchColumn;
        Expression<Func<Entity, string>> sub = (y) => y.Sub.SearchColumn;

        var result = Wrap(parent);
        Console.WriteLine(result);
        result.Compile();

        result = Wrap(sub);
        Console.WriteLine(result);
        result.Compile();

        result = Wrap<Entity>((y) => y.Sub.Sub.Sub.SearchColumn);
        Console.WriteLine(result);
        result.Compile();

    }

    private static Expression<Func<T, bool>> Wrap<T>(Expression<Func<T, string>> accessor)
    {
        var stringContains = typeof (String).GetMethod("Contains", new[] {typeof (String)});
        var pe = Expression.Parameter(typeof (T), "__x4326");
        var newBody = new ParameterReplacer(pe).Visit(accessor.Body);
        var call = Expression.Call(
            newBody,
            stringContains,
            Expression.Constant("foo")
            );
        return Expression.Lambda<Func<T, bool>>(call, pe);
    }
}

public class ParameterReplacer : ExpressionVisitor
{
    private ParameterExpression _target;

    public ParameterReplacer(ParameterExpression target)
    {
        _target = target;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        // here we are replacing original to a new one
        return _target;
    }
}

public class Entity
{
    public string SearchColumn { get; set; }

    public Entity Sub { get; set; }
}

PS: this example will work only if you have only one ParameterExpression in original query, otherwise visitor should differentiate them

UPDATE

Here is my working answer with your full example in update - https://dotnetfiddle.net/MXP7wE

like image 52
Sergey Litvinov Avatar answered Sep 28 '22 01:09

Sergey Litvinov