Click here to Skip to main content
Email Password   helpLost your password?

Introduction

In this article, I will talk about how to sort the generic .NET list using reflection and extension methods in C#. This extension method for the generic list is able to sort a list on multiple properties. The usage is simply text-based, for example:

userlist.Sort("Firstname asc, Birthday desc"); 

Background

Extension methods enable you to "add" methods to existing types without creating a new derived type, recompiling, or otherwise modifying the original type. Extension methods are a special kind of static method, but they are called as if they were instance methods on the extended type. For client code written in C# and Visual Basic, there is no apparent difference between calling an extension method and the methods that are actually defined in a type.

The Code

The following code is the extension method itself, the sort function.

It creates a list of generic comparers (because we want to enable sorting on multiple properties).
Then it loops through the sortExpressions and creates new Comparers.

/// <summary>
/// Sorts the specified list.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="list">The list to be sorted.</param>
/// <param name="sortExpression">The sort expression; format:
/// @param1 [sortdirection], @param2 [sortdirection], @param3 [sortdirection].
/// Valid sortDirections are: asc, desc, ascending and descending.</param>
public static void Sort<T>(this List<T> list, string sortExpression)
{
    string[] sortExpressions = sortExpression.Split(new string[] { "," },
                StringSplitOptions.RemoveEmptyEntries);

    List<GenericComparer> comparers = new List<GenericComparer>();

    foreach (string sortExpress in sortExpressions)
    {
        string sortProperty = sortExpress.Trim().Split(' ')[0].Trim();
        string sortDirection = sortExpress.Trim().Split(' ')[1].Trim();

        Type type = typeof(T);
        PropertyInfo PropertyInfo = type.GetProperty(sortProperty);
        if (PropertyInfo == null)
        {
            PropertyInfo[] props = type.GetProperties();
            foreach (PropertyInfo info in props)
            {
                if (info.Name.ToString().ToLower() == sortProperty.ToLower())
                {
                    PropertyInfo = info;
                    break;
                }
            }
            if (PropertyInfo == null)
            {
                throw new Exception(String.Format("{0} is not a valid property of type:
                    \"{1}\"", sortProperty, type.Name));
            }
        }

        SortDirection SortDirection = SortDirection.Ascending;
        if (sortDirection.ToLower() == "asc" || sortDirection.ToLower() == "ascending")
        {
            SortDirection = SortDirection.Ascending;
        }
        else if (sortDirection.ToLower() == "desc" ||
                sortDirection.ToLower() == "descending")
        {
            SortDirection = SortDirection.Descending;
        }
        else
        {
            throw new Exception("Valid SortDirections are:
                    asc, ascending, desc and descending");
        }

        comparers.Add(new GenericComparer { SortDirection = SortDirection,
                PropertyInfo = PropertyInfo, comparers = comparers });
    }
    listSort(comparers[0].Compare);
}  

The following code shows the GenericComparer class which compares the two elements. If the elements are equal, it will ask the second comparer to compare the second property and so on. This part is recursive!

    public class GenericComparer
    {
        public List<GenericComparer> comparers { get; set; }
        int level = 0;

        public SortDirection SortDirection { get; set; }
        public PropertyInfo PropertyInfo { get; set; }

        public int Compare<T>(T t1, T t2)
        {
            int ret = 0;

            if (level >= comparers.Count)
                return 0;

            object t1Value = comparers[level].PropertyInfo.GetValue(t1, null);
            object t2Value = comparers[level].PropertyInfo.GetValue(t2, null);

            if (t1 == null || t1Value == null)
            {
                if (t2 == null || t2Value == null)
                {
                    ret = 0;
                }
                else
                {
                    ret = -1;
                }
            }
            else
            {
                if (t2 == null || t2Value == null)
                {
                    ret = 1;
                }
                else
                {
                    ret = ((IComparable)t1Value).CompareTo(((IComparable)t2Value));
                }
            }
            if (ret == 0)
            {
                level += 1;
                ret = Compare(t1, t2);
                level -= 1;
            }
            else
            {
                if (comparers[level].SortDirection == SortDirection.Descending)
                {
                    ret *= -1;
                }
            }
            return ret;
        }
    } 

Using the Code

The following example shows us how to use it and the results:

public class ExampleUser
{
    public DateTime Birthday { get; set; }
    public string Firstname { get; set; }
}

public class ExampleClass
{
    public static void Example()
    {
        List<ExampleUser> userlist = new List<ExampleUser>();
        userlist.Add(new ExampleUser
            { Birthday = new DateTime(1988, 10, 1), Firstname = "Bryan" });
        userlist.Add(new ExampleUser
            { Birthday = new DateTime(1986, 11, 4), Firstname = "Michael" });
        userlist.Add(new ExampleUser
            { Birthday = new DateTime(1977, 2, 2), Firstname = "Arjan" });
        userlist.Add(new ExampleUser
            { Birthday = new DateTime(1990, 6, 13), Firstname = "Pieter" });
        userlist.Add(new ExampleUser
            { Birthday = new DateTime(1988, 10, 1), Firstname = "Ruben" });
        userlist.Add(new ExampleUser
            { Birthday = new DateTime(1987, 8, 21), Firstname = "Bastiaan" });
        userlist.Add(new ExampleUser
            { Birthday = new DateTime(1987, 8, 21), Firstname = "Pieter" });

        string unsorted = "Unsorted: ";
        foreach (ExampleUser user in userlist)
        {
            unsorted += String.Format("{0} / {1} {2}", user.Birthday.ToString
                ("dd-MM-yyyy"), user.Firstname, Environment.NewLine);
        }

        userlist.Sort("Firstname asc");
        string sorted1 = "Sorted by Firstname asc: " + Environment.NewLine;
        foreach (ExampleUser user in userlist)
        {
            sorted1 += String.Format("{0} / {1} {2}", user.Birthday.ToString
                ("dd-MM-yyyy"), user.Firstname, Environment.NewLine);
        }

        userlist.Sort("Firstname asc, Birthday desc");
        string sorted2 = "Sorted by Firstname asc, Birtday desc: " + Environment.NewLine;
        foreach (ExampleUser user in userlist)
        {
            sorted2 += String.Format("{0} / {1} {2}", user.Birthday.ToString
                ("dd-MM-yyyy"), user.Firstname, Environment.NewLine);
        }

        userlist.Sort("Birthday asc, Firstname asc");
        string sorted3 = "Sorted by Birthday ascending,
                Firstname asc: " + Environment.NewLine;
        foreach (ExampleUser user in userlist)
        {
            sorted3 += String.Format("{0} / {1} {2}", user.Birthday.ToString
                ("dd-MM-yyyy"), user.Firstname, Environment.NewLine);
        }
    }
}

Results

Unsorted:
01-10-1988 / Bryan
04-11-1986 / Michael
02-02-1977 / Arjan
13-06-1990 / Pieter
01-10-1988 / Ruben
21-08-1987 / Bastiaan
21-08-1987 / Pieter

Sorted by Firstname ascending:
02-02-1977 / Arjan
21-08-1987 / Bastiaan
01-10-1988 / Bryan
04-11-1986 / Michael
21-08-1987 / Pieter
13-06-1990 / Pieter
01-10-1988 / Ruben

Sorted by Firstname ascending, Birtday descending:
02-02-1977 / Arjan
21-08-1987 / Bastiaan
01-10-1988 / Bryan
04-11-1986 / Michael
13-06-1990 / Pieter
21-08-1987 / Pieter
01-10-1988 / Ruben

Sorted by Birthday ascending, Firstname ascending:
02-02-1977 / Arjan
04-11-1986 / Michael
21-08-1987 / Bastiaan
21-08-1987 / Pieter
01-10-1988 / Bryan
01-10-1988 / Ruben
13-06-1990 / Pieter

Performance Testing

I did some testing using the following code, where list1 is being sorted the usual way and list2 is being sorted using my code.

public class TestClass
{
    public string Text { get; set; }
}

public static void Test()
{
    List<TestClass> list1 = new List<TestClass>();
    List<TestClass> list2 = new List<TestClass>();

    for (int index = 0; index < 1000; index++)
    {
        Guid item = Guid.NewGuid();
        list1.Add(new TestClass { Text = item.ToString() });
        list2.Add(new TestClass { Text = item.ToString() });
    }
    DateTime start1 = DateTime.Now;
    list1.Sort(delegate(TestClass p1, TestClass p2)
        { return p1.Text.CompareTo(p2.Text); });
    DateTime stop1 = DateTime.Now;
    TimeSpan diff1 = stop1 - start1;

    DateTime start2 = DateTime.Now;
    list2.Sort("Text asc");
    DateTime stop2 = DateTime.Now;
    TimeSpan diff2 = stop2 - start2;
}

Results

index = 1000;
diff1: TotalSeconds = 0.015625
diff2: TotalSeconds = 0.0625
4 times slower.

index = 10.000;
diff1: TotalSeconds = 0.03125
diff2: TotalSeconds = 0.46875
15 times slower.

index = 100.000;
diff1: TotalSeconds = 0.4375
diff2: TotalSeconds = 5.859375
13.4 times slower

History

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralCulture Sensitive
mifla80
23:48 12 May '09  
How can we make this a culture sensitive sort?Any ideas or suggestions are most welcome Smile
GeneralRe: Culture Sensitive
mifla80
0:20 13 May '09  
I guess changing the current thread's culture helps, any ideas are most welcome
GeneralFor Feilds
void leenux();
12:08 22 Jan '09  
I Implement your code to sort Fields

Here is Implemented Sort Method:

public static void SortByField(this List list, string sortExpression) {
string[] sortExpressions = sortExpression.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries);

List comparers = new List();

foreach (string sortExpress in sortExpressions) {
string sortField = sortExpress.Trim().Split(' ')[0].Trim();
string sortDirection = sortExpress.Trim().Split(' ')[1].Trim();

Type type = typeof(T);
FieldInfo FieldInfo = type.GetField(sortField);
if (FieldInfo == null) {
FieldInfo[] fields = type.GetFields();
foreach (FieldInfo info in fields) {
if (info.Name.ToString().ToLower() == sortField.ToLower()) {
FieldInfo = info;
break;
}
}
if (FieldInfo == null) {
throw new Exception(String.Format("{0} is not a valid field of type: \"{1}\"", sortField, type.Name));
}
}

SortDirection SortDirection = SortDirection.Ascending;
if (sortDirection.ToLower() == "asc" || sortDirection.ToLower() == "ascending") {
SortDirection = SortDirection.Ascending;
} else if (sortDirection.ToLower() == "desc" || sortDirection.ToLower() == "descending") {
SortDirection = SortDirection.Descending;
} else {
throw new Exception("Valid SortDirections are: asc, ascending, desc and descending");
}

comparers.Add(new GenericFieldComparer { SortDirection = SortDirection, FieldInfo = FieldInfo, comparers = comparers });
}
list.Sort(comparers[0].Compare);
}


