Pass expression parameter as argument to another expression

c# entity-framework expression expression-trees lambda

Question

I have a search that limits the 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))
    });
}

I'm using the parameter q in the where clause to compare a property to a property from the qpi parameter. I'm attempting to change the where clause to an expression tree that would resemble something like this since the filter will be used in several places:

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)))
    });
}

The function in this query takes the argument q as a parameter:

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

How might I put this function into practice? Or should I choose a whole other strategy?

1
8
4/4/2015 3:57:52 PM

Accepted Answer

If I have this right, you want to reuse one expression tree inside of another one while still letting the compiler do the complex work of creating the expression tree.

I have successfully accomplished this on several times, so it is doable.

The secret is to method call-wrap your reusable component before unwrapping it and running the query.

As proposed by mr100, I would first make the function that retrieves the reusable component a static method returning your expression:

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

Wrapping would involve:

  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!");
  }

Unwrapping would thereafter take place in:

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

Of course, the visitor is where the most exciting action takes place. To replace a node with the body of a lambdaexpression, you must first identify nodes that are method calls to your AsQuote method. The method's first argument will be the lambda.

What would your resolveQuote guest 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();
        }
    }

We have already reached the halfway point. If your lambda doesn't have any arguments, the above is sufficient. In your situation, you do, thus you should really change the lambda's arguments to those from the original expression. I use the invoke expression to get the arguments I wish to include in the lambda for this.

First, let's design a visitor that will substitute the expressions you supply for all arguments.

    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);
        }
    }

We can now return to our ResolveQuoteVisitor and make the proper handle invocations:

        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 ought to be sufficient. You might use it for:

  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);
  }

Naturally, I believe you may increase this area's reusability significantly by creating expressions even at higher levels.

Even further, you could declare a ResolveQuotes on the IQueryable and then just visit the IQueryable. Using the initial provider and the result expression, an expression is created along with a new IQUeryable, for example:

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

You may create an expression tree inline in this manner. Even though it would be overkill, you might resolve quotations for each query that is conducted by overriding the default query provider for ef:P

You can also see how this would apply to any reusable expression trees that are comparable.

Hope this was helpful.

Disclaimer: Always read the documentation before copying and pasting code into a production environment. In order to make the code as short as possible, I didn't include any error handling here. Additionally, I didn't verify that the portions that need your classes would build. I also disclaim responsibility for the accuracy of this code, but I believe the explanation should be sufficient to grasp what is going on and make any necessary corrections. Also keep in mind that this only works in situations when a method call generates the expression. I'll soon publish a blog article based on this response that gives you more leeway there as well:P

9
4/6/2015 12:13:01 PM

Popular Answer

If you implement this in your own manner, the ef linq-to-sql parser will throw an exception. You call the FilterQuoteProductImagesByQuote function inside your Linq query; this is viewed as an Invoke expression and cannot be processed to SQL. Why? In general, calling an MSIL method from SQL is not possible. Only by storing it as an Expression> object outside of the query and then passing it to the Where function can an expression be sent to a query. This is impossible since there won't be a Quote object outside of the query. This suggests that, usually speaking, you cannot get what you desire. What you can do is save a copy of Select's whole statement elsewhere.

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)))
    };

Following that, you can send it as an argument to select:

_context.Context.Quotes.Select(selectExp);

thereby enabling reuse. If you want a reusable search term:

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

Then you would need to develop a new holding mechanism:

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

It would be feasible to apply it to your primary query, but doing so would be challenging and difficult to understand since it would involve creating your main query using the Expression class.



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow