2

I had another question similar answered here on NULL values being last on an order by.

Keep NULL rows last on Dynamic Linq Order By

I would also like to see if I can do the same thing with a Date column with the following criteria.

  1. All items with all end dates on the current date and up at the top, sorted by the most recent upcoming event
  2. Followed by all past events using the end date and comparing to the current date with the most recent end date passed and down. I do something similar in pure SQL at the moment.

            (CASE 
                WHEN ev.EndDate >= GETDATE() THEN 1
                ELSE 2
            END) ASC,
            (CASE
                WHEN ev.EndDate >= GETDATE() THEN ev.EndDate
                ELSE ev.StartDate
            END) ASC,
    

Example: Current Date 3/24/2017

EndDate

3/25/2017

4/15/2017

7/29/2017

3/23/2017

2/22/2016

Current Code

public static class OrderByHelper
{
    public static IOrderedQueryable<T> ThenBy<T>(this IEnumerable<T> source, string orderBy)
    {
        return source.AsQueryable().ThenBy(orderBy);
    }

    public static IOrderedQueryable<T> ThenBy<T>(this IQueryable<T> source, string orderBy)
    {
        return OrderBy(source, orderBy, false);
    }

    public static IOrderedQueryable<T> OrderBy<T>(this IEnumerable<T> source, string orderBy)
    {
        return source.AsQueryable().OrderBy(orderBy);
    }

    public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string orderBy)
    {
        return OrderBy(source, orderBy, true);
    }

    private static IOrderedQueryable<T> OrderBy<T>(IQueryable<T> source, string orderBy, bool initial)
    {
        if (string.IsNullOrWhiteSpace(orderBy))
            orderBy = "ID DESC";
        var parameter = Expression.Parameter(typeof(T), "x");
        var expression = source.Expression;
        foreach (var item in ParseOrderBy(orderBy, initial))
        {
            var order = item.PropertyName.Split('.')
                .Aggregate((Expression)parameter, Expression.PropertyOrField);
            if (!order.Type.IsValueType || Nullable.GetUnderlyingType(order.Type) != null)
            {
                var preOrder = Expression.Condition(
                        Expression.Equal(order, Expression.Constant(null, order.Type)),
                        Expression.Constant(1), Expression.Constant(0));
                expression = CallOrderBy(expression, Expression.Lambda(preOrder, parameter), item.Direction, initial);
                initial = false;
            }
            expression = CallOrderBy(expression, Expression.Lambda(order, parameter), item.Direction, initial);
            initial = false;
        }
        return (IOrderedQueryable<T>)source.Provider.CreateQuery(expression);
    }

    private static Expression CallOrderBy(Expression source, LambdaExpression selector, SortDirection direction, bool initial)
    {
        return Expression.Call(
            typeof(Queryable), GetMethodName(direction, initial),
            new Type[] { selector.Parameters[0].Type, selector.Body.Type },
            source, Expression.Quote(selector));
    }

    private static string GetMethodName(SortDirection direction, bool initial)
    {
        return direction == SortDirection.Ascending ?
            (initial ? "OrderBy" : "ThenBy") :
            (initial ? "OrderByDescending" : "ThenByDescending");
    }

    private static IEnumerable<OrderByInfo> ParseOrderBy(string orderBy, bool initial)
    {
        if (String.IsNullOrEmpty(orderBy))
            yield break;

        string[] items = orderBy.Split(',');

        foreach (string item in items)
        {
            string[] pair = item.Trim().Split(' ');

            if (pair.Length > 2)
                throw new ArgumentException(String.Format("Invalid OrderBy string '{0}'. Order By Format: Property, Property2 ASC, Property2 DESC", item));

            string prop = pair[0].Trim();

            if (String.IsNullOrEmpty(prop))
                throw new ArgumentException("Invalid Property. Order By Format: Property, Property2 ASC, Property2 DESC");

            SortDirection dir = SortDirection.Ascending;

            if (pair.Length == 2)
                dir = ("desc".Equals(pair[1].Trim(), StringComparison.OrdinalIgnoreCase) ? SortDirection.Descending : SortDirection.Ascending);

            yield return new OrderByInfo() { PropertyName = prop, Direction = dir, Initial = initial };

            initial = false;
        }

    }

    private class OrderByInfo
    {
        public string PropertyName { get; set; }
        public SortDirection Direction { get; set; }
        public bool Initial { get; set; }
    }

    private enum SortDirection
    {
        Ascending = 0,
        Descending = 1
    }
}
Community
  • 1
  • 1
Mike Flynn
  • 22,342
  • 54
  • 182
  • 341
  • Two queries, perhaps? – ErikEJ Mar 24 '17 at 13:19
  • Maybe, but how do you do a dynamic case with a string. Read the link of the previous question. – Mike Flynn Mar 24 '17 at 13:32
  • You can convert that sql query into linq query, so what's a problem? – Evk Mar 24 '17 at 13:36
  • I think it's doable, but only for a single date field. How would you specify that behavior in `orderBy` string? Currently you support "field1 ASC, field2 DESC" syntax, the code can identify `fieldX` being `DateTime` type, but then how to decide if it needs to use special sort or not? – Ivan Stoev Mar 24 '17 at 18:45
  • I would add a special field to the OrderBy class. My only issue how do you do this within the dynamic linq syntax. As you can see I can do this in SQL and even strongly typed Linq, just not this complex stuff. – Mike Flynn Mar 24 '17 at 19:09

1 Answers1

3

The way I understand, you have a DateTime property (ley call it Date), and instead of regular sort

.OrderBy(x => x.Date)

having something like

var baseDate = DateTime.Today;

you want to sort the future values first in ascending order followed by the past values in descending order.

It can be achieved in the following generic way (works in LINQ to Objects as well as EF):

.OrderBy(x => x.Date >= baseDate ? x.Date : DateTime.MaxValue)
.ThenByDescending(x => x.Date >= baseDate ? DateTime.MinValue : x.Date)

To implement that dynamically, you could insert the following inside the implementation method body loop:

if (order.Type == typeof(DateTime)) // && some other special condition
{
    var condition = Expression.GreaterThanOrEqual(
        order, Expression.Constant(DateTime.Today));
    var order1 = Expression.Condition(condition,
        order, Expression.Constant(DateTime.MaxValue));
    var order2 = Expression.Condition(condition,
        Expression.Constant(DateTime.MinValue), order);
    expression = CallOrderBy(expression,
        Expression.Lambda(order1, parameter), SortDirection.Ascending, initial);
    expression = CallOrderBy(expression,
        Expression.Lambda(order2, parameter), SortDirection.Descending, false);
    initial = false;
    continue;
}
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343