1

I have list of data and user defined sort list and I need to sort my source list based on total numbers defined in the sort list object. It should be dynamic so that the user can freely create his own sort list preference. In my sample code I used Person class. The class may have more properties in the future that's why I want that my sort expression is dynamic too. I used PropertyName to convey a lookup for property. In my example below I have list of person and I have list of sort preference. In my first example I want to sort the person list by Name ascending, then by Age descending. Can someone help me have a LINQ extension? I saw an example in this post Dynamic Linq Order By Variable

The scenario in that post is quite similar to mine except this one is using fixed properties. What I want to achieve is dynamic like the following.

  1. Sort expression is dynamic that is I need to look up for property name that has matching in my sort expression. If any found sort based on sort direction.
  2. Sort execution should be based on how many sort items are defined in the sort list. For example loop through the sort list and do OrderBy (if ascending), OrderByDescending (if descending), ThenBy, ThenBy so on and so fort. For example I have 2 sort order then the source list should be ordered by then by. If I have 5 then the list should sorted in 1 "OrderBy (desc or asc)" and 4 "ThenBy (desc or asc)". The chain should not be broken that for example given 4 sort order and all are ascending it will become persons.OrderBy(prop1).ThenBy(prop2).ThenBy(prop3).ThenBy(prop4).

C# Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SortDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            var persons = new List<Person>();
            persons.Add(new Person { Name="George", Age=25, Group="A", State="LA"});
            persons.Add(new Person { Name = "Anna", Age = 20, Group = "B", State = "CA" });
            persons.Add(new Person { Name = "Xenna", Age = 30, Group = "A", State = "DC" });
            persons.Add(new Person { Name = "Sam", Age = 40, Group = "C", State = "IL" });
            persons.Add(new Person { Name = "Elise", Age = 21, Group = "B", State = "LA" });
            persons.Add(new Person { Name = "Josh", Age = 29, Group = "C", State = "MI" });
            persons.Add(new Person { Name = "Mike", Age = 34, Group = "A", State = "NY" });
            persons.Add(new Person { Name = "Bernard", Age = 27, Group = "C", State = "WY" });

            var sorts = new List<Sort>();
            sorts.Add(new Sort { PropertyName = "Age", SortOrder = 2, Direction = "Descending" });
            sorts.Add(new Sort { PropertyName="Name", SortOrder=1, Direction="Ascending"});

            //sort by two properties
            foreach(var sort in sorts.OrderBy(x=>x.SortOrder))
            {
                //OrderBy if sort direction is Ascending
                //OrderByDescending if sort direction is Descending
                var sortedPersons = persons.OrderBy(x=>PropertyName==sort.PropertyName);
                //expected results
                //order persons by Name ascending
                //then by Age Descending
            }

            //another example
            var sorts1 = new List<Sort>();
            sorts1.Add(new Sort { PropertyName = "Name", SortOrder = 4, Direction = "Descending" });
            sorts1.Add(new Sort { PropertyName = "Age", SortOrder = 1, Direction = "Ascending" });
            sorts1.Add(new Sort { PropertyName = "State", SortOrder = 3, Direction = "Ascending" });
            sorts1.Add(new Sort { PropertyName = "Group", SortOrder = 2, Direction = "Ascending" });
            //sort by four properties
            foreach (var sort in sorts1.OrderBy(x => x.SortOrder))
            {
                //OrderBy if sort direction is Ascending
                //OrderByDescending if sort direction is Descending
                var sortedPersons1 = persons.OrderBy(x => PropertyName == sort.PropertyName);
                //expected results
                //order persons by Age Ascending
                //then by Group Ascending
                //then by State Ascending
                //then by Name Descending
            }
        }
    }
    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public string Group { get; set; }
        public string State { get; set; }
    }
    public class Sort
    {
        public string PropertyName { get; set; }
        public int SortOrder { get; set; }
        public string Direction { get; set; }
    }
}
  • 1
    Does this answer your question? https://stackoverflow.com/a/65850085/10646316 IEnumerable can be converted to IQueryable by AsQueryable method. – Svyatoslav Danyliv Mar 23 '21 at 08:16
  • Are you looking for an ability to sort in-memory list or it should support Linq to SQL, for example, EF? – dantey89 Mar 23 '21 at 09:24

