Tengo un método de extensión para ordenar mis entidades, y en algunos casos tengo que ordenar una propiedad de una colección secundaria
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));
}
}
Llamado desde
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);
}
me sale el error System.ArgumentException: At least one object must implement IComparable.
He visto ejemplos que sugieren que lo hago así.
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));
}
pero eso activa una consulta SELECT n + 1
(es decir, hace que todas las entidades en dB se carguen en la memoria y luego se clasifiquen).
¿Cuál es la forma correcta de hacerlo?
Bueno, el Min
/ Max
es la forma correcta en general. Por desgracia, como habrán notado, EF Core (a partir de v2.0) todavía no se traduce así ( GroupBy
) métodos agregados y cae de nuevo a la evaluación del cliente para su procesamiento.
Como solución OrderBy[Descending]
, podría sugerir el patrón alternativo OrderBy[Descending]
+ Select
+ FirstOrDefault
que afortunadamente se traduce a 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());
Aquí está el mismo encapsulado en un método de extensión personalizado:
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);
}
}
por lo que puede utilizar simplemente:
return payments.OrderBy(p => p.History, h => h.Timestamp, sortDirection == SortDirection.Asc)