Search code examples
c#linq-to-sqlexpression

Pass an Expression into another Expression as parameter


My problem derived from complex reusable logical specifications.

I have the following Expression:

Expression<Func<User, bool>> userExp =
            user => user.UserInRoles.Any(userInRole => userInRole.RoleId == SystemRoles.SysAdmin.Id);

And I need to get the ability of:

new CommonContext().Set<Estate>().Where(estate => userExp.WithParameter(estate.CreatorUser)).ToList();

So how can I pass the Creator of Estate entity into the expression which accepts a User entity and finally use the final expression in linq to sql ?

The problem is : WithParameter

EDIT:

This one works but its not efficient:

new CommonContext().Set<Estate>().ToList().Where(estate => userExp.Compile()(estate.CreatorUser)).ToList()

And the following is not the answer because the Invoke method can not be translated to store expression:

Expression<Func<User, bool>> userExp =
            user => user.UserInRoles.Any(userInRole => userInRole.RoleId == SystemRoles.SysAdmin.Id);

Expression<Func<Estate, User>> propAccessor = estate => estate.ApprovedByUser;


var estateParam = Expression.Parameter(typeof(Estate));
var userParam = Expression.Invoke(propAccessor, estateParam);
var translatedExp = Expression.Invoke(userExp, userParam);
var result = (Expression<Func<Estate, bool>>)Expression.Lambda(translatedExp, estateParam);

var exceptionProvider = new CommonContext().Set<Estate>().Where(result).ToList();

But I need something which can be translated into Store Expression maybe the final solution is decomposing and then recomposing the expression , and if so ,, how can I encapsulate it for reusing it in similar situations? (as this is what i'm trying to do)


Solution

  • You will need to rewrite the expression. I wrote an extension method for WithParameter.

    First we are going to need to borrow the ParameterVisitor class from this answer https://stackoverflow.com/a/5431309/1798889 and tweak it a bit. We don't want to just replace the parameter in one expression with a different parameter we want to remove the parameter and replace it with a new expression.

    public class ParameterVisitor : ExpressionVisitor
    {
        private readonly ReadOnlyCollection<ParameterExpression> from;
        // Changed from ReadOnlyCollection of ParameterExpression to Expression 
        private readonly Expression[] to;
    
        public ParameterVisitor(ReadOnlyCollection<ParameterExpression> from, params Expression[] to)
        {
            if (from == null) 
                throw new ArgumentNullException("from");
    
            if (to == null) 
                throw new ArgumentNullException("to");
    
            if (from.Count != to.Length)
                throw new InvalidOperationException("Parameter lengths must match");
    
            this.from = from;
            this.to = to;
        }
    
        protected override Expression VisitParameter(ParameterExpression node)
        {
            for (int i = 0; i < from.Count; i++)
            {
                if (node == from[i]) return to[i];
            }
    
            return node;
        }
    }
    

    Notice I changed the To parameter to just be an expression and not a ParameterExpression.

    Now we are going to create the WithParameter extension method:

    public static class QueryableExtensions
    {
        public static Expression<Func<TResult, bool>> WithParameter<TResult, TSource>(this Expression<Func<TSource, bool>> source, Expression<Func<TResult, TSource>> selector)
        {
            // Replace parameter with body of selector
            var replaceParameter = new ParameterVisitor(source.Parameters, selector.Body);
            // This will be the new body of the expression
            var newExpressionBody = replaceParameter.Visit(source.Body);
            return Expression.Lambda<Func<TResult, bool>>(newExpressionBody, selector.Parameters);
        }
    }
    

    In this we take the selector of how we know what property we want and replace the parameter of the Expression<Func<User, bool>> with that property.

    You use it like

    new CommonContext().Set<Estate>()
                       .Where(userExp.WithParameter<Estate, User>(estate => estate.CreatorUser))
                       .ToList();
    

    Word of warning I don't know Linq to SQL and didn't test it against that and I didn't test it against IQueryable, but it does work for IEnumerable. I don't see why it wouldn't work as the debug view of the two expression at the end look the same.

    Expression<Func<Estate, bool>> estateExp = estate => estate.CreatorUser.UserInRoles.Any(userInRole => userInRole.RoleId ==  SystemRoles.SysAdmin.Id);
    

    is the same expression tree as

    userExp.WithParameter(estate => estate.CreatorUser)