【问题标题】:ExpressionTree ExpressionVisitor to change/replace query OrderBy FieldExpressionTree ExpressionVisitor 更改/替换查询 OrderBy 字段
【发布时间】:2020-02-04 17:08:54
【问题描述】:

我需要能够在表达式树上的 orderby 字段转换为 sql 之前更改它。 例如,如果查询包含 orderby "className.PropertyA",我需要将其修改为 orderby "className.PropertyB" 到目前为止,我的想法是编写一个表达式访问者来更改树上的 orderby 方法节点。我的代码是这样的

public class ClassName
{
    public string PropertyA { get; set; }

    public string PropertyB { get; set; }

    public string PropertyC { get; set; }

    public string PropertyD { get; set; }
}

public class ChangeOrderByVisitor : ExpressionVisitor
{

    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        if (node.Method.DeclaringType == typeof(Queryable) &&
            (node.Method.Name == "OrderBy" || node.Method.Name == "OrderByDescending"))
        {
            //Only if ordering by className.PropertyA
                    //Somehow change the order by arguments.operands.body from className.PropertyA to className.PropertyB
        }

        return base.VisitMethodCall(node);
    }

}

在某些时候,此表达式树将被转换为 sql,并且在对数据库执行时应按 className.PropertyB 排序。

谢谢。

【问题讨论】:

    标签: c# .net linq expression-trees


    【解决方案1】:

    把它当作一个很好的编程练习 :-)

    这是一个处理反射和表达式的类:

    using System;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;
    
    public static class MyExtensions
    {
        /// <summary>
        /// Helper method to reflect the return type.
        /// </summary>
        public static Expression<Func<T1, TResult>> FuncX<T1, TResult>(Expression<Func<T1, TResult>> lambda)
            => lambda;
    
        /// <summary>
        /// Helper method to get a <see cref="MemberInfo"/>.
        /// </summary>
        public static MemberInfo GetMember<TSource, TResult>(this Expression<Func<TSource, TResult>> lambda)
            => (lambda?.Body as MemberExpression)?.Member ?? throw new ArgumentException($"Not a {nameof(MemberExpression)}.");
    
        /// <summary>
        /// Helper method to get a <see cref="MethodInfo"/>.
        /// </summary>
        public static MethodInfo GetMethod<TSource, TResult>(this Expression<Func<TSource, TResult>> lambda)
            => (lambda?.Body as MethodCallExpression)?.Method ?? throw new ArgumentException($"Not a {nameof(MethodCallExpression)}.");
    
        /// <summary>
        /// <see cref="Queryable.OrderBy{TSource,TKey}(System.Linq.IQueryable{TSource},System.Linq.Expressions.Expression{System.Func{TSource,TKey}})"/>
        /// </summary>
        private static readonly MethodInfo _miOrderBy = GetMethod((IQueryable<int> q) => q.OrderBy(x => x)).GetGenericMethodDefinition();
    
        /// <summary>
        /// Replace occurrencies of OrderBy(<paramref name="origKeySelector"/>) with OrderBy(<paramref name="newKeySelector"/>).
        /// </summary>
        /// <exception cref="ArgumentException"><paramref name="origKeySelector"/>'s body is not a <see cref="MemberExpression"/>.</exception>
        public static IQueryable<TQueryable> ChangeOrder<TQueryable, TOrdered, TOrigOrder, TNewOrder>(this IQueryable<TQueryable> queryable, Expression<Func<TOrdered, TOrigOrder>> origKeySelector, Expression<Func<TOrdered, TNewOrder>> newKeySelector)
        {
            var changed = new ChangeOrderVisitor<TOrdered, TOrigOrder, TNewOrder>(origKeySelector, newKeySelector).Visit(queryable.Expression);
            return queryable.Provider.CreateQuery<TQueryable>(changed);
        }
    
        private sealed class ChangeOrderVisitor<TOrdered, TOrigOrder, TNewOrder> : ExpressionVisitor
        {
            private static readonly MethodInfo _miOrigOrderBy = _miOrderBy.MakeGenericMethod(typeof(TOrdered), typeof(TOrigOrder));
            private static readonly MethodInfo _miNewOrderBy = _miOrderBy.MakeGenericMethod(typeof(TOrdered), typeof(TNewOrder));
    
            private readonly MemberInfo _origMember;
            private readonly Expression<Func<TOrdered, TNewOrder>> _newKeySelector;
    
            public ChangeOrderVisitor(Expression<Func<TOrdered, TOrigOrder>> origKeySelector, Expression<Func<TOrdered, TNewOrder>> newKeySelector)
            {
                _origMember = origKeySelector.GetMember();
                _newKeySelector = newKeySelector;
            }
    
            protected override Expression VisitMethodCall(MethodCallExpression node)
            {
                if (node.Method == _miOrigOrderBy)
                {
                    if (node.Arguments[1] is UnaryExpression u &&
                        u.Operand is LambdaExpression lambda &&
                        lambda.Body is MemberExpression mx &&
                        mx.Member == _origMember)
                        return Expression.Call(_miNewOrderBy, Visit(node.Arguments[0]), _newKeySelector);
                }
                return base.VisitMethodCall(node);
            }
        }
    }
    

    这是测试代码:

    var origOrder = MyExtensions.FuncX((Person p) => p.FirstName);
    var qOrig = new[]
    {
        new Person{ FirstName = "Elon", LastName = "Musk", ShoeSize = 44 },
        new Person{ FirstName = "Jeff", LastName = "Who?", ShoeSize = 40 }
    }
        .AsQueryable()
        .OrderBy(origOrder)
        .Select(p => p.LastName);
    var qChanged = qOrig.ChangeOrder(origOrder, x => x.ShoeSize); // <string, Person, string, int>
    var result = qChanged.ToList(); // "Who?", "Musk"
    

    【讨论】:

    • 谢谢,正是我需要的
    • 不幸的是,ef core 在解释更改后的查询“Microsoft.EntityFrameworkCore.Query:Warning: The LINQ expression 'orderby x => x.PropertyB asc' 时输出以下内容无法翻译并将在本地进行评估。”知道如何解决这个问题,以便在数据库服务器而不是客户端上评估表达式吗?
    • 很遗憾没有。原始(PropertyA)查询是否有效?当您再次用 PropertyA 替换原来的时它是否有效?
    • 让它工作并避免 ef 核心警告。我犯了一个语法错误。我的错。再次感谢。
    【解决方案2】:

    虽然您可以在运行时通过反射修改表达式,但为了简单起见,我建议您采用不同的方法。

    我建议您将按参数存储在表达式之外的顺序,直到您确定要编译该表达式。

    public class OrderByArgumentProvider<TSource> {
        private Expression<Func<TSource, object>> _keySelector;
    
        public void Update(Expression<Func<TSource, object>> newKey) {
            _keySelector = newKey;
        }
    
        public Expression<Func<TSource, object>> GetKeySelector() {
            return _keySelector;
        }
    }
    

    然后你可以像这样使用它:

    var orderByArgumentProvider = new OrderByArgumentProvider<Model>();
    
    orderByArgumentProvider.Update(x => x.Id);
    
    _context.Table.OrderBy(orderByArgumentProvider.GetKeySelector());
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-04-20
      • 1970-01-01
      • 2018-05-12
      • 1970-01-01
      • 2020-11-21
      • 2021-09-01
      • 1970-01-01
      • 2014-10-30
      相关资源
      最近更新 更多