Как объединить два дерева выражений элементов?

Я пытаюсь объединить следующие выражения в одно выражение: item => item.sub, sub => sub.key, чтобы стать item => item.sub.key. Мне нужно сделать это, чтобы я мог создать метод OrderBy, который переводит селектор элементов отдельно в селектор ключей. Этого можно добиться, используя одну из перегрузок OrderBy и предоставив IComparer<T>, но это не будет преобразовано в SQL.

Ниже приведена сигнатура метода для дальнейшего разъяснения того, чего я пытаюсь достичь, а также реализация, которая не работает, но должна проиллюстрировать суть.

    public static IOrderedQueryable<TEntity> OrderBy<TEntity, TSubEntity, TKey>(
        this IQueryable<TEntity> source, 
        Expression<Func<TEntity, TSubEntity>> selectItem, 
        Expression<Func<TSubEntity, TKey>> selectKey)
        where TEntity : class
        where TSubEntity : class 
    {
        var parameterItem = Expression.Parameter(typeof(TEntity), "item");
        ...
        some magic
        ...
        var selector = Expression.Lambda(magic, parameterItem);
        return (IOrderedQueryable<TEntity>)source.Provider.CreateQuery(
            Expression.Call(typeof(Queryable), "OrderBy", new Type[] { source.ElementType, selector.Body.Type },
                 source.Expression, selector
                 ));
    } 

который будет называться:

.OrderBy(item => item.Sub, sub => sub.Key)

Это возможно? Есть ли способ лучше? Причина, по которой я хочу, чтобы метод OrderBy работал таким образом, заключается в поддержке сложного выражения выбора ключа, которое применяется ко многим сущностям, хотя они отображаются по-разному. Кроме того, я знаю способ сделать это, используя строковые представления глубоких свойств, но я пытаюсь сохранить его строго типизированным.


person LaserJesus    schedule 18.02.2009    source источник


Ответы (3)


Поскольку это LINQ-to-SQL, вы обычно можете использовать Expression.Invoke, чтобы ввести в действие подвыражение. Я посмотрю, смогу ли я привести пример (обновление: выполнено). Обратите внимание, однако, что EF не поддерживает это — вам нужно будет перестроить выражение с нуля. У меня есть код для этого, но он довольно длинный...

Код выражения (с использованием Invoke) довольно прост:

var param = Expression.Parameter(typeof(TEntity), "item");
var item = Expression.Invoke(selectItem, param);
var key = Expression.Invoke(selectKey, item);
var lambda = Expression.Lambda<Func<TEntity, TKey>>(key, param);
return source.OrderBy(lambda);

Вот пример использования на Northwind:

using(var ctx = new MyDataContext()) {
    ctx.Log = Console.Out;
    var rows = ctx.Orders.OrderBy(order => order.Customer,
        customer => customer.CompanyName).Take(20).ToArray();
}

С TSQL (переформатирован для соответствия):

SELECT TOP (20) [t0].[OrderID], -- snip
FROM [dbo].[Orders] AS [t0]
LEFT OUTER JOIN [dbo].[Customers] AS [t1]
  ON [t1].[CustomerID] = [t0].[CustomerID]
ORDER BY [t1].[CompanyName]
person Marc Gravell    schedule 18.02.2009
comment
Спасибо за это, Марк, Expression.Invoke был тем волшебством, которое я искал. Я отклонил его, думая, что это будет точно так же, как selectItem.Compile().Invoke(...), что, очевидно, не сработает. Я могу подтвердить через профилировщик, что SQL был создан, как и ожидалось. - person LaserJesus; 18.02.2009
comment
Чем это отличается от ctx.Orders.OrderBy(o => o.Customer).OrderBy(o => o.Customer.CompanyName) или, если уж на то пошло, просто ctx.Orders.OrderBy(o => o.Customer.CompanyName) ? - person John Leidegren; 18.02.2009
comment
Я упростил свой пример, на самом деле выражение, которое выбирает ключ (CompanyName), очень сложное, но постоянное, а часть, которая выбирает элемент (Customer), проста, но весьма разнообразна. Поэтому я пытался инкапсулировать постоянную часть. - person LaserJesus; 18.02.2009
comment
Есть ли шанс, что вы могли бы дать ссылку на этот длинный код, пожалуйста, Марк? У меня есть решение, основанное на этой работе для Linq to SQL, но я не могу заставить его работать с EF из-за вызова :-( - person Doctor Jones; 25.09.2010
comment
@DoctaJonez — попробуйте это — если оно неполное, пусть я знаю. - person Marc Gravell; 25.09.2010
comment
@Марк Отлично! Твой Expression-FU силен, мой добрый человек! :-) - person Doctor Jones; 25.09.2010
comment
Чтобы добавить небольшую заметку, вы также можете заставить это работать с LINQ to EF, используя метод Expand из LinqKit (albahari.com/nutshell/linqkit.aspx); например - var key = Expression.Invoke(selectKey, item.Expand()); var lambda = Expression.Lambda‹Func‹TEntity, TKey››(key.Expand(), param); Самое замечательное, что он по-прежнему будет работать и с LINQ to SQL! - person Doctor Jones; 27.09.2010

