When using LINQ OrderBy
to order by property that is a class type, I would like the collection to be ordered by the values of one of the properties of the class type. How do I control this, while being able to be translated into SQL query by EF Core?
public class FooDate
{
public DateTime DateTime {get; set;}
public bool UserSetValue {get; set;}
// other properties
}
public class Bar
{
public FooDate Date {get; set;}
// other properties
}
When I would order by the property that is of class type:
List<Bar> sortedInstances = existingList.OrderBy(x => x.Date).ToList();
I would like that the collection is ordered by values of FooDate.DateTime
.
Update:
I tried to to implement IComparable
but it did not translate to SQL in EF Core.
public int CompareTo(object obj)
{
if (obj == null) return 1;
FooDate other = obj as FooDate;
if (other != null)
return this.DateTime.CompareTo(other.DateTime);
else
throw new ArgumentException("Object is not a FooDate");
}
So to start with, make an interface that indicates the type has an expression that it can be compared using:
public interface IComparableExpression<TObject>
{
static abstract Expression<Func<TObject, object>> Comparer { get; }
}
And implement it on your class:
public class FooDate : IComparableExpression<FooDate>
{
// other properties
public static Expression<Func<FooDate, object>> Comparer => fooDate => fooDate.DateTime;
}
Then make your own versions of the OrderBy
that constrains the key to having its own comparer:
public static IQueryable<TSource> OrderByComparable<TSource, TKey>(this IQueryable<TSource> query,
Expression<Func<TSource, TKey>> selector)
where TKey : IComparableExpression<TKey>
{
return query.OrderBy(selector.Compose(TKey.Comparer));
}
While you're at it, you probably also want ThenBy
and Descending
versions of each:
public static IQueryable<TSource> OrderByDescendingComparable<TSource, TKey>(this IQueryable<TSource> query,
Expression<Func<TSource, TKey>> selector)
where TKey : IComparableExpression<TKey>
{
return query.OrderByDescending(selector.Compose(TKey.Comparer));
}
public static IQueryable<TSource> ThenByComparable<TSource, TKey>(this IOrderedQueryable<TSource> query,
Expression<Func<TSource, TKey>> selector)
where TKey : IComparableExpression<TKey>
{
return query.ThenBy(selector.Compose(TKey.Comparer));
}
public static IQueryable<TSource> ThenByDescendingComparable<TSource, TKey>(this IOrderedQueryable<TSource> query,
Expression<Func<TSource, TKey>> selector)
where TKey : IComparableExpression<TKey>
{
return query.ThenByDescending(selector.Compose(TKey.Comparer));
}
This relies on the following method to compose two expressions:
public static Expression<Func<TSource, TResult>> Compose<TSource, TIntermediate, TResult>(
this Expression<Func<TSource, TIntermediate>> first,
Expression<Func<TIntermediate, TResult>> second)
{
var param = Expression.Parameter(typeof(TSource));
var intermediateValue = first.Body.ReplaceParameter(first.Parameters[0], param);
var body = second.Body.ReplaceParameter(second.Parameters[0], intermediateValue);
return Expression.Lambda<Func<TSource, TResult>>(body, param);
}
public static Expression ReplaceParameter(this Expression expression,
ParameterExpression toReplace,
Expression newExpression)
{
return new ParameterReplaceVisitor(toReplace, newExpression)
.Visit(expression);
}
public class ParameterReplaceVisitor : ExpressionVisitor
{
private ParameterExpression from;
private Expression to;
public ParameterReplaceVisitor(ParameterExpression from, Expression to)
{
this.from = from;
this.to = to;
}
protected override Expression VisitParameter(ParameterExpression node)
{
return node == from ? to : base.Visit(node);
}
}
And now you have the tools for your (mostly) original desired code:
List<Bar> sortedInstances = existingList.OrderByComparable(x => x.Date).ToList();
Which will, behind the scenes, construct something equivalent to:
List<Bar> sortedInstances = existingList.OrderBy(x => x.Date.DateTime).ToList();