Reputation: 5918
I have an extension method to sort my entities, and in some cases i need to sort on a property of a child collection
public static IQueryable<Payment> SetSort(this IQueryable<Payment> payments, string sortProperty, string direction)
if (string.Equals(sortProperty, PaymentSortProperties.TimeStamp, StringComparison.CurrentCultureIgnoreCase))
{
return sortDirection == SortDirection.Asc ? payments.OrderBy(x => x.History.OrderBy(h=> h.Timestamp)) : payments.OrderByDescending(x => x.History.OrderByDescending(h => h.Timestamp));
}
}
Called from
public async Task<IPagedList<Payment>> Get(int pageNumber, int pageSize, string sortProperty, string direction, string searchString)
{
var result = _data.Payments
.Include(x => x.History)
.ThenInclude(x=>x.Status)
.Filter(searchString)
.SetSort(sortProperty, direction);
return await result.ToPagedListAsync(pageNumber, pageSize);
}
i get the error System.ArgumentException: At least one object must implement IComparable.
I've seen examples that suggest i do it like this
if (string.Equals(sortProperty, PaymentSortProperties.TimeStamp, StringComparison.CurrentCultureIgnoreCase))
{
return sortDirection == SortDirection.Asc ?
payments.OrderBy(x => x.History.Min(h=> h.Timestamp))
: payments.OrderByDescending(x => x.History.Max(h => h.Timestamp));
}
but that triggers a SELECT n + 1
query (ie causing all entities in dB to be loaded into memory, and then sorted).
What is the right way of going about it?
Upvotes: 1
Views: 2250
Reputation: 205819
Well, the Min
/ Max
is the correct way in general. Unfortunately as you have noticed, EF Core (as of v2.0) still doesn't translate well (GroupBy
) aggregate methods and falls back to client evaluation for processing them.
As a workaround, I could suggest the alternative pattern OrderBy[Descending]
+ Select
+ FirstOrDefault
which luckily translates to SQL:
return sortDirection == SortDirection.Asc ?
payments.OrderBy(p => p.History.OrderBy(h => h.Timestamp).Select(h => h.Timestamp).FirstOrDefault()) :
payments.OrderByDescending(x => x.History.OrderByDescending(h => h.Timestamp).Select(h => h.Timestamp).FirstOrDefault());
Here is the same encapsulated in a custom extension method:
public static class QueryableExtensions
{
public static IOrderedQueryable<TOuter> OrderBy<TOuter, TInner, TKey>(
this IQueryable<TOuter> source,
Expression<Func<TOuter, IEnumerable<TInner>>> innerCollectionSelector,
Expression<Func<TInner, TKey>> keySelector,
bool ascending)
{
return source.OrderBy(innerCollectionSelector, keySelector, ascending, false);
}
public static IOrderedQueryable<TOuter> ThenBy<TOuter, TInner, TKey>(
this IOrderedQueryable<TOuter> source,
Expression<Func<TOuter, IEnumerable<TInner>>> innerCollectionSelector,
Expression<Func<TInner, TKey>> keySelector,
bool ascending)
{
return source.OrderBy(innerCollectionSelector, keySelector, ascending, true);
}
static IOrderedQueryable<TOuter> OrderBy<TOuter, TInner, TKey>(
this IQueryable<TOuter> source,
Expression<Func<TOuter, IEnumerable<TInner>>> innerCollectionSelector,
Expression<Func<TInner, TKey>> innerKeySelector,
bool ascending, bool concat)
{
var parameter = innerCollectionSelector.Parameters[0];
var innerOrderByMethod = ascending ? "OrderBy" : "OrderByDescending";
var innerOrderByCall = Expression.Call(
typeof(Enumerable), innerOrderByMethod, new[] { typeof(TInner), typeof(TKey) },
innerCollectionSelector.Body, innerKeySelector);
var innerSelectCall = Expression.Call(
typeof(Enumerable), "Select", new[] { typeof(TInner), typeof(TKey) },
innerOrderByCall, innerKeySelector);
var innerFirstOrDefaultCall = Expression.Call(
typeof(Enumerable), "FirstOrDefault", new[] { typeof(TKey) },
innerSelectCall);
var outerKeySelector = Expression.Lambda(innerFirstOrDefaultCall, parameter);
var outerOrderByMethod = concat ? ascending ? "ThenBy" : "ThenByDescending" : innerOrderByMethod;
var outerOrderByCall = Expression.Call(
typeof(Queryable), outerOrderByMethod, new[] { typeof(TOuter), typeof(TKey) },
source.Expression, Expression.Quote(outerKeySelector));
return (IOrderedQueryable<TOuter>)source.Provider.CreateQuery(outerOrderByCall);
}
}
so you can use simply:
return payments.OrderBy(p => p.History, h => h.Timestamp, sortDirection == SortDirection.Asc)
Upvotes: 3