Список выражений <Func <T, TProperty >>

Я ищу способ хранения коллекции 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>> as).

Я также попытался динамически создать Expression (с помощью Expression.Convert), чтобы иметь Expression<Func<T, object>>, но затем я получил исключение из фреймворка сущности, который сказал, что он не смог обработать выражение Expression.Convert.

Если возможно, я не хочу использовать внешнюю библиотеку, такую ​​как Динамическая библиотека Linq.

Ответ 1

Это один из немногих случаев, когда может быть подходящим решение dynamic/reflection.

Я думаю, тебе нужно что-то вроде этого? (Я читал между строками и делал некоторые изменения в вашей структуре, где я считал нужным).

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

Ключевым трюком становится С# dynamic, чтобы сделать ужасное разрешение перегрузки и тип-вывода для нас. Что еще, я считаю, что выше, несмотря на использование dynamic, на самом деле безопасен по типу!

Ответ 2

Один из способов сделать это - "сохранить" все классовые предложения в виде 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, весь этот код безопасен по типу и относительно легко понять.

Ответ 3

Вы можете хранить ваши лямбда-выражения в коллекции как экземпляры типа 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));

Вот определения 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();
    }
}

Ответ 4

в EF 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; }

Вы можете комбинировать упорядочения по своему усмотрению, сортировку по полу, игровой опыт по убыванию (обратите внимание на истинное значение кортежа) и имя.

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