Pass expression parameter as argument to another expression
Asked Answered
F

2

9

I have a query which filters results:

public IEnumerable<FilteredViewModel> GetFilteredQuotes()
{
    return _context.Context.Quotes.Select(q => new FilteredViewModel
    {
        Quote = q,
        QuoteProductImages = q.QuoteProducts.SelectMany(qp => qp.QuoteProductImages.Where(qpi => q.User.Id == qpi.ItemOrder))
    });
}

In the where clause I'm using the parameter q to match a property against a property from the parameter qpi. Because the filter will be used in several places I'm trying to rewrite the where clause to an expression tree which would look like something like this:

public IEnumerable<FilteredViewModel> GetFilteredQuotes()
{
    return _context.Context.Quotes.Select(q => new FilteredViewModel
    {
        Quote = q,
        QuoteProductImages = q.QuoteProducts.SelectMany(qp => qp.QuoteProductImages.AsQueryable().Where(ExpressionHelper.FilterQuoteProductImagesByQuote(q)))
    });
}

In this query the parameter q is used as a parameter to the function:

public static Expression<Func<QuoteProductImage, bool>> FilterQuoteProductImagesByQuote(Quote quote)
{
    // Match the QuoteProductImage's ItemOrder to the Quote's Id
}

How would I implement this function? Or should I use a different approach alltogether?

Fetterlock answered 4/4, 2015 at 15:57 Comment(0)
C
13

If I understand correctly, you want to reuse an expression tree inside another one, and still allow the compiler to do all the magic of building the expression tree for you.

This is actually possible, and I have done it in many occasions.

The trick is to wrap your reusable part in a method call, and then before applying the query, unwrap it.

First I would change the method that gets the reusable part to be a static method returning your expression (as mr100 suggested):

 public static Expression<Func<Quote,QuoteProductImage, bool>> FilterQuoteProductImagesByQuote()
 {
     return (q,qpi) => q.User.Id == qpi.ItemOrder;
 }

Wrapping would be done with:

  public static TFunc AsQuote<TFunc>(this Expression<TFunc> exp)
  {
      throw new InvalidOperationException("This method is not intended to be invoked, just as a marker in Expression trees!");
  }

Then unwrapping would happen in:

  public static Expression<TFunc> ResolveQuotes<TFunc>(this Expression<TFunc> exp)
  {
      var visitor = new ResolveQuoteVisitor();
      return (Expression<TFunc>)visitor.Visit(exp);
  }

Obviously the most interesting part happens in the visitor. What you need to do, is find nodes that are method calls to your AsQuote method, and then replace the whole node with the body of your lambdaexpression. The lambda will be the first parameter of the method.

Your resolveQuote visitor would look like:

    private class ResolveQuoteVisitor : ExpressionVisitor
    {
        public ResolveQuoteVisitor()
        {
            m_asQuoteMethod = typeof(Extensions).GetMethod("AsQuote").GetGenericMethodDefinition();
        }
        MethodInfo m_asQuoteMethod;
        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            if (IsAsquoteMethodCall(node))
            {
                // we cant handle here parameters, so just ignore them for now
                return Visit(ExtractQuotedExpression(node).Body);
            }
            return base.VisitMethodCall(node);
        }

        private bool IsAsquoteMethodCall(MethodCallExpression node)
        {
            return node.Method.IsGenericMethod && node.Method.GetGenericMethodDefinition() == m_asQuoteMethod;
        }

        private LambdaExpression ExtractQuotedExpression(MethodCallExpression node)
        {
            var quoteExpr = node.Arguments[0];
            // you know this is a method call to a static method without parameters
            // you can do the easiest: compile it, and then call:
            // alternatively you could call the method with reflection
            // or even cache the value to the method in a static dictionary, and take the expression from there (the fastest)
            // the choice is up to you. as an example, i show you here the most generic solution (the first)
            return (LambdaExpression)Expression.Lambda(quoteExpr).Compile().DynamicInvoke();
        }
    }

Now we are already half way through. The above is enough, if you dont have any parameters on your lambda. In your case you do, so you want to actually replace the parameters of your lambda to the ones from the original expression. For this, I use the invoke expression, where I get the parameters I want to have in the lambda.

First lets create a visitor, that will replace all parameters with the expressions that you specify.

    private class MultiParamReplaceVisitor : ExpressionVisitor
    {
        private readonly Dictionary<ParameterExpression, Expression> m_replacements;
        private readonly LambdaExpression m_expressionToVisit;
        public MultiParamReplaceVisitor(Expression[] parameterValues, LambdaExpression expressionToVisit)
        {
            // do null check
            if (parameterValues.Length != expressionToVisit.Parameters.Count)
                throw new ArgumentException(string.Format("The paraneter values count ({0}) does not match the expression parameter count ({1})", parameterValues.Length, expressionToVisit.Parameters.Count));
            m_replacements = expressionToVisit.Parameters
                .Select((p, idx) => new { Idx = idx, Parameter = p })
                .ToDictionary(x => x.Parameter, x => parameterValues[x.Idx]);
            m_expressionToVisit = expressionToVisit;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            Expression replacement;
            if (m_replacements.TryGetValue(node, out replacement))
                return Visit(replacement);
            return base.VisitParameter(node);
        }

        public Expression Replace()
        {
            return Visit(m_expressionToVisit.Body);
        }
    }

