Expression<Func<T, TProperty>> 列表

12
我正在寻找一种方法来存储一个Expression<Func<T, TProperty>>集合,用于对元素进行排序,然后将存储的列表执行到IQueryable<T>对象上(底层提供程序是Entity Framework)。
例如,我想做这样的事情(这是伪代码):
public class Program
{
    public static void Main(string[] args)
    {
        OrderClause<User> orderBys = new OrderClause<User>();
        orderBys.AddOrderBy(u => u.Firstname);
        orderBys.AddOrderBy(u => u.Lastname);
        orderBys.AddOrderBy(u => u.Age);

        Repository<User> userRepository = new Repository<User>();
        IEnumerable<User> result = userRepository.Query(orderBys.OrderByClauses);
    }
}

一个order by子句(用于排序的属性):
public class OrderClause<T>
{
    public void AddOrderBy<TProperty>(Expression<Func<T, TProperty>> orderBySelector)
    {
        _list.Add(orderBySelector);
    }

    public IEnumerable<Expression<Func<T, ???>>> OrderByClauses
    {
        get { return _list; }
    }
}

我的查询方法存储库:

public class Repository<T>
{
    public IEnumerable<T> Query(IEnumerable<OrderClause<T>> clauses)
    {
        foreach (OrderClause<T, ???> clause in clauses)
        {
            _query = _query.OrderBy(clause);
        }

        return _query.ToList();
    }
}

我的第一个想法是将Expression<Func<T, TProperty>>转换为字符串(用于排序的属性名称)。因此,我存储了一个字符串列表,而不是一个类型化列表(因为TProperty不是常量),用于排序。

但这样做不起作用,因为我无法重新构建Expression(我需要它,因为IQueryable.OrderBy需要一个Expression<Func<T, TKey>>作为参数)。

我还尝试动态创建表达式(借助Expression.Convert的帮助),以获得Expression<Func<T, object>>,但然后我从实体框架中得到了一个异常,该异常指出它无法处理Expression.Convert语句。

如果可能,我不想使用外部库,如Dynamic Linq Library


顺便说一句,你的代码不会起作用,你需要调用 OrderBy() 一次并在后续调用中使用 ThenBy() - svick
正如我在问题中所述,这只是伪代码...实际上,我已经有了解决我的问题的方法,但是使用动态 Linq 库,而我想避免。因此,您提到的排序问题已经解决:),但还是谢谢! - Bidou
4个回答

13

这是少数几种情况之一,可以采用动态/反射解决方案。

我认为您想要的是这样的内容?(我已经读懂了您的意思,并在必要时对结构进行了一些更改。)

public class OrderClauseList<T>
{
    private readonly List<LambdaExpression> _list = new List<LambdaExpression>();

    public void AddOrderBy<TProperty>(Expression<Func<T, TProperty>> orderBySelector)
    {
        _list.Add(orderBySelector);
    }

    public IEnumerable<LambdaExpression> OrderByClauses
    {
        get { return _list; }
    }
}

public class Repository<T>
{
    private IQueryable<T> _source = ... // Don't know how this works

    public IEnumerable<T> Query(OrderClause<T> clauseList)
    {
        // Needs validation, e.g. null-reference or empty clause-list. 

        var clauses = clauseList.OrderByClauses;

        IOrderedQueryable<T> result = Queryable.OrderBy(_source, 
                                                        (dynamic)clauses.First());

        foreach (var clause in clauses.Skip(1))
        {
            result = Queryable.ThenBy(result, (dynamic)clause);
        }

        return result.ToList();
    }
}

关键的技巧是利用C# dynamic 来为我们完成糟糕的重载解析和类型推断。更重要的是,尽管使用了 dynamic,上述内容实际上是类型安全的!


3

有一种方法可以实现这一点,就是将所有的排序条件存储在类似于 Func<IQueryable<T>, IOrderedQueryable<T>> 的东西中(也就是一个调用排序方法的函数):

public class OrderClause<T>
{
    private Func<IQueryable<T>, IOrderedQueryable<T>> m_orderingFunction;

