Ho un metodo di estensione per ordinare le mie entità, e in alcuni casi ho bisogno di ordinare su una proprietà di una collezione figlio
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));
}
}
Chiamato da
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);
}
ottengo l'errore System.ArgumentException: At least one object must implement IComparable.
Ho visto esempi che suggeriscono di farlo in questo modo
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));
}
ma questo innesca una query SELECT n + 1
(cioè, fa sì che tutte le entità in dB vengano caricate in memoria e quindi ordinate).
Qual è il modo giusto di affrontarlo?
Bene, il Min
/ Max
è il modo corretto in generale. Sfortunatamente come avete notato, EF Core (a partire dalla v2.0) continua a non tradurre bene ( GroupBy
) i metodi di aggregazione e ricade nella valutazione del cliente per elaborarli.
Come soluzione, potrei suggerire il modello alternativo OrderBy[Descending]
+ Select
+ FirstOrDefault
che per fortuna si traduce in 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());
Ecco lo stesso incapsulato in un metodo di estensione personalizzato:
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);
}
}
quindi puoi usare semplicemente:
return payments.OrderBy(p => p.History, h => h.Timestamp, sortDirection == SortDirection.Asc)