How do I combine two Member Expression Trees?

.net c# expression-trees lambda linq-to-sql

Question

I'm trying to combine the following expressions into a single expression: item => item.sub, sub => sub.key to become item => item.sub.key. I need to do this so I can create an OrderBy method which takes the item selector separately to the key selector. This can be accomplished using one of the overloads on OrderBy and providing an IComparer<T>, but it won't translate to SQL.

Following is a method signature to further clarify what I am trying to achive, along with an implementation that doesn't work, but should illustrate the point.

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

which would be called as:

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

Is this possible? Is there a better way? The reason I want an OrderBy method that works this way is to support a complex key selection expression that applies to many entities, though they are exposed in different ways. Also, I'm aware of a way to do this using String representations of deep properties, but I'm trying to keep it strongly typed.

Accepted Answer

Since this is LINQ-to-SQL, you can usually use Expression.Invoke to bring a sub-expression into play. I'll see if I can come up with an example (update: done). Note, however, that EF doesn't support this - you'd need to rebuild the expression from scratch. I have some code to do this, but it is quite lengthy...

The expression code (using Invoke) is quite simple:

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

Here's example usage on Northwind:

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

With TSQL (reformatted to fit):

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]

Popular Answer

I needed the same so made this small extension method:

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



Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why