Here is Implemented Field Comparer:

public class GenericFieldComparer {
public List comparers { get; set; }
int level = 0;

public SortDirection SortDirection { get; set; }
public FieldInfo FieldInfo { get; set; }

public int Compare(T t1, T t2) {
int ret = 0;

if (level >= comparers.Count)
return 0;

object t1Value = comparers[level].FieldInfo.GetValue(t1);
object t2Value = comparers[level].FieldInfo.GetValue(t2);

if (t1 == null || t1Value == null) {
if (t2 == null || t2Value == null) {
ret = 0;
} else {
ret = -1;
}
} else {
if (t2 == null || t2Value == null) {
ret = 1;
} else {
ret = ((IComparable)t1Value).CompareTo(((IComparable)t2Value));
}
}
if (ret == 0) {
level += 1;
ret = Compare(t1, t2);
level -= 1;
} else {
if (comparers[level].SortDirection == SortDirection.Descending) {
ret *= -1;
}
}
return ret;
}
}


Regards...
GeneralSortDirection [modified]
void leenux();
17:21 21 Jan '09  
You used an enum from System.Web.UI.WebControls.
But it is not necessary.
Who want to use your sample in his/her desktop application or class library, just will be surprised...

They can add following lines...

public enum SortDirection {
Ascending = 0,
Descending = 1
}