    public void AddOrderBy<TProperty>(Expression<Func<T, TProperty>> orderBySelector)
    {
        if (m_orderingFunction == null)
        {
            m_orderingFunction = q => q.OrderBy(orderBySelector);
        }
        else
        {
            // required so that m_orderingFunction doesn't reference itself
            var orderingFunction = m_orderingFunction;
            m_orderingFunction = q => orderingFunction(q).ThenBy(orderBySelector);
        }
    }

    public IQueryable<T> Order(IQueryable<T> source)
    {
        if (m_orderingFunction == null)
            return source;

        return m_orderingFunction(source);
    }
}

这种方法可以避免使用反射或dynamic,所有代码都是类型安全的且相对容易理解。


@Bidou,我真的不明白你想说什么。如果你想保持Repository就像你的伪代码中那样,只需更改其代码以获取整个OrderClause,然后执行return orderBys.Order(_query).ToList(); - svick
1
+1:这是一个非常好的解决方案,正如你所说,巧妙地避开了动态/反射。 - Ani
确实是个不错的解决方案!但相比于“动态”解决方案,调试可能更加困难。我的意思是,没有简单的方法来检查“OrderClause”的各个组件(排序表达式),你必须将其应用到“IQueryable”中才能看到其中的内容。 - Gebb
@svick 很难用几个词来解释所有的东西...但实际上,OrderClause<T>更像是一个QueryOption<T>,其中我有OrderBy以及其他一些东西,比如Where谓词、分页等等...这样做的目的是为了让GUI有可能创建一个查询,而不必知道它在业务逻辑中是如何工作的(例如,QueryOption<T>使用DTO,例如QueryOption<UserDTO>,但我的Repository<T>使用实体对象,例如QueytionOption<User>。这意味着我在两者之间做了一个映射...) - Bidou
例如,因为在GUI中直接访问IQueryable<T>可以通过导航属性浏览整个EF图形(在我的情况下,IQueryable<T>的提供程序是Entity Framework),这将绕过所有安全逻辑!这就是我选择DTO模式以及为什么我不想在此QueryOption<T>中使用IQueryable的原因。我错了吗? - Bidou
显示剩余4条评论

2

你可以将lambda表达式存储在一个集合中,作为LambdaExpression类型的实例。

或者更好的方法是,存储排序定义,每个排序定义除了表达式之外,还存储了排序方向。

假设您有以下扩展方法:

public static IQueryable<T> OrderBy<T>(
    this IQueryable<T> source,
    SortDefinition sortDefinition) where T : class
{
    MethodInfo method;
    Type sortKeyType = sortDefinition.Expression.ReturnType;
    if (sortDefinition.Direction == SortDirection.Ascending)
    {
        method = MethodHelper.OrderBy.MakeGenericMethod(
            typeof(T),
            sortKeyType);
    }
    else
    {
        method = MethodHelper.OrderByDescending.MakeGenericMethod(
            typeof(T),
            sortKeyType);
    }

    var result = (IQueryable<T>)method.Invoke(
        null,
        new object[] { source, sortDefinition.Expression });
    return result;
}

还有一个类似的方法ThenBy。然后你可以这样做:

myQueryable = myQueryable.OrderBy(sortDefinitions.First());

myQueryable = sortDefinitions.Skip(1).Aggregate(
   myQueryable,
   (current, sortDefinition) => current.ThenBy(sortDefinition));

以下是SortDefinitionMethodHelper的定义: SortDefinition:用于定义排序规则的接口,它包含了排序的方向、属性名称等信息。 MethodHelper:一个帮助类,提供了一些常用的方法,如获取类的属性、判断两个对象是否相等等。
public class SortDefinition
{
    public SortDirection Direction
    {
        get;
        set;
    }

    public LambdaExpression Expression
    {
        get;
        set;
    }
}

internal static class MethodHelper
{
    static MethodHelper()
    {
        OrderBy = GetOrderByMethod();
        ThenBy = GetThenByMethod();
        OrderByDescending = GetOrderByDescendingMethod();
        ThenByDescending = GetThenByDescendingMethod();
    }

    public static MethodInfo OrderBy
    {
        get;
        private set;
    }

    public static MethodInfo ThenBy
    {
        get;
        private set;
    }

    public static MethodInfo OrderByDescending
    {
        get;
        private set;
    }

    public static MethodInfo ThenByDescending
    {
        get;
        private set;
    }

