Pass expression parameter as argument to another expression

后端 未结 2 635
野趣味
野趣味 2020-11-27 06:02

I have a query which filters results:

public IEnumerable GetFilteredQuotes()
{
    return _context.Context.Quotes.Select(q => new         


        
相关标签:
2条回答
  • 2020-11-27 06:31

    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.

    0 讨论(0)
  • 2020-11-27 06:52

    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

    0 讨论(0)
提交回复
热议问题