Мне нужно было то же самое, поэтому я сделал этот небольшой метод расширения:

    /// <summary>
    /// From A.B.C and D.E.F makes A.B.C.D.E.F. D must be a member of C.
    /// </summary>
    /// <param name="memberExpression1"></param>
    /// <param name="memberExpression2"></param>
    /// <returns></returns>
    public static MemberExpression JoinExpression(this Expression memberExpression1, MemberExpression memberExpression2)
    {
        var stack = new Stack<MemberInfo>();
        Expression current = memberExpression2;
        while (current.NodeType != ExpressionType.Parameter)
        {
            var memberAccess = current as MemberExpression;
            if (memberAccess != null)
            {
                current = memberAccess.Expression;
                stack.Push(memberAccess.Member);
            }
            else
            {
                throw new NotSupportedException();
            }
        }


        Expression jointMemberExpression = memberExpression1;
        foreach (var memberInfo in stack)
        {
            jointMemberExpression = Expression.MakeMemberAccess(jointMemberExpression, memberInfo);
        }

        return (MemberExpression) jointMemberExpression;
    }
person veb    schedule 29.10.2015

То, что у вас есть, это сотринг, затем проецирование и снова сортировка.

.OrderBy(x => x.Sub)
    .Select(x => x.Sub)
        .OrderBy(x => x.Key)

Ваш метод может быть таким:

public static IOrderedQueryable<TSubEntity> OrderBy<TEntity, TSubEntity, TKey>(
    this IQueryable<TEntity> source, 
    Expression<Func<TEntity, TSubEntity>> selectItem, 
    Expression<Func<TSubEntity, TKey>> selectKey)
    where TEntity : class
    where TSubEntity : class 
{
    return (IOrderedQueryable<TSubEntity>)source.
        OrderBy(selectItem).Select(selectItem).OrderBy(selectKey)
}

Это будет выполнено с помощью SQL, но, как вы могли заметить, мне пришлось изменить здесь возвращаемый тип на IOrderedQueryable‹TSubEntity>. Вы можете обойти это?

person John Leidegren    schedule 18.02.2009
comment
На самом деле это не то же самое, что объединение в одну проекцию, и (как вы сами заметили) возвращает очень разные данные... не уверены, что это особенно полезно? - person Marc Gravell; 18.02.2009
comment
Ну, это зависит от поставщика LINQ to SQL, не так ли? Тип возврата изменился, да, но я не могу сказать, будет ли это реальной проблемой, этого может быть достаточно. Прелесть LINQ заключается в возможности составлять подобные запросы. Вам не нужно ходить по дереву выражений для чего-то подобного. - person John Leidegren; 18.02.2009
comment
Спасибо за ответ, Джон, вашего подхода было достаточно в нескольких местах, но в других мне нужно было вернуться как TEntity. Для случаев, когда оба подхода будут достаточными, какие-либо мысли о различиях в производительности между ними? - person LaserJesus; 18.02.2009
comment
...продолжение; если бы вы могли просто скомпилировать выражение и, вероятно, получить что-то очень похожее по производительности в любом случае. - person LaserJesus; 18.02.2009
comment
Я почти уверен, что производительность у них одинаковая. LINQ to SQL фактически компилирует вещи за сценой, поэтому это не должно иметь большого значения. Удивительно, но написание хорошего кода LINQ to SQL намного менее тривиально, чем можно было бы подумать, особенно когда все усложняется. - person John Leidegren; 18.02.2009