3

For example, I want to try something like this. The sorting par might have none, 1 or several sorting columns by different order. But I can't use the ThenBy method as it's only available after an OrderBy. The code below will keep resetting the order to the last item in the sorting list. I don't want to change the method signature either. Help would be greatly appreciated, thanks.

public IQueryable<Person> FilterList(string forename, List<Sorting> sorting)
{
    IQueryable<Person> query = dc.Set<Person>();

    if(!string.IsNullOrEmpty(forename)){
        query = query.Where(w=>w.Forename.Contains(forename));

    foreach(var sort in sorting)
    {
        switch(sort.By)
        {
            case "asc":
               switch(sort.Sort)
               {
                   case "forename":
                       query = query.OrderBy(o=>o.Forename);
                       break;

                   case "surname":
                       query = query.OrderBy(o=>o.Surname);
                       break;
               }
               break;

            case "desc":
               switch(sort.Sort)
               {
                   case "forename":
                       query = query.OrderByDescending(o=>o.Forename);
                       break;

                   case "surname":
                       query = query.OrderByDescending(o=>o.Surname);
                       break;
               }
               break;
        }
    }

    return query;
}

public class Sorting
{
    public string Sort{get;set;}
    public string By{get;set;}
}
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Rob White
  • 950
  • 1
  • 6
  • 16
  • It's always good to take a look at this : http://stackoverflow.com/questions/41244/dynamic-linq-orderby-on-ienumerablet when you wanna dynamic ordering (even if it won't solved directly your problem) – Raphaël Althaus Jun 05 '14 at 09:03
  • You can also look at: [Order by fields in an anonymous type](http://stackoverflow.com/questions/22429156/order-by-fields-in-an-anonymous-type) – nlips Jun 05 '14 at 18:04

5 Answers5

5

AgentFire's answer is effectively appropriate, but a bit long-winded to special-case in each situation. I'd add an extension method:

EDIT: Apparently, the code below doesn't correctly determine whether the query is already ordered; even the unordered version implements IOrderedQueryable<T>, apparently. Using source.Expression.Type to check for IOrderedQueryable<T> apparently works, but I've left this code here as the more logical approach - and as this "fix" sounds more brittle than I'd like for an answer with my name on it :)

public static IOrderedQueryable<T> AddOrdering<T, TKey>(
    this IQueryable<T> source,
    Expression<Func<T, TKey>> keySelector,
    bool descending)
{
    IOrderedQueryable<T> ordered = source as IOrderedQueryable<T>;
    // If it's not ordered yet, use OrderBy/OrderByDescending.
    if (ordered == null)
    {
        return descending ? source.OrderByDescending(keySelector)
                          : source.OrderBy(keySelector);
    }
    // Already ordered, so use ThenBy/ThenByDescending
    return descending ? ordered.ThenByDescending(keySelector)
                      : ordered.ThenBy(keySelector);
}

Then your calling code becomes:

foreach(var sort in sorting)
{
    bool descending = sort.By == "desc";
    switch (sort.Sort)
    {
        case "forename":
            query = query.AddOrdering(o => o.Forename, descending);
            break;
        case "surname":
            query = query.AddOrdering(o => o.Surname, descending);
            break;
    }
}

This way each additional sort option only adds a tiny code overhead.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • I'd choose to write an extension when I need the algorythm in at least two places. – AgentFire Jun 05 '14 at 08:49
  • @AgentFire: Well the code using it already *calls* it in two different places, within the same method. It's not clear to me whether using an `Expression>` is always appropriate - it depends on whether the provider translation uses the type argument in determining how to handle it. – Jon Skeet Jun 05 '14 at 08:56
  • Hi Jon, code isn't working for me. Inside the extension method the ordered variable is never null so always calls the ThenBy. Any thoughts? :-D – Rob White Jun 05 '14 at 14:49
  • @RobWhite: That suggests it's already ordered in the first place... which is odd, but I wouldn't expect it to do the wrong thing. What results do you get? – Jon Skeet Jun 05 '14 at 15:33
  • It breaks because it never does the first order by, though I have stopped it from breaking now using a combination of your solution and another solution that cant find anymore. It involved checking the Expression.Type of the source to see if it was IOrderedQueryable rather than using as – Rob White Jun 05 '14 at 15:41
  • @RobWhite: Eek. It's worrying if the unordered version implements `IOrderedQueryable` though. – Jon Skeet Jun 05 '14 at 15:42
  • I can't explain it either, it's definitely not ordered. I think others have had the same problem but I don;t know how they fixed it. I wondered if it was because at the stage of being ordered the query hasnt been loaded. Honestly..... I'm not sure – Rob White Jun 05 '14 at 15:45
  • lol, Jon, would you prefer to have your name against code that doesn't work despite being logical? and I wouldn't call it a fix it's a solution. All the same your help has been immensely useful. Thanks – Rob White Jun 05 '14 at 15:50
  • @Rob: I suggest you post your code as a separate answer. It's not a generally "okay" thing to edit your own code into someone else's answer unless they've suggested that you do so. Minor typos aside, of course. Given this limitation, I might actually use a different approach to start with. – Jon Skeet Jun 05 '14 at 15:55
  • Thanks, JonSkeet, and Rob. now my generic repo is more comfortable based on your answers. ASC/DESC is good now. – sebu Oct 05 '21 at 11:04
  • This doesn't work with EFCore after performing a `Where()`, as that returns an instance of `EntityQueryable` which implements `IOrderedQeuryable` despite not being ordered. The solution from @RobWhite using `source.Expression.Type` does work in this scenario. – Bouke Dec 22 '22 at 08:59
  • @Bouke: As noted in the "EDIT" part... – Jon Skeet Dec 22 '22 at 09:41
2

This is the code I'm using, tried and tested. Based on Jon Skeets solution, credit to him I merely tweaked for my purposes.

public static IOrderedQueryable<T> AddOrdering<T, TKey>(
this IQueryable<T> source,
Expression<Func<T, TKey>> keySelector,
bool descending)
{

// If it's not ordered yet, use OrderBy/OrderByDescending.
if(source.Expression.Type != typeof(IOrderedQueryable<T>))
{
    return descending ? source.OrderByDescending(keySelector)
                      : source.OrderBy(keySelector);
}
// Already ordered, so use ThenBy/ThenByDescending
return descending ? ((IOrderedQueryable<T>)source).ThenByDescending(keySelector)
                  : ((IOrderedQueryable<T>)source).ThenBy(keySelector);
}

Then your calling code becomes:

foreach(var sort in sorting)
{
bool descending = sort.By == "desc";
switch (sort.Sort)
{
    case "forename":
        query = query.AddOrdering(o => o.Forename, descending);
        break;
    case "surname":
        query = query.AddOrdering(o => o.Surname, descending);
        break;
}
}
Rob White
  • 950
  • 1
  • 6
  • 16
1

You might want to use cast:

var sortedQueryable = query as IOrderedQueryable<Person>;

if (sortedQueryable != null)
{
    query = sortedQueryable.ThenBy(o => o.Forename);
}
else
{
    query = query.OrderBy(o => o.Forename);
}

In each iteration it will do.


Also, I would change magic strings "asc" and "desc" to enum (like enum SortMode), and List<Sorting> parameter to this:

List<Tuple<Expression<Func<Person, object>>, SortMode>> parameter;

So in your foreach loop you would only need to if for SortMode and pass the Item2 of a tuple to orderers.

AgentFire
  • 8,944
  • 8
  • 43
  • 90
  • I am actually using enums but for the sake of ease my example was in strings. I am however glad you mentioned it, it's nice to know your coding the way others would. – Rob White Jun 05 '14 at 08:56
  • AgentFire cheers for this. Your answer works well with the example I've given. I'll have to put Jon answers as the marked one though because It is something I can reuse. Thanks again. +1 – Rob White Jun 05 '14 at 09:13
  • This doesn't work with EFCore after performing a `Where()`, as that returns an instance of `EntityQueryable` which implements `IOrderedQeuryable` despite not being ordered. The solution from @RobWhite using `source.Expression.Type` does work in this scenario. – Bouke Dec 22 '22 at 09:00
0

First order by a constant so you have an IOrderedQueryable<T>, then you can do all your custom ordering through the ThenBy method.

public IQueryable<Person> FilterList(string forename, List<Sorting> sorting) {
    IQueryable<Person> query = dc.Set<Person>();
    if(!string.IsNullOrEmpty(forename)){
        query = query.Where(w=>w.Forename.Contains(forename));

    var orderedQuery = query.OrderBy(x => 1);

    foreach(var sort in sorting) {
        switch(sort.By) {
            case "asc":
               switch(sort.Sort) {
                   case "forename":
                       orderedQuery = orderedQuery.ThenBy(o=>o.Forename);
                   break;
                   case "surname":
                       orderedQuery = orderedQuery.ThenBy(o=>o.Surname);
                   break;
               }
            break;
            case "desc":
               switch(sort.Sort) {
                   case "forename":
                       orderedQuery = orderedQuery.ThenByDescending(o=>o.Forename);
                   break;
                   case "surname":
                       orderedQuery = orderedQuery.ThenByDescending(o=>o.Surname);
                   break;
               }
            break;
        }
    }

    return orderedQuery;
}
Maarten
  • 22,527
  • 3
  • 47
  • 68
-1

There are a lot of options to do it. For example:

bool isSorted = false;

foreach(var sort in sorting)
{
    switch(sort.By)
    {
        case "asc":

           switch(sort.Sort)
           {
               case "forename":
                   if (!isSorted)
                   {
                       query = query .OrderBy(o => o.Forename);
                       isSorted = true;
                   }
                   else
                   {
                       query = ((IOrderedQueryable<Person>)query).ThenBy(o => o.Forename);
                   }
               ...
           }
        break;
        case "desc":
               ...
    }
}

Edit

Thanks to AgentFire for pointing out my error.

ssimeonov
  • 1,416
  • 12
  • 13