Click here to Skip to main content
Click here to Skip to main content

Dynamic List Sorting

By , 14 Feb 2006
 

Introduction

This article spawned from a post on the Danish site DotNetForum.dk. The poster asked how you would go about sorting a list of, say, person objects, in a dynamic fashion like SQL. At that point, somebody mentioned the IComparer and IComparer<> interfaces and that you probably could make some sort of generic comparer using those interfaces. And a link to an article by Peter Provost named “Generic Property Comparer Class” was also provided. The article explains how to make a generic comparer using IComparer in .NET 1.1. Peter’s solution is okay considering .NET 1.1 but it’s rather bad in a .NET 2.0 context for several reasons:

  1. It only sorts one property at a time.
  2. It only sorts in ascending order.
  3. It isn’t strongly typed.
  4. It is very slow!

Note: Point 3 and 4 are just an artifact of the fact that .NET 1.1 is lacking both generics and dynamic methods.

So this made me decide that I would try to come up with a better solution utilizing the new generics in .NET 2.0 plus some other very cool features of the framework.

A key point to keep in mind when starting a project like this is performance. I mean, nobody wants to wait 5 seconds while a list gets sorted. Another key point in this project was that it should be dynamic and should, if possible, enable the user to enter a string with a SQL “Order By” like syntax, e.g., “FirstName, Age DESC, LastName”.

The Solution

The first solution I jammed out was a comparer which only used generics and was able to sort ascending as well as descending on any number of properties. So this solution met most of the important requirements. However, even though I made this before I saw Peter's 1.1 solution, they were in fact very similar except mine used generics for specifying the type of objects whereas in Peter’s solution, you pass in the item type as a parameter. Unfortunately, this solution also had the performance problem. After a few performance tests, it was clear to me that this solution was not an option.

The problem is that you can’t really fulfill the requirements without dynamically generating some amount of specific code. At least, that was the conclusion I made after a while. Fortunately, .NET 2.0 has a new feature called “dynamic methods”.

A “dynamic method” is a method you create in-memory during runtime. I won’t dive deeper into this subject but I recommend that you check it out. Basically, what I do is I compile a strongly typed version of the “Compare” method in-memory and call it using a delegate. As you will see in the next section, this is a very powerful way of getting a strongly typed method while keeping it completely dynamic and very fast.

Performance Comparison

I’ve done a few performance tests of the most commonly used sorting methods along with Peter Provost’s solution. The results are listed below:

Diagram of performance test results

Fig. 1: Sorting times for 10 to 100000 items.

As you see in fig. 1, the dynamic comparer solution is a good way of sorting your custom entity lists. The dynamic comparer is sharing the first place with the hard-coded comparer. The hard-coded comparer seems to be starting out slowly but that is just a measuring error. This clearly shows that from the runtime's perspective, the dynamic method used to sort is pretty much like the hard-coded one except that we call it through a delegate.

On the second place, we have both the weakly and the strongly typed datasets. Both of them are following the same line which is a little steeper than the other methods. That means that they are getting slower at a faster rate compared to the other methods. Some might think that the strongly typed dataset should be sorting faster than the weakly typed dataset but think again… The strongly typed dataset is just an overloaded version of the weakly typed dataset, so in fact, it is using the same sorting methods as the weakly typed version. It is also worth mentioning that populating a dataset with 10000 items or more is much slower than populating the regular lists.

Finally, on the third place, we have the generic class comparer. As you can see, it is much slower than the other methods of sorting. Not because it is badly designed or coded, but because .NET 1.1 supports neither generics nor dynamic methods.

Usage

So how do you use this comparer? Well, it’s as easy as 1-2-3:

//Example:
string orderBy = "FirstName, LastName DESC"; // 1
DynamicComparer<Person> comparer = new DynamicComparer<Person>(orderBy); // 2
persons.Sort(comparer.Compare); // 3
  1. Declare a sort string or a SortProperty array. Note: This could be created and maintained by a custom editgrid.
  2. Declare an instance of the DynamicComparer class using the generic constructor.
  3. Call the sort method on the List<Person>.

That’s it!

So what’s going on behind the scenes? Let’s begin at line 2 of the previous example. At the time of instantiation of the DynamicComparer, the class will first parse the input string into a SortProperty array. These SortProperties will then be validated against the type we specified when instantiating the DynamicComparer, in this case, “Person”. This validation checks if the Person class is public, if it has any public properties named “FirstName” and “LastName”, if these properties are readable, and finally, if these properties are of a type that implements the IComparable interface. If any of these validations fail, the class will throw an exception. If the validation succeeds, the class will then generate a dynamic method using the specified SortPropertyies and instantiate an internal delegate pointing to the method. The “Compare” method on the DynamicComparer will then in turn be able to invoke this delegate when called. This means that the comparer is ready for use.

