Mladen Prajdić Blog

Blog about stuff and things and stuff. Mostly about SQL server and .Net

Sorter class v2 - A generic comparer/sorter class

In my previous post i showed how a generic comparer class in .net C# 2.0 can be made with the use of reflection.

In my constant search to optimize code and make it better i've stumbled upon this article that explaines how IL code generation can be use in this situation. In .net 2.0 emiting IL was made very simple with the indroduction of a DynamicMethod class.

The speed of this approach is comparable to execution time of compiled code.

Result when sorting 10.000  TestClass objects 10 times was 2±0.2 seconds with DynamicMethod and 50±2 seconds with direct use of reflection on my computer. So that's a lot of speed gain :))

The class performs even a bit faster if we know the type of the property we're sorting on but i've chosen to make the class general (property data type unknown).

I didn't include much error handling and sorting on fields which can also be achived with DynamicMethod with FieldInfo class

Sample use:

Random r = new Random();
List<TestClass> testClassList = new List<TestClass>();
for (int i = 0; i < 10000; i++)
{
    testClassList.Add(new TestClass("strType" + r.Next(0, 1000).ToString(), r.Next(15, 50), (decimal)r.NextDouble()));
}
// sorting on properties on their default comparer
Sorter<TestClass> sort = new Sorter<TestClass>(testClassList, "StringType DESC, DecimalType Desc, IntType asc");
List<TestClass> sorted = sort.GetSortedList();

// if we want to sort some properties on their ToString() representation comparer // and not on the default comparer List<ObjectSortProperty> sortProps = new List<ObjectSortProperty>(); sortProps.Add(new ObjectSortProperty("StringType", SortOrderEnum.DESC)); sortProps.Add(new ObjectSortProperty("DecimalType", SortOrderEnum.DESC, SortOnEnum.String)); sortProps.Add(new ObjectSortProperty("IntType")); Sorter<TestClass> sort2 = new Sorter<TestClass>(testClassList, sortProps); List<TestClass> sorted2 = sort2.GetSortedList();

This is the code for the Sorter class and for the TestClass:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
namespace GenericSorter
{
    #region Enums
    /// <summary>
    /// Enum that specifies the sort order
    /// </summary>
    public enum SortOrderEnum { ASC = 1, DESC = -1 };
    /// <summary>
    /// Enum that specifies the sort order type
    /// Default = Compare using types default comparer
    /// String = Compare ToString() values
    /// </summary>
    public enum SortOnEnum { Default = 0, String = 1 };
    #endregion
    #region Sorter class
    /// <summary>
    /// Sorter class
    /// </summary>
    /// <typeparam name="TObject">Generic type. Can be any object.</typeparam>
    public class Sorter<TObject>
    {
        #region variables
        private List<TObject> _OriginalList;
        private List<ObjectSortProperty> _ObjectSortOnProperties; 
        #endregion        
    <span class="preproc">#region</span> Constructors
    <span class="rem">/// &lt;summary&gt;</span>
    <span class="rem">/// Sort collection of objects</span>
    <span class="rem">/// &lt;/summary&gt;</span>
    <span class="rem">/// &lt;param name=&#34;itemCollection&#34;&gt;Object collection&lt;/param&gt;</span>
    <span class="rem">/// &lt;param name=&#34;orderBy&#34;&gt;SQL style Order by. Property names are CASE SENSITIVE. Properties are sorted with property datatype default comparer&lt;/param&gt;</span>
    <span class="kwrd">public</span> Sorter(IEnumerable itemCollection, <span class="kwrd">string</span> orderBy)
        : <span class="kwrd">this</span>(itemCollection, ParseOrderBy(orderBy))
    { }        
    