Now we can advance back to our ResolveQuoteVisitor, and hanlde invocations correctly:

        protected override Expression VisitInvocation(InvocationExpression node)
        {
            if (node.Expression.NodeType == ExpressionType.Call && IsAsquoteMethodCall((MethodCallExpression)node.Expression))
            {
                var targetLambda = ExtractQuotedExpression((MethodCallExpression)node.Expression);
                var replaceParamsVisitor = new MultiParamReplaceVisitor(node.Arguments.ToArray(), targetLambda);
                return Visit(replaceParamsVisitor.Replace());
            }
            return base.VisitInvocation(node);
        }

This should do all the trick. You would use it as:

  public IEnumerable<FilteredViewModel> GetFilteredQuotes()
  {
      Expression<Func<Quote, FilteredViewModel>> selector = q => new FilteredViewModel
      {
          Quote = q,
          QuoteProductImages = q.QuoteProducts.SelectMany(qp => qp.QuoteProductImages.Where(qpi => ExpressionHelper.FilterQuoteProductImagesByQuote().AsQuote()(q, qpi)))
      };
      selector = selector.ResolveQuotes();
      return _context.Context.Quotes.Select(selector);
  }

Of course I think you can make here much more reusability, with defining expressions even on a higher levels.

You could even go one step further, and define a ResolveQuotes on the IQueryable, and just visit the IQueryable.Expression and creating a new IQUeryable using the original provider and the result expression, e.g:

    public static IQueryable<T> ResolveQuotes<T>(this IQueryable<T> query)
    {
        var visitor = new ResolveQuoteVisitor();
        return query.Provider.CreateQuery<T>(visitor.Visit(query.Expression));
    }

This way you can inline the expression tree creation. You could even go as far, as override the default query provider for ef, and resolve quotes for every executed query, but that might go too far :P

You can also see how this would translate to actually any similar reusable expression trees.

I hope this helps :)

Disclaimer: Remember never copy paste code from anywhere to production without understanding what it does. I didn't include much error handling here, to keep the code to minimum. I also didn't check the parts that use your classes if they would compile. I also don't take any responsability for the correctness of this code, but i think the explanation should be enough, to understand what is happening, and fix it if there are any issues with it. Also remember, that this only works for cases, when you have a method call that produces the expression. I will soon write a blog post based on this answer, that allows you to use more flexibility there too :P

Cheshvan answered 6/4, 2015 at 12:13 Comment(3)
Ok I'm impressed, it worked perfectly. This is definitely very useful and I'll try and make it more generic so I can use it on more occassions.Fetterlock
I get a null reference exception on the ResolveQuoteVisitor, line m_asQuoteMethod = typeof(Extensions).GetMethod("AsQuote").GetGenericMethodDefinition(); any ideas?Lilialiliaceous
This post is really old, you should try LinqKit, a which can do all the above out of the boxCheshvan
O
3

Implementing this your way will cause an exception thrown by ef linq-to-sql parser. Within your linq query you invokes FilterQuoteProductImagesByQuote function - this is interpreted as Invoke expression and it simply cannot be parsed to sql. Why? Generally because from SQL there is no possibility to invoke MSIL method. The only way to pass expression to query is to store it as Expression> object outside of the query and then pass it to Where method. You can't do this as outside of the query you will not have there Quote object. This implies that generally you cannot achieve what you wanted. What you possibly can achieve is to hold somewhere whole expression from Select like this:

Expression<Func<Quote,FilteredViewModel>> selectExp =
    q => new FilteredViewModel
    {
        Quote = q,
        QuoteProductImages = q.QuoteProducts.SelectMany(qp =>  qp.QuoteProductImages.AsQueryable().Where(qpi => q.User.Id == qpi.ItemOrder)))
    };

And then you may pass it to select as argument:

_context.Context.Quotes.Select(selectExp);

thus making it reusable. If you would like to have reusable query:

qpi => q.User.Id == qpi.ItemOrder

Then first you would have to create different method for holding it:

public static Expression<Func<Quote,QuoteProductImage, bool>> FilterQuoteProductImagesByQuote()
{
    return (q,qpi) => q.User.Id == qpi.ItemOrder;
}

Application of it to your main query would be possible, however quite difficult and hard to read as it will require defining that query with use of Expression class.

Outguess answered 5/4, 2015 at 17:38 Comment(2)
Thanks for your clear answer!I have tried manually building the expression tree but I run into the problem where I don't have access to the q parameter and redefining it is not allowed. I could build the entire query (not just the where clause) myself but that's not worth the effort since the actual query I would have to build is rather large and complex. I will just ditch the reusability instead and write the same query multiple times.Fetterlock
I as well tried to build that query manually and I almost finished it, but it looked very complex and thus would be hard to maintain, so I came to a conclusion that you won't be interested to see it as it gives no real benefit. During working with ef unfortunatelly I very often came to a conclusion that in certain situations we have to agree on code duplication.Outguess

© 2022 - 2024 — McMap. All rights reserved.