Note: If you want to change the sorting of an instance of the DynamicComparer, you can call the “Initialize” method on the instance and pass in a new ORDER BY string or a SortProperty array.

At the next step, we call the “Sort” method on the “persons” list passing in a reference to the comparer.Compare method.

Conclusion

The dynamic comparer seems to be a very efficient way of getting a flexible comparer while keeping it fast, and I’ve yet to see a better or even just a different solution that has the same flexibility and performance. Still, a couple of things could be improved in the comparer but mostly in the instantiation process. I leave it to you to come up with new suggestions and ideas to improve the comparer. I'm looking forward for your feedback!

License

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

About the Author

Johannes Hansen
Web Developer Jubii A/S
Denmark Denmark
Member
Johannes Hansen was born in denmark during a thunderstorm in mid-june 1980.
 
He grew up stealing time on his father's C64 and finally got his own computer in 1995.
 
He currently work as a developer/ at the danish company Jubii A/S where he spends his time developing spam filters, portals and oher user-driven solutions using C#.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralWorth Readingmemberbilal7869917 Mar '13 - 22:06 
Nice one. For understanding Lists visit:
http://codesmesh.com/dynamic-array-using-list-csharp/[^]
GeneralThanksmemberJon Cunningham26 Nov '11 - 4:43 
This saved me alot of time... I needed a dynamic sort on some lists and this code fit the bill.
 

Thanks again,
Jon Cunningham
Jon Cunningham

GeneralMy vote of 5memberSBaghestani27 Jun '10 - 1:38 
thank yoy for your code
GeneralIt Works in Vb.net as well but...memberOslec2 Dec '08 - 16:22 
I add it on my project reference and create a function
 
Public Function DynamicIListComparer(ByVal orderBy As String, ByVal pages As List(Of PageIListCollection)) As List(Of PageIListCollection)

Dim comparer As New DynamicComparer(Of PageIListCollection)(orderBy)
pages.Sort(comparer)
Return pages
 
End Function

 
It works well!..but if i changed the code to pages.Sort(comparer.Compare)
 
i got an error..
 
Error 2 Argument not specified for parameter 'x' of 'Public Function Compare(x As T, y As T) As Integer'.
 
any suggestions.. Confused | :confused:
 
By the way thanks Johannes Hansen Big Grin | :-D
AnswerRe: It Works in Vb.net as well but...memberJohannes Hansen2 Dec '08 - 20:23 
Hi Oslec
 
First of all, thank you for getting involved. Wink | ;)
 
Secondly, I'm not sure why you are experiencing that particular problem, but I do know that my original code had some bugs in it and that the implementation Marc Brooks did is much more robust. So I recommend you try his implementation out and see if that works better (which I'm sure it will). You can grab his implementation at the Dynamic Reflection Library's project site on Codeplex[^]. If you have any comments/bug reports/suggestions to Marc's implementation please post them on the Codeplex site, thanks. Smile | :)
 
Kind regards,
Johannes Hansen
frontAvenue A/S

Questionnull string problemmemberColinBashBash29 Sep '08 - 10:03 
ok. i just noticed that if i have null string intermixed in my List's it errors out. any help would be wonderful. I do love this solution that you've put out here. I hope you'll get a chance to look at this soon.
 
i tried looking into the "#region Generate IL for dynamic method," but i got lost too quickly to get anywhere with it. (at the bottom i have a separate question on templates)
 
public class Project{
  string _entryCode;
  public string EntryCode{
    get{return _entryCode;}
    set{_entryCode=value;}
  }
  public Project() {}
  public Project(string ec) {_entryCode = ec;}
}
 

//More test code to show error
List<project> projects = new List<project>();
 
projects.Add(new Project());
projects.Add(new Project());
projects.Add(new Project("asf"));
projects.Add(new Project("veev"));
projects.Add(new Project());
projects.Add(new Project("asdfe"));
projects.Add(new Project());
projects.Add(new Project());
 
projects.Sort(new DynamicComparer<project>("EntryCode"));
 
gridview1.DataSource = projects;
gridview1.DataBind();
 
</project></project></project>
 

=========================================
===========Error Message=================
=========================================
Object reference not set to an instance of an object.
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
 
Exception Details: System.NullReferenceException: Object reference not set to an instance of an object.
 
Source Error:
 
An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below.
 
Stack Trace:
 

