Ich habe eine Erweiterungsmethode, um meine Entitäten zu sortieren, und in einigen Fällen muss ich nach einer Eigenschaft einer Kindsammlung sortieren
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));
}
}
Genannt von
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);
}
Ich bekomme den Fehler System.ArgumentException: At least one object must implement IComparable.
Ich habe Beispiele gesehen, die darauf hindeuten, dass ich es so mache
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));
}
aber das löst eine SELECT n + 1
Abfrage aus (dh alle Objekte in dB werden in den Speicher geladen und dann sortiert).
Was ist der richtige Weg?
Nun, der Min
/ Max
ist der richtige Weg im Allgemeinen. Leider, wie Sie bemerkt haben, übersetzt EF Core (ab v2.0) immer noch nicht gut ( GroupBy
) aggregierte Methoden und GroupBy
auf die Client-Auswertung zurück, um sie zu verarbeiten.
Als Workaround könnte ich das alternative Muster OrderBy[Descending]
+ Select
+ FirstOrDefault
das glücklicherweise in SQL übersetzt wird:
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());
Hier ist das Gleiche eingekapselt in einer benutzerdefinierten Erweiterungsmethode:
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 können Sie einfach verwenden:
return payments.OrderBy(p => p.History, h => h.Timestamp, sortDirection == SortDirection.Asc)