    private static MethodInfo GetOrderByMethod()
    {
        Expression<Func<IQueryable<object>, IOrderedQueryable<object>>> expr =
            q => q.OrderBy((Expression<Func<object, object>>)null);

        return ((MethodCallExpression)expr.Body).Method.GetGenericMethodDefinition();
    }

    private static MethodInfo GetThenByMethod()
    {
        Expression<Func<IOrderedQueryable<object>, IOrderedQueryable<object>>> expr =
            q => q.ThenBy((Expression<Func<object, object>>)null);

        return ((MethodCallExpression)expr.Body).Method.GetGenericMethodDefinition();
    }

    private static MethodInfo GetOrderByDescendingMethod()
    {
        Expression<Func<IQueryable<object>, IOrderedQueryable<object>>> expr =
            q => q.OrderByDescending((Expression<Func<object, object>>)null);

        return ((MethodCallExpression)expr.Body).Method.GetGenericMethodDefinition();
    }

    private static MethodInfo GetThenByDescendingMethod()
    {
        Expression<Func<IOrderedQueryable<object>, IOrderedQueryable<object>>> expr =
            q => q.ThenByDescending((Expression<Func<object, object>>)null);

        return ((MethodCallExpression)expr.Body).Method.GetGenericMethodDefinition();
    }
}

非常好,谢谢!不过,聚合可能不起作用(我没有测试),因为如果你想进行多重排序,你需要调用 ThenBy/ThenByDescending。 - Bidou
还有一件事:按照Ani提出的动态关键字来执行查询更容易。基本上,它替换了你的MethodHelper类。但无论如何,感谢你的帮助回答! - Bidou
@Bidou:我测试了一下,发现带有OrderBy的聚合不起作用。我会编辑答案。 - Gebb

0
在 E.F. Core 中,您可以使用以下辅助类。
public static class OrderBuilder
{
    public static IQueryable<TSource> OrderBy<TSource>(this IQueryable<TSource> queryable, params Tuple<Expression<Func<TSource, object>>, bool>[] keySelectors)
    {
        if (keySelectors == null || keySelectors.Length == 0) return queryable;

        return keySelectors.Aggregate(queryable, (current, keySelector) => keySelector.Item2 ? current.OrderDescending(keySelector.Item1) : current.Order(keySelector.Item1));
    }

    private static bool IsOrdered<TSource>(this IQueryable<TSource> queryable)
    {
        if (queryable == null) throw new ArgumentNullException(nameof(queryable));

        return queryable.Expression.Type == typeof(IOrderedQueryable<TSource>);
    }

    private static IQueryable<TSource> Order<TSource, TKey>(this IQueryable<TSource> queryable, Expression<Func<TSource, TKey>> keySelector)
    {
        if (!queryable.IsOrdered()) return queryable.OrderBy(keySelector);

        var orderedQuery = queryable as IOrderedQueryable<TSource>;

        return (orderedQuery ?? throw new InvalidOperationException()).ThenBy(keySelector);
    }

    private static IQueryable<TSource> OrderDescending<TSource, TKey>(this IQueryable<TSource> queryable, Expression<Func<TSource, TKey>> keySelector)
    {
        if (!queryable.IsOrdered()) return queryable.OrderByDescending(keySelector);

        var orderedQuery = queryable as IOrderedQueryable<TSource>;
        return (orderedQuery ?? throw new InvalidOperationException()).ThenByDescending(keySelector);
    }
}

然后将其用作..以下示例,其中使用名为Player的类及其以下成员..(代码简化以节省篇幅)

 public class Player
 {
    ...

    public string FirstName { get; set; 
    public int GenderTypeId { get; set; } 
    public int PlayingExperience { get; set; }

您可以按照自己的喜好组合排序方式,例如按性别、按玩耍经验降序排列(请注意元组的true值)以及按名字排序。

    var combinedOrder = new[]
    {
        new Tuple<Expression<Func<Player, object>>, bool>(p => p.GenderTypeId, false),
        new Tuple<Expression<Func<Player, object>>, bool>(p => p.PlayingExperience, true),
        new Tuple<Expression<Func<Player, object>>, bool>(p => p.FirstName, false),
    };

只需按照以下顺序执行命令

        var data = context.Set<Player>()
            .OrderBy(combinedOrder)
            .ToArray();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接