【问题标题】:EntityFramework 4 OrderBy overwrites previous OrderBy callsEntityFramework 4 OrderBy 覆盖之前的 OrderBy 调用
【发布时间】:2011-09-10 01:23:25
【问题描述】:

我经常想为 IQueryables 提供排序,如果稍后指定另一个排序,它们应该像二级排序一样。例如:

Repository.All.OrderBy(o => o.Name).OrderBy(o => o.SerialNumber)  [A]

应该相当于:

Repository.All.OrderBy(o => o.SerialNumber).ThenBy(o => o.Name)

使用 LINQ to SQL 可以正常工作。但是,在 EntityFramework 4 中,生成的 SQL 中的 Order-By 子句如下所示:

ORDER BY [Project1].[SerialNumber] ASC

它完全忽略了第一个 OrderBy 语句,它实际上破坏了 OrderBy 是一个稳定的排序。 ThenBy 对我来说不是一个选项,因为排序并不总是在同一个地方定义(例如,在上面的语句 [A] 中,OrderBy(o => o.Name) 可以在存储库中定义。对 IQueryable<TModel> 的扩展不是一个好的解决方案,因为它不允许不同的存储库以不同的方式排序,并且消费代码不应该调用一些 .SortDefault() 代码,因为这不是它关心的问题。

有没有什么好方法可以强制 Linq to Entities 尊重多个 OrderBy 语句?

谢谢

【问题讨论】:

标签: c# entity-framework linq-to-entities


【解决方案1】:

我不同意后续的OrderBy 应该等同于ThenBy。如果是这样,就不需要ThenBy,而且你永远不能覆盖现有的排序。

我不能说我喜欢它,但在我看来,这似乎是下游排序的一个选项:

IQueryable<Item> items = Repository.GetAllWhichMightBeOrderedAlready();
return items is IOrderedEnumerable<Item>
    ? ((IOrderedQueryable<Item>)items).ThenBy(x => x.SomeProperty)
    : items.OrderBy(x => x.SomeProperty);

酌情替换IOrderedEnumerable&lt;T&gt;。

【讨论】:

  • 你误解了我的问题。 OrderBy 应该是一种稳定的排序,这意味着如果两条记录在排序中具有相同的位置,它们将保持其原始顺序。如果我执行 OrderBy(A).OrderBy(B),那应该等同于 OrderBy(B).ThenBy(A),因为“原始订单”是 A。我并不是说后续的 OrderBy 应该等同于 ThenBy;它当然不应该。您的答案不起作用,因为它只提供二级排序,而我正在尝试在原始排序之上添加一级排序。
【解决方案2】:

好吧,这不是最优雅的解决方案,但我能够以一种似乎可以正常工作的方式克服这个问题,尽管我怀疑所有时髦的反射可能会使它变得太慢。我创建了自己的自定义 IQueryable 类和关联的查询提供程序,它们采用 ExpressionVisitor 并在 GetEnumerator 和 Execute 调用上对该访问者调用 .Visit() 。我的基础存储库类返回一个新的 MappedExpressionQuery 并将 DbContext.Set() 返回的查询与产生所需排序的 ExpressionVisitor 一起传递给它。自定义可查询和提供程序类:

public class MappedExpressionQuery<T> : IOrderedQueryable<T>
{
  private IQueryable<T> baseQuery;
  private MappedExpressionQueryProvider<T> provider;

  public MappedExpressionQuery(IQueryable<T> query, ExpressionVisitor expressionMap)
  {
    baseQuery = query;
    provider = new MappedExpressionQueryProvider<T>(query.Provider, expressionMap);
  }

  #region IOrderedQueryable<T> Members

  public IEnumerator<T> GetEnumerator()
  {
    return baseQuery.Provider.CreateQuery<T>(provider.ExpressionMap.Visit(baseQuery.Expression)).GetEnumerator();
  }

  IEnumerator IEnumerable.GetEnumerator()
  {
    return baseQuery.Provider.CreateQuery(provider.ExpressionMap.Visit(baseQuery.Expression)).GetEnumerator();
  }

  public Type ElementType
  {
    get { return baseQuery.ElementType; }
  }

  public Expression Expression
  {
    get { return baseQuery.Expression; }
  }

  public IQueryProvider Provider
  {
    get { return provider; }
  }

  #endregion
}

public class MappedExpressionQueryProvider<T> : IQueryProvider
{
  public ExpressionVisitor ExpressionMap { get; private set; }
  private IQueryProvider baseProvider;

  public MappedExpressionQueryProvider(IQueryProvider baseProvider, ExpressionVisitor expressionMap)
  {
    this.ExpressionMap = expressionMap;
    this.baseProvider = baseProvider;
  }

  #region IQueryProvider Members

  public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
  {
    return new MappedExpressionQuery<TElement>(baseProvider.CreateQuery<TElement>(expression), ExpressionMap);
  }

  public IQueryable CreateQuery(Expression expression)
  {
    throw new NotImplementedException();
  }

  public TResult Execute<TResult>(Expression expression)
  {
    return baseProvider.Execute<TResult>(ExpressionMap.Visit(expression));
  }

  public object Execute(Expression expression)
  {
    return baseProvider.Execute(ExpressionMap.Visit(expression));
  }

  #endregion
}

当我的自定义 ExpressionVisitor 类找到 OrderBy 或 ThenBy 语句时,它会沿着表达式树向下移动,记录每个排序应处于的正确顺序,直到它找到一个不是 Order 语句且不与 Order 语句可交换的语句。然后它在表达式的末尾再次构建所有语句。因此,OrderBy(A).ThenBy(B).OrderBy(C).OrderBy(D).ThenBy(E) 返回并附有以下附加表达式:.OrderBy(D).ThenBy(E).ThenBy(C).ThenBy(A).ThenBy(B)。是的,它是多余的,但是 EntityFramework 无论如何都会忽略更下游的表达式,并且我只将此 QueryProvider 与来自 DbContext 的可查询对象一起使用。此表达式访问者的代码(我还修复了 .ToString() 即使在用于常量时也不会转换为 SQL,因此 DbContext.Set&lt;T&gt;().Where(o =&gt; o.Name == SomeConstant.ToString()) 现在可以使用):

public abstract class QueryModifier : ExpressionVisitor
{
  private bool OrganizedOrdering { get; set; }

  protected override Expression VisitMethodCall(MethodCallExpression node)
  {
    if (node.Method.Name == "ToString" && node.Method.DeclaringType == typeof(object))
    {
      try
      {
        //If the object calling ToString is parameterless, invoke the method and convert it into a constant.
        return Expression.Constant(Expression.Lambda(node).Compile().DynamicInvoke());
      }
      catch (InvalidOperationException)
      {
        throw new InvalidOperationException("ToString() can only be translated into SQL when used on parameterless expressions.");
      }
    }
    else if (IsOrderStatement(node.Method))
    {
      if (!OrganizedOrdering)
      {
        OrganizedOrdering = true;
        return RearrangeOrderStatements(node);
      }
      else
        return base.VisitMethodCall(node);
    }
    else if (OrganizedOrdering && !IsOrderCommutative(node.Method))
    {
      OrganizedOrdering = false;
      return base.VisitMethodCall(node);
    }
    else
    {
      return base.VisitMethodCall(node);
    }
  }

  private Expression RearrangeOrderStatements(MethodCallExpression node)
  {
    //List to store (OrderBy expression, position) tuples
    List<Tuple<MethodCallExpression, double>> orderByExpressions = new List<Tuple<MethodCallExpression, double>>();
    double low = 0;
    double high = 1;

    MethodCallExpression startNode = node;
    Expression lastNode = node.Arguments[0];

    //Travel down the chain and store all OrderBy and ThenBy statements found with their relative positions
    while (node != null && node.Method.DeclaringType == typeof(System.Linq.Queryable))
    {
      if (node.Arguments.Count == 0)
        break;

      if (node.Method.Name.StartsWith("OrderBy"))
      {
        orderByExpressions.Add(new Tuple<MethodCallExpression, double>(node, low));
        low = low + 1;
        high = low + 1;
      }
      else if (node.Method.Name.StartsWith("ThenBy"))
      {
        double pos = (high - low) * 0.9 + low;
        orderByExpressions.Add(new Tuple<MethodCallExpression, double>(node, pos));
        high = pos;
      }
      else if (!IsOrderCommutative(node.Method))
      {
        break;
      }

      lastNode = node.Arguments[0];
      node = lastNode as MethodCallExpression;
    }

    lastNode = startNode;
    var methods = typeof(Queryable).GetMethods().Where(o => IsOrderStatement(o));

    Type queryType = startNode.Arguments[0].Type.GetGenericArguments()[0];

    bool firstStatement = true;
    foreach (var tuple in orderByExpressions.OrderBy(o => o.Item2))
    {
      string methodName;
      if (firstStatement)
      {
        methodName = "OrderBy";
        firstStatement = false;
      }
      else
        methodName = "ThenBy";
      if (tuple.Item1.Method.Name.EndsWith("Descending"))
        methodName = methodName + "Descending";

      Type orderByTValueType = tuple.Item1.Arguments[1].Type.GetGenericArguments()[0].GetGenericArguments()[1];

      if (tuple.Item1.Arguments.Count == 3)
      {
        var method = methods.Single(o => o.Name == methodName && o.GetParameters().Length == 3)
          .MakeGenericMethod(queryType, orderByTValueType);
        lastNode = Expression.Call(method, lastNode, tuple.Item1.Arguments[1], tuple.Item1.Arguments[2]);
      }
      else
      {
        var method = methods.Single(o => o.Name == methodName && o.GetParameters().Length == 2)
          .MakeGenericMethod(queryType, orderByTValueType);
        lastNode = Expression.Call(method, lastNode, tuple.Item1.Arguments[1]);
      }
    }

    return Visit(lastNode);
  }

  /// <summary>
  /// Returns true if the given method call expression is commutative with OrderBy statements.
  /// </summary>
  /// <param name="expression"></param>
  /// <returns></returns>
  private bool IsOrderCommutative(MethodInfo method)
  {
    return new string[] { "Where", "Distinct", "AsQueryable" }.Contains(method.Name)
      && method.DeclaringType == typeof(System.Linq.Queryable);
  }

  private bool IsOrderStatement(MethodInfo method)
  {
    return (method.Name.StartsWith("OrderBy") || method.Name.StartsWith("ThenBy"))
      && method.DeclaringType == typeof(System.Linq.Queryable);
  }
}

【讨论】:

    【解决方案3】:

    所以您不能使用 ThenBy 因为初始的 OrderBy 可能会被跳过?做一个初始的虚拟 OrderBy 怎么样,然后其他的都是 ThenBy。

    // Basically, everything gets the same orderby ranking
    // I don't know offhand if you can use a constant here, but if you have an id,
    // you should be able to this.
    var list = context.MyTable.OrderBy(mt => mt.id - mt.id);
    
    if (order by field1)
        list = list.ThenBy(mt => mt.field1);
    
    if (order by field2)
        list = list.ThenBy(mt => mt.field2);
    

    等等……

    编辑:没关系。这行不通。正如我所想的那样,不能单独在单独的行上使用 ThenBy。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-02-01
      • 1970-01-01
      • 2012-07-20
      • 2014-10-05
      相关资源
      最近更新 更多