如何合并两个成员表达式树?

4

我正在尝试将以下表达式组合为单个表达式: 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方法,以支持适用于许多实体的复杂键选择表达式,尽管它们以不同的方式公开。此外,我知道一种使用深层属性的字符串表示来完成此操作的方法,但我正在尝试保持其强类型。

3个回答

4

由于这是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]

我将我的示例简单化了,实际上选择键(CompanyName)的表达式非常复杂但是不变的,而选择项目(Customer)的部分很简单但是相当多变。因此,我试图封装常数部分。 - LaserJesus
你能否提供这个“冗长”的代码链接吗,马克?我已经根据此方法在 Linq to SQL 中实现了一个解决方案,但由于invoke的原因,我无法在 EF 中实现。 - Doctor Jones
@DoctaJonez - 试试这个 - 如果不完整,请告诉我。 - Marc Gravell
@Marc 太棒了!你的表达式功夫真是厉害啊,先生! :-) - Doctor Jones
只是想添加一个快速的注释,您也可以使用LinqKit(http://www.albahari.com/nutshell/linqkit.aspx)中的Expand方法来使LINQ to EF工作; 例如- var key = Expression.Invoke(selectKey,item.Expand()); var lambda = Expression.Lambda <Func <TEntity,TKey >>(key.Expand(),param); 最棒的事情是它仍然可以与LINQ to SQL一起使用! - Doctor Jones
显示剩余2条评论

3

我也有同样的需求,因此编写了这个小扩展方法:

    /// <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;
    }

1
你那里有的是存储,接着是投影,然后再次排序。
.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>。你能解决这个问题吗?


那并不实际上与合并到单个投影做相同的事情,而且(正如你自己观察到的)返回非常不同的数据...不确定这是否特别有帮助? - Marc Gravell
那就取决于LINQ to SQL提供程序了,不是吗?返回类型已经改变了,但我无法确定这是否会成为一个真正的问题,可能已经足够了。 LINQ的美妙之处在于能够像这样组合查询。对于这样的事情,您不应该必须遍历表达式树。 - John Leidegren
谢谢回复,约翰。你的方法在一些地方是足够的,但在其他地方我需要返回TEntity。对于两种方法都可以使用的情况,你有关于它们之间性能差异的想法吗? - LaserJesus
继续;虽然在任何一种情况下,您只需编译表达式,很可能会获得非常类似的性能。 - LaserJesus
我非常确定这两者的性能是相同的。LINQ to SQL实际上在幕后编译一些东西,所以这并不重要。令人惊讶的是,编写良好的LINQ to SQL代码比人们想象的要复杂得多,尤其是当事情变得复杂时。 - John Leidegren

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