[NullReferenceException: Object reference not set to an instance of an object.]
DynamicComparison(Project , Project ) +12
DynamicComparer.DynamicComparer`1.Compare(T x, T y) in C:\Documents and Settings\colin.cole\My Documents\Visual Studio 2005\Projects\comparer\DynamicComparer\DynamicComparer.cs:65
System.Collections.Generic.ArraySortHelper`1.SwapIfGreaterWithItems(T[] keys, TValue[] values, IComparer`1 comparer, Int32 a, Int32 b) +125
 
[InvalidOperationException: Failed to compare two elements in the array.]
System.Collections.Generic.ArraySortHelper`1.SwapIfGreaterWithItems(T[] keys, TValue[] values, IComparer`1 comparer, Int32 a, Int32 b) +433
System.Collections.Generic.ArraySortHelper`1.QuickSort(T[] keys, TValue[] values, Int32 left, Int32 right, IComparer`1 comparer) +107
System.Collections.Generic.ArraySortHelper`1.Sort(T[] keys, TValue[] values, Int32 index, Int32 length, IComparer`1 comparer) +129
System.Collections.Generic.ArraySortHelper`1.Sort(T[] items, Int32 index, Int32 length, IComparer`1 comparer) +125
System.Array.Sort(T[] array, Int32 index, Int32 length, IComparer`1 comparer) +193
System.Collections.Generic.List`1.Sort(Int32 index, Int32 count, IComparer`1 comparer) +93
 

==================================
==================================
==================================
 
is it possible to write the DynamicMethod piece in a similar way as to a generic template? (see below.) I'm assuming you can't, since it looks like it's generating assembly level code. Or is the generic implementation method just slower? I'm just curious, since i know so little about this. I am in no way trying to be critical.
 
(note the GenericTemplateImplementation can be found here, written by yet someone else who's smarter than me: http://couldbedone.blogspot.com/2007/07/implementing-itemplate-as-anonymous.html[^].
 
 
    public delegate void InstantiateTemplateDelegate(Control container);
 
    public class GenericTemplateImplementation : ITemplate {
        private InstantiateTemplateDelegate m_instantiateTemplate;
 
        public void InstantiateIn(Control container) {
            m_instantiateTemplate(container);
        }
 
        public GenericTemplateImplementation(InstantiateTemplateDelegate instantiateTemplate) {
            m_instantiateTemplate = instantiateTemplate;
        }
    }
 
            rptCategory.ItemTemplate = new GenericTemplateImplementation(
                delegate(Control cont) {
                    RepeaterItem Container = (RepeaterItem)cont;
 
                    if (Container.ItemIndex != 0)
                        Container.Controls.Add(new LiteralControl(" > "));
                    LinkButton b = new LinkButton();
                    b.ID = "lnk";
                    b.CommandName = "lnk";
                    Container.Controls.Add(b);
                });
 

AnswerRe: null string problem [modified]memberJohannes Hansen2 Oct '08 - 17:42 
Hi Colin, sorry for the late reply.
 
I'm not planning to make any fixes or changes to the current implementation of the DynamicComparer. But don't be sad, go take a look at Marc Brook's implementation of the DynamicComparer at CodePlex[^]. I'm pretty sure his implementation fixes most of the bugs in my initial prototype/example implementation.
 
As to your second question, I'm not really sure I understand it, but I'll try to answer it anyways. Wink | ;) Early on in the project I did experiment with a generic comparer but it turned out to be dreadfully slow. This is mainly because you then have to use reflection to lookup field and property types at runtime as well as casting the values before comparison. So no-go for a generic only implementation.
 
If you wanted to implement a simpler DynamicComparer it could probably be done in .NET 3.5 using LINQ expression trees. However, they might be a little slower than Marc's implementation since expression trees probably has to be more generalized, and thus has to do more checks at runtime (I haven't tried it though so I might be completely wrong). Also, Marc's implementation is extremely streamlined and is faster than compiled code in some (maybe even most) cases.
 
modified on Thursday, October 2, 2008 11:57 PM

GeneralRe: null string problemmemberColinBashBash3 Oct '08 - 3:33 
No problem. Thanks!
Questioncan this order propertys from composite classes?membercottsak10 Apr '08 - 16:49 
can this order propertys from composite classes?
like this class can:
 

/*------------------------------------------------------------------------
      This class is adapted from:
* http://www.rosscode.com/blog/index.php?title=generic_multiple_sorting_for_typed_colle&more=1&c=1&tb=1&pb=1
* and http://www.codeplex.com/NSFxSamples/SourceControl/FileView.aspx?itemId=212083&changeSetId=16449
      (thanx Joel)
     
      Created: 2008.04.10
      Author: Matthew Kocaj
------------------------------------------------------------------------*/
 
using System;
using System.Collections.Generic;
using System.Reflection;
 

public enum OrderDirection
{
      Ascending,
      Descending
}
 
/// <summary>
/// used to sort generic collections.
/// will accept n number of columns where
/// those columns are in any n number
/// of composite classes for each item
/// </summary>
/// <typeparam name="T"></typeparam>
public class EntityComparer<T> : IComparer<T>
{
      private List<OrderItem> _orderItems = new List<OrderItem>();
 

      #region Constructors
 
      public EntityComparer() { }
 
      public EntityComparer(List<OrderItem> orderItems)
      {
            this._orderItems = orderItems;
      }
 
      public EntityComparer(string sortColumn, OrderDirection orderDirection)
      {
            this._orderItems.Add(new OrderItem(sortColumn, orderDirection));
      }
 
      /// <summary>
      /// used for sort expressions in the form "Column1 DESC, Column2 ASC, ..."
      /// </summary>
      /// <param name="sortExpression"></param>
      public EntityComparer(string sortExpression)
      {
            if (sortExpression != String.Empty)
            {
                  string[] fields = sortExpression.Split(',');
                  foreach (string field in fields)
                  {
                        string[] parts = field.Trim().Split(' ');
                        if (parts[1] == "ASC")
                              this._orderItems.Add(new OrderItem(parts[0], OrderDirection.Ascending));
                        if (parts[1] == "DESC")
                              this._orderItems.Add(new OrderItem(parts[0], OrderDirection.Descending));
                  }
            }
      }
 
      #endregion
 

      public List<OrderItem> OrderItems
      {
            get { return this._orderItems; }
            set { this._orderItems = value; }
      }
 

      #region IComparer<T> Members
 
      public int Compare(T x, T y)
      {
            if (this._orderItems.Count == 0)
                  return 0;
            else
                  return this.CheckSort(0, x, y);
      }
 
      #endregion
 
      /// <summary>
      /// Recursive function to do sorting for multiple columns
      /// </summary>
      /// <param name="sortIndex">The current index of the column we're sorting at</param>
      /// <param name="x"></param>
      /// <param name="y"></param>
      /// <returns></returns>
      private int CheckSort(int sortIndex, T x, T y)
      {
            int returnValue = 0;
 
            if (this._orderItems.Count - 1 >= sortIndex)
            {
                  if (x.GetType().ToString() == y.GetType().ToString())
                  {
                        string[] propertyList = this._orderItems[sortIndex].SortColumn.Split('.');
 
                        object currentObject1 = x;
                        object currentObject2 = y;
 
                        for (int i = 0; i < propertyList.Length; i++)
                        {
                              PropertyInfo property = currentObject1.GetType().GetProperty(propertyList.GetValue(i).ToString());
 
                              if (property != null && property.CanRead)
                              {
                                    currentObject1 = this.GetPropertyValue(property, currentObject1);
                                    currentObject2 = this.GetPropertyValue(property, currentObject2);
                              }
                        }
 
                        if (currentObject1 != null && currentObject2 != null)
                        {
                              //   Assume all property types used are
                              //   Comparable
                              IComparable value1 = (IComparable)currentObject1;
                              IComparable value2 = (IComparable)currentObject2;
 
                              returnValue = value1.CompareTo(value2);
                        }
 
                        // apply Descending differences
                        if (this._orderItems[sortIndex].OrderDirection == OrderDirection.Descending)
                              returnValue = returnValue * -1;
                  }
 
                  // call again for multiple columns
                  if (returnValue == 0)
                        returnValue = this.CheckSort(sortIndex + 1, x, y);
            }
 
            return returnValue;
      }
 
      /// <summary>
      /// Get Property value
      /// </summary>
      /// <param name="property">Property</param>
      /// <param name="entity">Entity</param>
      /// <returns>Value</returns>
      private object GetPropertyValue(PropertyInfo property, object entity)
      {
            return property.GetValue(entity, null);
      }
 

      public class OrderItem
      {
            private string _sortColumn = String.Empty;
            private OrderDirection _orderDirection = 0;
 

            public OrderItem(string sortColumn, OrderDirection orderDirection)
            {
                  this._sortColumn = sortColumn;
                  this._orderDirection = orderDirection;
            }
 

            public string SortColumn
            {
                  get { return this._sortColumn; }
            }
 
            public OrderDirection OrderDirection
            {
                  get { return this._orderDirection; }
            }
      }
}
GeneralRe: can this order propertys from composite classes?memberJohannes Hansen10 Apr '08 - 21:17 
Hi,
 
Yes, the comparer that I've suggested can order any type of object, please take a look at Marc Brooks implementation for an improved version of the DynamicComparer.
 
The problem with the EntityComparer you've sent is the it will probably perform very poorly because it's using reflection, type casting, recursion and other nasty stuff in the CheckSort method. Actually I think this will perform worse than the "GenericClassComparer" from the chart in my article.
 
Kind regards,
Johannes Hansen
frontAvenue A/S

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

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130523.1 | Last Updated 14 Feb 2006
Article Copyright 2005 by Johannes Hansen
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid