Click here to Skip to main content
15,887,746 members
Articles / Programming Languages / C# 4.0
Tip/Trick

Dynamic Sorting in LINQ, Part 2

Rate me:
Please Sign up or sign in to vote.
4.00/5 (2 votes)
11 Oct 2013CPOL2 min read 14.5K   7   1
Dynamically sort query results using LINQ expressions and reflection with sorting

Introduction

Continuing from my previous tip, "Dynamic Sorting in LINQ", we now add sorting into the mix. This is done by making use of delegates to give us a succinct, efficient manner to decide on which extension method should be called, either OrderBy or OrderByDescending.

Using the Code

In this example, we're going to begin by defining a basic data model as a structure containing the first name, last name, and middle initial of a person:

C#
public struct Person 
{
    public string FirstName { get; set; }
    public string MiddleInitial { get; set; }
    public string LastName { get; set; }

    public override string ToString() {
        return String.Format("{0}, {1} {2}", LastName, FirstName, MiddleInitial);
    }
}   

Next, we set up a data store containing a list of people:

C#
public class PeopleDataStore {
    public const bool SortAscending = true;
    public const bool SortDescending = false;

    private List<Person> m_people = null;

    public PeopleDataStore() 
    {
        m_people = new List<Person>();

        m_people.AddRange(new Person[] {
            new Person() { FirstName = "John", MiddleInitial = "Q", LastName = "Doe" },
            new Person() { FirstName = "Jane", MiddleInitial = null, LastName = "Appleton" },
            new Person() { FirstName = "Jim", MiddleInitial = "H", LastName = "Smith" },
            new Person() { FirstName = "Joe", MiddleInitial = null, LastName = "Plumber" }
        });
    }

    public List<Person> People 
    {
        get { return m_people; }
    }

    public List<Person> GetPeople(string sortPropertyName, bool sortAscending) 
    {
        if (!String.IsNullOrEmpty(sortPropertyName)) {
            Type personType = typeof(Person);
            
            if (personType.GetProperties().Any(prop => prop.Name == sortPropertyName)) {
                PropertyInfo pinfo = personType.GetProperty(sortPropertyName);
                Func<Person, object> orderByExpr = (person => pinfo.GetValue(person, null));

                Func<IEnumerable<Person>, IOrderedEnumerable<Person>> sortFunc = null;
                if (sortAscending) sortFunc = (list => list.OrderBy(orderByExpr));
                else sortFunc = (list => list.OrderByDescending(orderByExpr));

                return sortFunc(m_people).ToList();
            }
        }

        return m_people;
    }
} 

Let's take a quick jaunt through the code. First, we define a couple of Boolean constants for better legibility when calling the GetPeople method; either these constants or literal Boolean values can be used for the sortAscending parameter on the method. Next, we create a strongly-typed list of the Person type to hold our data values, then populate the list in the constructor; providing a corollary read-only property of the same type for access to the list.

The GetPeople method is then defined with two parameters: the first being the name of the public property from the Person structure used for sorting, and then a Boolean flag indicating whether the list should be sorted in ascending or descending order.

If there is no value supplied for the sortPropertyName parameter, the method simply returns the list values in their default order. If a value is supplied, we then check to see if the Person type defines a property of the same name and continue processing should this prove true. From there, we obtain a PropertyInfo instance which will be used by a Func delegate to be passed into an extension method for ordering. The Func delegate, orderByExpr, is a lambda expression which matches the delegate signature for both the OrderBy and OrderByDescending extension methods.

Next, we create another Func delegate to define which sorting method to call based on the sortAscending parameter supplied to the method call. Since we know that the OrderBy and OrderByDescending methods are extensions supplied to the IEnumerable(T) interface and our list implements this interface, we define the Func delegate to accept an input parameter of List<Person>. The output parameter is then defined as the same return type as defined by the OrderBy and OrderByDescending methods, this being an instance of IOrderedEnumerable(T). With the delegate defined, we then return the result.

Now, to put the code to use in a simple console app:

C#
public static class Program 
{
    static void Main() 
    {
        PeopleDataStore dataStore = new PeopleDataStore();
        Action<Person> consoleAction = (person => Console.WriteLine(person));

        Console.WriteLine("All people listed in default order:");
        dataStore.People.ForEach(consoleAction);

        Console.WriteLine();
        Console.WriteLine("All people listed in reverse alphabetical order by last name:");
        dataStore.GetPeople("LastName", PeopleDataStore.SortDescending)
            .ForEach(consoleAction);
    }
} 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Engineer Robert Half Technology
United States United States
Polyglot, architect, and general all-around nerd.

Comments and Discussions

 
QuestionGreat Article! A few thoughts for use in the real world? Pin
Brad Joss11-Oct-13 8:22
professionalBrad Joss11-Oct-13 8:22 
I love this idea and is it much more elegant than what I currently do, using a switch/case on sortPropertyName to set the orderby delegate function.
Some thoughts on how this might be enhanced for a real-world application:

1) Ensure the property is readable (has a getter). Basically:

Type personType = typeof(Person);
PropertyInfo pinfo = personType.GetProperty(sortPropertyName);

if !(pinfo != null && pinfo.CanRead)
throw new ArguementException(...)

2) What about fields? Maybe extend to say, if pinfo == null, then personType.GetField()…?
3) To avoid casing issues in sorting, sort on the value with normalized case, e.g.:

Func<Person, object> orderByExpr = (person => String.Format("{0}", pinfo.GetValue(person, null)).ToLowerCase());

Again, I love this article and I think it is a great idea, I just wanted to see if I can add some value for someone copying and pasting this code into a real-world application.

Very kindly,
Brad

--- EDIT ---

It occurs to me that in my casing comment I failed to account for non-string data types, so if the data type was int, "11,22,111,222" would erroneously be sorted as "11,111,22,222". That could be worked around with a pinfo.PropertyType check, but that is somewhat clumsy.

My bigger fear is that since the reflected call will be called as part of the sort algorithm, the proposed solution may not scale well. Even with relatively small lists of highly un-sorted data, the reflected call may get called hundreds or thousands of times to resolve the sort.

How about something like this:

Alter the sort request function to take in a getter function. This solves the problem I created allowing sorts to be based on native data type and extends support for case insensitive string searches.

public List<Person> GetPeople(Func<Person, object> sortProperyGetter, bool sortAscending)
{

Func<IEnumerable<Person>, IOrderedEnumerable<Person>> sortFunc = sortAscending
? (list => list.OrderBy(sortProperyGetter))
: (list => list.OrderByDescending(sortProperyGetter));

return sortFunc(m_people).ToList();
}

public void Test()
{
// This then allows customization of the property, for example case insensitive string search
var byfirstname = GetPeople(p => p.FirstName.ToLower(), true);

// And allows for proper data type sorting.
var bydate = GetPeople(p => p.Birth, false);
}

This obviously reduces the dynamic-ness of the original solution, which is a huge concession to make. I guess that is what always vexes us developers, that balancing act between things like flexibility and scalability… There may be a balance in using something like ILGenerator to create a dynamic class, eliminating the reflection, but at a massive increase in complexity?

Sorry for muddying up the post, but this problem is near to my heart and I enjoy hearing different ways to solve it.

-- modified 12-Oct-13 13:11pm.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.