2 Answers2

1

Try this to sort an in-memory list.

List<Person> SortDynamically(IEnumerable<Person> persons, IList<Sort> sorts)
{
    // this line to get an IOrderedEnumerable<T> so that we can chain ThenBy(s)
    var sortedPersons = persons.OrderBy(x => 1);
    foreach(var sort in sorts.OrderBy(x => x.SortOrder))
    {
        sortedPersons = sort.Direction switch
        {
            "Ascending" => sortedPersons
                .ThenBy(x => x.GetType().GetProperty(sort.PropertyName)?.GetValue(x, null)),
            "Descending" => sortedPersons
                .ThenByDescending(x => x.GetType().GetProperty(sort.PropertyName)?.GetValue(x, null)),
            _ => throw new ArgumentException("Sort Direction must be Ascending or Descending")
        };
    }
    return sortedPersons.ToList();
}

Alternatively, if you do not like the persons.OrderBy(x => 1) trick, you could the call OrderBy and ThenBy separately.

List<Person> SortDynamicallyAlt(IEnumerable<Person> persons, IList<Sort> sorts)
{
    if(sorts.Count == 0)
    {
        return persons.ToList();
    }
    var firstSort = sorts.OrderBy(x => x.SortOrder).First();
    
    var sortedPersonsAlt = firstSort.Direction switch
    {
        "Ascending" => persons
            .OrderBy(x => x.GetType().GetProperty(firstSort.PropertyName)?.GetValue(x, null)),
        "Descending" => persons
            .OrderByDescending(x => x.GetType().GetProperty(firstSort.PropertyName)?.GetValue(x, null)),
        _=> throw new ArgumentException("Sort Direction must be Ascending or Descending")
    };
    
    foreach(var sort in sorts.OrderBy(x => x.SortOrder).Skip(1))
    {
        sortedPersonsAlt = sort.Direction switch
        {
            "Ascending" => sortedPersonsAlt
                .ThenBy(x => x.GetType().GetProperty(sort.PropertyName)?.GetValue(x, null)),
            "Descending" => sortedPersonsAlt
                .ThenByDescending(x => x.GetType().GetProperty(sort.PropertyName)?.GetValue(x, null)),
            _=> throw new ArgumentException("sort Direction must be Ascending or Descending")
        };
    }
    return sortedPersonsAlt.ToList();
}
michael yin
  • 219
  • 3
  • 5
  • @Svyatoslav Danyliv 's answer is better, because it builds an expression instead of using reflection to get property value every time. The performance improvement could be meaningful because the difference is multiplied by the number of elements in the list. – michael yin Mar 23 '21 at 23:33
  • The first example using "var sortedPersons = persons.OrderBy(x => 1);" worked perfectly in my case. I was able to customize the code to use our own "Sort" class and work with List. Thanks for the great example. – Ken Aug 31 '23 at 21:27
1

This is my original answer which has IQueryable implementation - good for LINQ to Entities queries, but also can be performant for LINQ to Objects.

In your case it can be used in this way:

var sorts = new List<Sort>();
sorts.Add(new Sort { PropertyName = "Age",  SortOrder = 2, Direction = "Descending" });
sorts.Add(new Sort { PropertyName = "Name", SortOrder = 1, Direction = "Ascending"});

var orderByInfo = sorts.OrderBy(s => s.SortOrder)
   .Select(s => Tuple.Create(s.PropertyName, s.Direction == "Descending"));

var sordedPersons = persons.AsQueryable().ApplyOrderBy(orderByInfo).ToList();

Svyatoslav Danyliv
  • 21,911
  • 3
  • 16
  • 32
  • Note: List can't be figured out by C# Intellisense. What is the namespace you're using for the Sort class? – Ken Aug 31 '23 at 21:23