Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Combining two Linq Expressions

Tags:

c#

linq

Using info from various SO posts and notably this blog (corrected to use AndAlso rather than And) I've managed to combine similarly typed linq expressions into a single predicate. But now I want to combine two Expressions where one is an input to the other. Here's the fully expanded original Expression;

    private Expression<Func<T, bool>> ExpressionIsNamed(IEnumerable<EntityName> AccessorNames)
    {
        // works
        Expression<Func<T, bool>> Texpr = x => x.Security.Readers.Any(n => AccessorNames.ToStringArray().Contains(n.Text));

        return Texpr;
    }

Note that crucially, I need to manage these as Expressions because my DB driver needs to walk the tree and convert into a native call so using Compile() to combine is not an option.

So below is the function I want to combine with the Any() call above. The final output Expression needs to be of type Expression<Func<T, bool>> and I need to pass x.Security.Readers into this one.

    public static Expression<Func<IEnumerable<EntityName>,bool>> AccessCheckExpression(IEnumerable<EntityName> AccessorNames)
    {
        return accessList => accessList.Any(n => AccessorNames.ToStringArray().Contains(n.Text));
    }

I've got as far as this, but I'm struggling to work out how to swap out accessList => from accessCheck and have it use accessList in a single Expression. So far, I have this;

    private Expression<Func<T, bool>> ExpressionIsNamed(IEnumerable<EntityName> AccessorNames)
    {
        Expression<Func<T, IEnumerable<EntityName>>> accessList = (T x) => x.Security.Readers;
        Expression<Func<IEnumerable<EntityName>, bool>> accessCheck = SecurityDescriptor.AccessCheckExpression(AccessorNames);

        // Combine?
        Expression<Func<T, bool>> Texpr = ??? accessCheck + accessList ???

        return Texpr;
    }

[Update]

So I've got a little further;

    class ParameterUpdateVisitor : System.Linq.Expressions.ExpressionVisitor
    {
        private ParameterExpression _oldParameter;
        private ParameterExpression _newParameter;

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

        protected override Expression VisitParameter(ParameterExpression node)
        {
            if (object.ReferenceEquals(node, _oldParameter))
                return _newParameter;

            return base.VisitParameter(node);
        }
    }

    static Expression<Func<T, bool>> UpdateParameter<T>(
        Expression<Func<T, IEnumerable<EntityName>>> expr,
        ParameterExpression newParameter)
    {
        var visitor = new ParameterUpdateVisitor(expr.Parameters[0], newParameter);
        var body = visitor.Visit(expr.Body);

        return Expression.Lambda<Func<T, bool>>(body, newParameter);
    }

I can then compile with;

UpdateParameter(accessList, accessCheck.Parameters[0]);

Thanks to all for those Invoke() suggestions, but my guess is that by the time they get to the MongoDB driver it won't like InvocationExpression. However, both Invoke and my code above now fail in exactly the same way. Namely;

System.ArgumentException: Expression of type 
'System.Func`2[MyLib.Project,System.Collections.Generic.IEnumerable`1[MyLib.EntityName]]' 
cannot be used for parameter of type 
                            'System.Collections.Generic.IEnumerable`1[MyLib.EntityName]'

So it would appear that the implicit parameter x.Security.Readers is a different type to a plain old IEnumerable<EntityName>

like image 671
cirrus Avatar asked Nov 22 '12 12:11

cirrus


People also ask

How do you combine two expressions?

to combine two expressions or more, put every expression in brackets, and use: *?

What is Expression C#?

An expression in C# is a combination of operands (variables, literals, method calls) and operators that can be evaluated to a single value. To be precise, an expression must have at least one operand but may not have any operator.


1 Answers

VisitorExpression is your friend here. Here's a simplified but complete example of merging something similar:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

class Source {
    public List<Value> Values {get;set;}
}
class Value {
    public int FinalValue {get;set;}
}
static class Program {
    static void Main() {
        Expression<Func<Source, IEnumerable<Value>>> f1 =
            source => source.Values;
        Expression<Func<IEnumerable<Value>, bool>> f2 =
            vals => vals.Any(v => v.FinalValue == 3);

        // change the p0 from f2 => f1
        var body = SwapVisitor.Swap(f2.Body, f2.Parameters[0], f1.Body);
        var lambda = Expression.Lambda<Func<Source, bool>>(body,f1.Parameters);
        // which is:
        // source => source.Values.Any(v => (v.FinalValue == 3))
    }

}
class SwapVisitor : ExpressionVisitor {
    private readonly Expression from, to;
    private SwapVisitor(Expression from, Expression to) {
        this.from = from;
        this.to = to;
    }
    public static Expression Swap(Expression body,
        Expression from, Expression to)
    {
        return new SwapVisitor(from, to).Visit(body);
    }
    public override Expression Visit(Expression node)
    {
         return node == from ? to : base.Visit(node);
    }
}

Edit: in your case, this would be:

private Expression<Func<T, bool>> ExpressionIsNamed(
    IEnumerable<EntityName> AccessorNames)
{
    Expression<Func<T, IEnumerable<EntityName>>> accessList =
        (T x) => x.Security.Readers;
    Expression<Func<IEnumerable<EntityName>, bool>> accessCheck =
        SecurityDescriptor.AccessCheckExpression(AccessorNames);


    var body = SwapVisitor.Swap(accessCheck.Body,
        accessCheck.Parameters[0], accessList.Body);
    return Expression.Lambda<Func<T, bool>>(body, accessList.Parameters);
}
like image 184
Marc Gravell Avatar answered Sep 30 '22 08:09

Marc Gravell