Regards...

modified on Wednesday, January 21, 2009 10:27 PM

GeneralGeneric multi-property sorting in .NET 3.0
Rich Visotcky
9:07 15 Aug '08  
Here's how to do the same multi-property sorting in .NET 3.0 with support for nested properties. The input is a dictionary of parameters to sort on and the direction to sort them by instead of a string to be parsed. The sort methods work on nested properties because the lambda expression is built from scratch based on the passed in parameters.

http://ubernostrum.wordpress.com/2008/08/01/updated-multi-property-sorting-with-linq/[^]
GeneralVery nice but...
cmschick
3:01 22 Jul '08  
This looks great. It's nice to see multi parameter sorting. Just one question. Have you tried adding times to your dates? I have had trouble in the past sorting dates where there are several dates that match but have different times down to the second. I had to first convert the dates to ticks to get the accuracy needed but I may have missed something else altogether...
GeneralRe: Very nice but...
Arjan Pot
9:54 31 Jul '08  
You are right. The easiest way is creating a new property that returns the date-part only. And then sort on that property.

Never change a working program unless you want to change it.

GeneralLooks great
Steven Berkovitz
7:11 16 Jul '08  
This looks great, but I am curious about the performance implications. Have you done any perf testing?

-Steven

GeneralRe: Looks great
raiden88
7:30 16 Jul '08  
I did, I'll give u the results as soon as possible.

--Bryan
GeneralRe: Looks great
Bryan Sumter
23:04 16 Jul '08  
The article is updated with the performance testing.


Last Updated 16 Jul 2008 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010