views:

909

answers:

2

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.

+1  A: 

What you have there is sotring, followed by projecting and then sorting again.

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

Your method could be like this:

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

This will be executed by SQL but as you might have noticed I had to change the return type here to IOrderedQueryable<TSubEntity>. Can you work around that?

John Leidegren
That doesn't actually do the same thing as combining to the single projection, and (as you yourself observed) returns very different data... not sure this is especially helpful?
Marc Gravell
Well, that's up to the LINQ to SQL provider is it not? The return type have changed, yes, but I can't tell if that's gonna be a real issue it might be enough. The beauty with LINQ is the ability to compose queries like this. You shouldn't have to walk the expression tree for something like this.
John Leidegren
Thanks for the response John, your approach was sufficent in a few places but in others I needed the return as TEntity. For the cases where both approaches would suffice, any thoughts on performance differences between the two?
Brehtt
...continued; granted you could just compile the expression and probably get something very similar performance-wise in either case.
Brehtt
I'm pretty sure that the performance is the same of the two. LINQ to SQL actually does compile things behind the scene so it shouldn't really matter. Surprisingly so writing good LINQ to SQL code is a lot less trivial than one might think, especially when things get complex.
John Leidegren
+2  A: 

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]
Marc Gravell
Thanks for that Marc, Expression.Invoke was the magic I was looking for. I dismissed it thinking it would be exactly the same as selectItem.Compile().Invoke(...) which obviously wouldn't work. I can confirm through profiler that the SQL was created as expected too.
Brehtt
How is this different from ctx.Orders.OrderBy(o => o.Customer).OrderBy(o => o.Customer.CompanyName) or for that matter just ctx.Orders.OrderBy(o => o.Customer.CompanyName)?
John Leidegren
I trivialised my example, in actuality the expression that selects the key (CompanyName) is very complex but constant, while the part that selects the item (Customer) is simple but quite varied. So I was trying to encapsulate the constant part.
Brehtt
Any chance you could link to this "lengthy" code please Mark? I've got a solution based on this working for Linq to SQL, but can't get it to work with EF because of the invoke :-(
DoctaJonez
@DoctaJonez - try [this](http://stackoverflow.com/questions/1717444/combining-two-lamba-expressions-in-c/1720642#1720642) - if it is incomplete, let me know.
Marc Gravell
@Marc Perfect! Your Expression-FU is strong my good man! :-)
DoctaJonez
Just to add a quick note, you can also make this work with LINQ to EF by using the Expand method from LinqKit (http://www.albahari.com/nutshell/linqkit.aspx); eg - var key = Expression.Invoke(selectKey, item.Expand()); var lambda = Expression.Lambda<Func<TEntity, TKey>>(key.Expand(), param); The great thing is that it will still work with LINQ to SQL too!
DoctaJonez