Search code examples
c#linq

LINQ create sorting expression basis property of class, support in EF Core


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");
}

Solution

  • 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();