    <span class="rem">/// &lt;summary&gt;</span>
    <span class="rem">/// Sort collection of objects</span>
    <span class="rem">/// &lt;/summary&gt;</span>
    <span class="rem">/// &lt;param name=&#34;itemCollection&#34;&gt;Object collection&lt;/param&gt;</span>
    <span class="rem">/// &lt;param name=&#34;objectSortProperties&#34;&gt;List of objects&#39; sort properties to sort on&lt;/param&gt;</span>
    <span class="kwrd">public</span> Sorter(IEnumerable itemCollection, IEnumerable&lt;ObjectSortProperty&gt; objectSortProperties)
    {
        _OriginalList = <span class="kwrd">new</span> List&lt;TObject&gt;();
        _ObjectSortOnProperties = <span class="kwrd">new</span> List&lt;ObjectSortProperty&gt;();
        SetNewCollection(itemCollection, objectSortProperties);
    }
    <span class="preproc">#endregion</span>
    <span class="preproc">#region</span> Static <span class="kwrd">private</span> methods
    <span class="kwrd">private</span> <span class="kwrd">static</span> List&lt;ObjectSortProperty&gt; ParseOrderBy(<span class="kwrd">string</span> orderBy)
    {
        <span class="kwrd">if</span> (orderBy == <span class="kwrd">null</span> || orderBy.Length == 0)
            <span class="kwrd">return</span> <span class="kwrd">new</span> List&lt;ObjectSortProperty&gt;(0);
        <span class="kwrd">string</span>[] props = orderBy.Split(<span class="kwrd">new</span> <span class="kwrd">char</span>[] { <span class="str">&#39;,&#39;</span> } , StringSplitOptions.RemoveEmptyEntries);
        List&lt;ObjectSortProperty&gt; properties = <span class="kwrd">new</span> List&lt;ObjectSortProperty&gt;(props.Length);
        <span class="kwrd">for</span> (<span class="kwrd">int</span> i = 0; i &lt; props.Length; i++)
        {
            <span class="kwrd">string</span> prop = props[i].Trim();
            <span class="kwrd">string</span>[] words = prop.Split(<span class="kwrd">new</span> <span class="kwrd">char</span>[] { <span class="str">&#39; &#39;</span> }, StringSplitOptions.RemoveEmptyEntries);
            <span class="kwrd">if</span> (words.Length &gt; 2)
                <span class="kwrd">throw</span> <span class="kwrd">new</span> ArgumentException(<span class="str">&#34;Too many words in sort expression&#34;</span>, <span class="str">&#34;orderBy&#34;</span>);
            <span class="kwrd">if</span> (words.Length == 2 &amp;&amp; (!words[1].Equals(<span class="str">&#34;DESC&#34;</span>, StringComparison.OrdinalIgnoreCase) &amp;&amp; !words[1].Equals(<span class="str">&#34;ASC&#34;</span>, StringComparison.OrdinalIgnoreCase)))
                <span class="kwrd">throw</span> <span class="kwrd">new</span> ArgumentException(<span class="str">&#34;The second word in the sort expression must be ASC or DESC&#34;</span>, <span class="str">&#34;orderBy&#34;</span>);
            <span class="kwrd">if</span> (words.Length == 1 || words[1].Equals(<span class="str">&#34;ASC&#34;</span>, StringComparison.OrdinalIgnoreCase))
                properties.Add(<span class="kwrd">new</span> ObjectSortProperty(words[0], SortOrderEnum.ASC));
            <span class="kwrd">else</span>
                properties.Add(<span class="kwrd">new</span> ObjectSortProperty(words[0], SortOrderEnum.DESC));
        }
        <span class="kwrd">return</span> properties;
    }
    <span class="preproc">#endregion</span>
    <span class="preproc">#region</span> Public methods
    <span class="rem">/// &lt;summary&gt;</span>
    <span class="rem">/// Gets the sorted list of items</span>
    <span class="rem">/// &lt;/summary&gt;</span>
    <span class="rem">/// &lt;returns&gt;Generic list of sorted objects&lt;/returns&gt;</span>
    <span class="kwrd">public</span> List&lt;TObject&gt; GetSortedList()
    {
        <span class="kwrd">return</span> _OriginalList;
    }
    <span class="rem">/// &lt;summary&gt;</span>
    <span class="rem">/// Gets the sorted array of items</span>
    <span class="rem">/// &lt;/summary&gt;</span>
    <span class="rem">/// &lt;returns&gt;Generic array of sorted objects&lt;/returns&gt;</span>
    <span class="kwrd">public</span> TObject[] GetSortedArray()
    {
        <span class="kwrd">return</span> _OriginalList.ToArray();
    }
    <span class="rem">/// &lt;summary&gt;</span>
    <span class="rem">/// Set new collection to sort on already given properties</span>
    <span class="rem">/// &lt;/summary&gt;</span>
    <span class="rem">/// &lt;param name=&#34;newCollection&#34;&gt;Object implemeting IEnumerable to sort&lt;/param&gt;</span>
    <span class="kwrd">public</span> <span class="kwrd">void</span> SetNewCollection(IEnumerable newCollection)
    {
        AddObjectsToList(newCollection);
    }
    <span class="rem">/// &lt;summary&gt;</span>
    <span class="rem">/// Set new collection to sort on new properties</span>
    <span class="rem">/// &lt;/summary&gt;</span>
    <span class="rem">/// &lt;param name=&#34;newCollection&#34;&gt;Object implemeting IEnumerable to sort&lt;/param&gt;</span>
    <span class="rem">/// &lt;param name=&#34;objectSortProperty&#34;&gt;&lt;/param&gt;</span>
    <span class="kwrd">public</span> <span class="kwrd">void</span> SetNewCollection(IEnumerable newCollection, IEnumerable&lt;ObjectSortProperty&gt; objectSortProperties)
    {
        AddObjectSortPropertiesToList(objectSortProperties);
        AddObjectsToList(newCollection);
    }
    <span class="kwrd">public</span> <span class="kwrd">void</span> SetNewSortProperties(IEnumerable&lt;ObjectSortProperty&gt; objectSortProperties)
    {
        AddObjectSortPropertiesToList(objectSortProperties);
        SortList();
    }
    <span class="kwrd">private</span> <span class="kwrd">void</span> AddObjectsToList(IEnumerable newCollection)
    {
        _OriginalList.Clear();
        <span class="kwrd">foreach</span> (TObject item <span class="kwrd">in</span> newCollection)
            _OriginalList.Add(item);
        SortList();
    }
    <span class="kwrd">private</span> <span class="kwrd">void</span> AddObjectSortPropertiesToList(IEnumerable&lt;ObjectSortProperty&gt; objectSortProperties)
    {
        _ObjectSortOnProperties.Clear();
        <span class="kwrd">foreach</span> (ObjectSortProperty property <span class="kwrd">in</span> objectSortProperties)
            _ObjectSortOnProperties.Add(property);
    }
    <span class="preproc">#endregion</span>
    <span class="preproc">#region</span> Private Methods
    <span class="kwrd">private</span> <span class="kwrd">void</span> SortList()
    {        
        <span class="rem">// if one it means we&#39;re sorting on one property</span>
        <span class="kwrd">if</span> (_ObjectSortOnProperties.Count == 1)
        {
            <span class="rem">// sort based on the SortOn property</span>
            <span class="kwrd">if</span> (_ObjectSortOnProperties[0].SortOn == SortOnEnum.String)
                _OriginalList.Sort(CompareSinglePropertyString);
            <span class="kwrd">else</span>
                _OriginalList.Sort(CompareSinglePropertyDefault);
        }
        <span class="rem">// ... else do complex multi property sort</span>
        <span class="kwrd">else</span>
        {
            _OriginalList.Sort(CompareValuesMultiProperty);
        }
    }
    <span class="preproc">#region</span> Comparer stuff
    <span class="rem">/// &lt;summary&gt;</span>
    <span class="rem">/// Compares the current objects properties with another object properties of the same type. </span>
    <span class="rem">/// &lt;/summary&gt;</span>
    <span class="rem">/// &lt;param name=&#34;o1&#34;&gt;First value to compare&lt;/param&gt;</span>
    <span class="rem">/// &lt;param name=&#34;o2&#34;&gt;Second value to compare&lt;/param&gt;</span>
    <span class="rem">/// &lt;returns&gt;Returns the IComparable.CompareTo result&lt;/returns&gt;</span>
    <span class="kwrd">private</span> <span class="kwrd">int</span> CompareSinglePropertyDefault(TObject o1, TObject o2)
    {            
        PropertyValueDelegate&lt;TObject&gt; GetPropValue = GetPropertyValueDelegate&lt;TObject&gt;(_ObjectSortOnProperties[0].PropertyName);
        <span class="kwrd">object</span> obj1 = GetPropValue(o1);
        <span class="kwrd">object</span> obj2 = GetPropValue(o2);
        <span class="rem">// if sort order is decsending then reverse the result.</span>
        <span class="kwrd">return</span> _ObjectSortOnProperties[0].SortOrderMultiplier * ((IComparable)obj1).CompareTo(obj2);
    }
    <span class="kwrd">private</span> <span class="kwrd">int</span> CompareSinglePropertyString(TObject o1, TObject o2)
    {
        PropertyValueDelegate&lt;TObject&gt; GetPropValue = GetPropertyValueDelegate&lt;TObject&gt;(_ObjectSortOnProperties[0].PropertyName);
        <span class="kwrd">string</span> obj1 = GetPropValue(o1).ToString();
        <span class="kwrd">string</span> obj2 = GetPropValue(o2).ToString();
        <span class="rem">// if sort order is decsending then reverse the result.</span>
        <span class="kwrd">return</span> _ObjectSortOnProperties[0].SortOrderMultiplier * obj1.CompareTo(obj2);
    }
    <span class="kwrd">private</span> <span class="kwrd">int</span> CompareValuesMultiProperty(TObject o1, TObject o2)
    {
        <span class="kwrd">int</span> sortResult = 0;            
        List&lt;ObjectSortProperty&gt;.Enumerator enmr = _ObjectSortOnProperties.GetEnumerator();
        <span class="kwrd">while</span> (sortResult == 0 &amp;&amp; enmr.MoveNext())
        {
            PropertyValueDelegate&lt;TObject&gt; GetPropValue = GetPropertyValueDelegate&lt;TObject&gt;(enmr.Current.PropertyName);
            <span class="kwrd">object</span> obj1 = GetPropValue(o1);
            <span class="kwrd">object</span> obj2 = GetPropValue(o2);
            <span class="rem">// reflection Method - Old</span>
            <span class="rem">/*object obj1 = typeof(TObject).GetProperty(enmr.Current.PropertyName).GetValue(o1, null);</span>

object obj2 = typeof(TObject).GetProperty(enmr.Current.PropertyName).GetValue(o2, null);*/ if (enmr.Current.SortOn == SortOnEnum.String) { obj1 = obj1.ToString(); obj2 = obj2.ToString(); } // if sort order is descending then reverse the result. sortResult = enmr.Current.SortOrderMultiplier * ((IComparable)obj1).CompareTo(obj2); } enmr.Dispose(); return sortResult; } /// <summary> /// Compares the current instance with another simple object of the same type. /// </summary> /// <param name="o1">First value to compare</param> /// <param name="o2">Second value to compare</param> /// <returns>Returns the IComparable.CompareTo result</returns> private int CompareSimple(TObject o1, TObject o2) { return ObjectSortOnProperties[0].SortOrderMultiplier * ((IComparable)o1).CompareTo(o2); } #endregion #endregion #region IL generator public delegate object PropertyValueDelegate<TObj>(TObj obj); public static PropertyValueDelegate<TObj> CreatePropertyValueDelegate<TObj>(string propertyName) { // Use Reflection to get the MethodInfo for our Property's get accessor MethodInfo mi = typeof(TObj).GetMethod("get" + propertyName); if (mi == null) throw new Exception("Property '" + propertyName + "' doesn't have a get implementation");

        <span class="rem">// Create a new Dynamic Method for retrieving the Property&#39;s value</span>
        DynamicMethod dm = <span class="kwrd">new</span> DynamicMethod(<span class="str">&#34;Get&#34;</span> + propertyName + <span class="str">&#34;PropertyValue&#34;</span>,
            <span class="kwrd">typeof</span>(<span class="kwrd">object</span>), <span class="kwrd">new</span> Type[] { <span class="kwrd">typeof</span>(TObj) }, <span class="kwrd">typeof</span>(TObj).Module);
        <span class="rem">// Let&#39;s Emit some IL code to actually call the Property&#39;s accessor and return the value.</span>
        ILGenerator il = dm.GetILGenerator();
        <span class="rem">// Load arg 0 (Object) onto the stack</span>
        il.Emit(OpCodes.Ldarg_0);
        <span class="rem">// Call the property&#39;s get accessor (method) on the object that was loaded onto the stack</span>
        il.EmitCall(OpCodes.Call, mi, <span class="kwrd">null</span>);
        <span class="rem">// if the property&#39;s get accessor (method) returns a value(int, datetime, etc) type then we have to box it</span>
        <span class="kwrd">if</span> (mi.ReturnType.IsValueType)
            il.Emit(OpCodes.Box, mi.ReturnType);
        
        <span class="rem">// Return the value (back to the caller) that was returned (to us) from calling the method above</span>
        il.Emit(OpCodes.Ret);
        <span class="rem">// Turn the Dynamic Method into a delegate and return it</span>
        <span class="kwrd">return</span> (PropertyValueDelegate&lt;TObj&gt;)dm.CreateDelegate(<span class="kwrd">typeof</span>(PropertyValueDelegate&lt;TObj&gt;));
    }
    <span class="kwrd">private</span> <span class="kwrd">static</span> Hashtable StoredDelegatesHashTable = <span class="kwrd">new</span> Hashtable();
    <span class="kwrd">public</span> <span class="kwrd">static</span> PropertyValueDelegate&lt;TObj&gt; GetPropertyValueDelegate&lt;TObj&gt;(<span class="kwrd">string</span> propertyName)
    {            
        <span class="kwrd">object</span> returnValue = StoredDelegatesHashTable[propertyName];
        <span class="kwrd">if</span> (returnValue == <span class="kwrd">null</span>)
        {
            returnValue = CreatePropertyValueDelegate&lt;TObj&gt;(propertyName);
            StoredDelegatesHashTable[propertyName] = returnValue;
        }
        <span class="kwrd">return</span> (PropertyValueDelegate&lt;TObj&gt;)returnValue;
    }
    <span class="preproc">#endregion</span>
}
<span class="preproc">#endregion</span>
<span class="preproc">#region</span> ObjectSortProperty <span class="kwrd">class</span>
<span class="kwrd">public</span> <span class="kwrd">class</span> ObjectSortProperty
{
    <span class="preproc">#region</span> Variables
    <span class="kwrd">private</span> <span class="kwrd">string</span> _PropertyName = <span class="kwrd">string</span>.Empty;
    <span class="kwrd">private</span> SortOnEnum _SortOn = SortOnEnum.Default;
    <span class="kwrd">private</span> <span class="kwrd">int</span> _SortOrder = Convert.ToInt16(SortOrderEnum.ASC);
    <span class="preproc">#endregion</span>
    <span class="preproc">#region</span> Constructor
    <span class="kwrd">public</span> ObjectSortProperty(<span class="kwrd">string</span> propertyName)
        : <span class="kwrd">this</span>(propertyName, SortOrderEnum.ASC)
    { }
    <span class="kwrd">public</span> ObjectSortProperty(<span class="kwrd">string</span> propertyName, SortOrderEnum sortOrder)
        : <span class="kwrd">this</span>(propertyName, sortOrder, SortOnEnum.Default)
    { }
    <span class="rem">/// &lt;summary&gt;</span>
    <span class="rem">/// Constructor for property to sort an object on</span>
    <span class="rem">/// &lt;/summary&gt;</span>
    <span class="rem">/// &lt;param name=&#34;propertyName&#34;&gt;Object&#39;s property to sort on&lt;/param&gt;</span>
    <span class="rem">/// &lt;param name=&#34;sortOrder&#34;&gt;Sort order&lt;/param&gt;</span>
    <span class="rem">/// &lt;param name=&#34;sortOn&#34;&gt;Sort on object&#39;s default comparer or on object.ToString()&#39;s comparer&lt;/param&gt;</span>
    <span class="kwrd">public</span> ObjectSortProperty(<span class="kwrd">string</span> propertyName, SortOrderEnum sortOrder, SortOnEnum sortOn)
    {
        _PropertyName = propertyName;
        _SortOn = sortOn;
        <span class="kwrd">this</span>.SortOrder = sortOrder;
    }
    <span class="preproc">#endregion</span>
    <span class="preproc">#region</span> Properties
    <span class="rem">/// &lt;summary&gt;</span>
    <span class="rem">/// Object&#39;s Property name to sort on</span>
    <span class="rem">/// &lt;/summary&gt;</span>
    <span class="kwrd">public</span> <span class="kwrd">string</span> PropertyName
    {
        get { <span class="kwrd">return</span> _PropertyName; }
        set { _PropertyName = <span class="kwrd">value</span>; }
    }
    <span class="rem">/// &lt;summary&gt;</span>
    <span class="rem">/// Sort on object&#39;s default comparer or on object.ToString()&#39;s comparer</span>
    <span class="rem">/// &lt;/summary&gt;</span>
    <span class="kwrd">public</span> SortOnEnum SortOn
    {
        get { <span class="kwrd">return</span> _SortOn; }
        set { _SortOn = <span class="kwrd">value</span>; }
    }
    <span class="rem">/// &lt;summary&gt;</span>
    <span class="rem">/// Sort Order of sorted list</span>
    <span class="rem">/// &lt;/summary&gt;</span>
    <span class="kwrd">public</span> SortOrderEnum SortOrder
    {
        get
        {
            <span class="kwrd">return</span> (SortOrderEnum)_SortOrder;
        }
        set
        {
            _SortOrder = Convert.ToInt16(<span class="kwrd">value</span>);
        }
    }
    <span class="rem">/// &lt;summary&gt;</span>
    <span class="rem">/// Get&#39;s the integer value of the sort order (1 = ASC, -1 = DESC). Used only for easier multiplication of comparer result so that no enum casting is needed</span>
    <span class="rem">/// &lt;/summary&gt;</span>
    <span class="kwrd">public</span> <span class="kwrd">int</span> SortOrderMultiplier
    {
        get { <span class="kwrd">return</span> _SortOrder; }
    }
    <span class="preproc">#endregion</span>
}
<span class="preproc">#endregion</span>
<span class="kwrd">class</span> TestClass
{
    <span class="kwrd">private</span> <span class="kwrd">string</span> _StringType = <span class="kwrd">string</span>.Empty;        
    <span class="kwrd">private</span> <span class="kwrd">int</span> _IntType = 0;        
    <span class="kwrd">private</span> <span class="kwrd">decimal</span> _DecimalType;
    <span class="kwrd">public</span> TestClass(<span class="kwrd">string</span> stringType, <span class="kwrd">int</span> intType, <span class="kwrd">decimal</span> decimalType)
    {
        <span class="kwrd">this</span>._StringType = stringType;
        <span class="kwrd">this</span>._IntType = intType;
        <span class="kwrd">this</span>._DecimalType = decimalType;
    }
    <span class="kwrd">public</span> <span class="kwrd">string</span> StringType
    {
        get { <span class="kwrd">return</span> _StringType; }
        set { _StringType = <span class="kwrd">value</span>; }
    }
    <span class="kwrd">public</span> <span class="kwrd">int</span> IntType
    {
        get { <span class="kwrd">return</span> _IntType; }
        set { _IntType = <span class="kwrd">value</span>; }
    }
    <span class="kwrd">public</span> <span class="kwrd">decimal</span> DecimalType
    {
        get { <span class="kwrd">return</span> _DecimalType; }
        set { _DecimalType = <span class="kwrd">value</span>; }
    }
}

}

Legacy Comments


ugur
2007-05-15
re: Sorter class v2 - A generic comparer/sorter class
How can we use this class with ObjectDataSource. When I try it I receive this error:
The data source 'ObjectDataSource1' does not support sorting with IEnumerable data. Automatic sorting is only supported with DataView, DataTable, and DataSet.