Strongly typed dynamic Linq sorting

I'm trying to build some code for dynamically sorting a Linq IQueryable<>.

The obvious way is here, which sorts a list using a string for the field name
http://dvanderboom.wordpress.com/2008/12/19/dynamically-composing-linq-orderby-clauses/

However I want one change - compile time checking of field names, and the ability to use refactoring/Find All References to support later maintenance. That means I want to define the fields as f=>f.Name, instead of as strings.

For my specific use I want to encapsulate some code that would decide which of a list of named "OrderBy" expressions should be used based on user input, without writing different code every time.

Here is the gist of what I've written:

var list = from m Movies select m; // Get our list

var sorter = list.GetSorter(...); // Pass in some global user settings object

sorter.AddSort("NAME", m=>m.Name);
sorter.AddSort("YEAR", m=>m.Year).ThenBy(m=>m.Year);

list = sorter.GetSortedList();

...
public class Sorter<TSource>
...
public static Sorter<TSource> GetSorter(this IQueryable<TSource> source, ...)

The GetSortedList function determines which of the named sorts to use, which results in a List object, where each FieldData contains the MethodInfo and Type values of the fields passed in AddSort:

public SorterItem<TSource> AddSort(Func<T, TKey> field)
{
   MethodInfo ... = field.Method;
   Type ... = TypeOf(TKey);
   // Create item, add item to diction, add fields to item's List<>
   // The item has the ThenBy method, which just adds another field to the List<>
}

I'm not sure if there is a way to store the entire field object in a way that would allow it be returned later (it would be impossible to cast, since it is a generic type)

Is there a way I could adapt the sample code, or come up with entirely new code, in order to sort using strongly typed field names after they have been stored in some container and retrieved (losing any generic type casting)


Solution 1:

The easiest way to do this would be to have your AddSort() function take an Expression<Func<Movie>> instead of just a Func. This allows your sort method to inspect the Expression to extract out the name of the property that you want to sort on. You can then store this name internally as a string, so storing is very easy and you can use the sorting algorithm you linked to, but you also get type safety and compile time checking for valid property names.

static void Main(string[] args)
{
    var query = from m in Movies select m;

    var sorter = new Sorter<Movie>();
    sorter.AddSort("NAME", m => m.Name);
}

class Sorter<T>
{
    public void AddSort(string name, Expression<Func<T, object>> func)
    {
        string fieldName = (func.Body as MemberExpression).Member.Name;
    }
}

In this case, i've used object as the return type of the func, because its easily automatically convertible, but you could implement that with different types, or generics, as appropriate, if you require more functionality. In this case, since the Expression is just there to be inspected, it doesn't really matter.

The other possible way is to still take a Func, and store that in the dictionary itself. Then, when it comes to sorting, and you need to get the value to sort on, you can call something like:

// assuming a dictionary of fields to sort for, called m_fields
m_fields[fieldName](currentItem)

Solution 2:

Bummer! I must learn how to read the specifications from end to end :-(

However, now that I have spent too much time fooling around rather than working, I will post my results anyway hoping this will inspire people to read, think, understand (important) and then act. Or how to be too clever with generics, lambdas and funny Linq stuff.

A neat trick I discovered during this exercise, are those private inner classes which derives from Dictionary. Their whole purpose is to remove all those angle brackets in order to improve readability.

Oh, almost forgot the code:

UPDATE: Made the code generic and to use IQueryable instead of IEnumerable

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;


namespace StackOverflow.StrongTypedLinqSort
{
    [TestFixture]
    public class SpecifyUserDefinedSorting
    {
        private Sorter<Movie> sorter;

        [SetUp]
        public void Setup()
        {
            var unsorted = from m in Movies select m;
            sorter = new Sorter<Movie>(unsorted);

            sorter.Define("NAME", m1 => m1.Name);
            sorter.Define("YEAR", m2 => m2.Year);
        }

        [Test]
        public void SortByNameThenYear()
        {
            var sorted = sorter.SortBy("NAME", "YEAR");
            var movies = sorted.ToArray();

            Assert.That(movies[0].Name, Is.EqualTo("A"));
            Assert.That(movies[0].Year, Is.EqualTo(2000));
            Assert.That(movies[1].Year, Is.EqualTo(2001));
            Assert.That(movies[2].Name, Is.EqualTo("B"));
        }

        [Test]
        public void SortByYearThenName()
        {
            var sorted = sorter.SortBy("YEAR", "NAME");
            var movies = sorted.ToArray();

            Assert.That(movies[0].Name, Is.EqualTo("B"));
            Assert.That(movies[1].Year, Is.EqualTo(2000));
        }

        [Test]
        public void SortByYearOnly()
        {
            var sorted = sorter.SortBy("YEAR");
            var movies = sorted.ToArray();

            Assert.That(movies[0].Name, Is.EqualTo("B"));
        }

        private static IQueryable<Movie> Movies
        {
            get { return CreateMovies().AsQueryable(); }
        }

        private static IEnumerable<Movie> CreateMovies()
        {
            yield return new Movie {Name = "B", Year = 1990};
            yield return new Movie {Name = "A", Year = 2001};
            yield return new Movie {Name = "A", Year = 2000};
        }
    }


    internal class Sorter<E>
    {
        public Sorter(IQueryable<E> unsorted)
        {
            this.unsorted = unsorted;
        }

        public void Define<P>(string name, Expression<Func<E, P>> selector)
        {
            firstPasses.Add(name, s => s.OrderBy(selector));
            nextPasses.Add(name, s => s.ThenBy(selector));
        }

        public IOrderedQueryable<E> SortBy(params string[] names)
        {
            IOrderedQueryable<E> result = null;

            foreach (var name in names)
            {
                result = result == null
                             ? SortFirst(name, unsorted)
                             : SortNext(name, result);
            }

            return result;
        }

        private IOrderedQueryable<E> SortFirst(string name, IQueryable<E> source)
        {
            return firstPasses[name].Invoke(source);
        }

        private IOrderedQueryable<E> SortNext(string name, IOrderedQueryable<E> source)
        {
            return nextPasses[name].Invoke(source);
        }

        private readonly IQueryable<E> unsorted;
        private readonly FirstPasses firstPasses = new FirstPasses();
        private readonly NextPasses nextPasses = new NextPasses();


        private class FirstPasses : Dictionary<string, Func<IQueryable<E>, IOrderedQueryable<E>>> {}


        private class NextPasses : Dictionary<string, Func<IOrderedQueryable<E>, IOrderedQueryable<E>>> {}
    }


    internal class Movie
    {
        public string Name { get; set; }
        public int Year { get; set; }
    }
}

Solution 3:

Based on what everyone has contributed I have come up with the following.

It provides bi-directional sorting as well solving the problem inside out. Meaning it didn't make much sense to me that a new Sorter need to be created for every unsorted list of a given type. Why can't this the unsorted list be passed into the sorter. This then means that we could create a signelton instance of the Sorter for our different types...

Just an idea:

[TestClass]
public class SpecifyUserDefinedSorting
{
    private Sorter<Movie> sorter;
    private IQueryable<Movie> unsorted;

    [TestInitialize]
    public void Setup()
    {
        unsorted = from m in Movies select m;
        sorter = new Sorter<Movie>();
        sorter.Register("Name", m1 => m1.Name);
        sorter.Register("Year", m2 => m2.Year);
    }

    [TestMethod]
    public void SortByNameThenYear()
    {
        var instructions = new List<SortInstrcution>()
                               {
                                   new SortInstrcution() {Name = "Name"},
                                   new SortInstrcution() {Name = "Year"}
                               };
        var sorted = sorter.SortBy(unsorted, instructions);
        var movies = sorted.ToArray();

        Assert.AreEqual(movies[0].Name, "A");
        Assert.AreEqual(movies[0].Year, 2000);
        Assert.AreEqual(movies[1].Year, 2001);
        Assert.AreEqual(movies[2].Name, "B");
    }

    [TestMethod]
    public void SortByNameThenYearDesc()
    {
        var instructions = new List<SortInstrcution>()
                               {
                                   new SortInstrcution() {Name = "Name", Direction = SortDirection.Descending},
                                   new SortInstrcution() {Name = "Year", Direction = SortDirection.Descending}
                               };
        var sorted = sorter.SortBy(unsorted, instructions);
        var movies = sorted.ToArray();

        Assert.AreEqual(movies[0].Name, "B");
        Assert.AreEqual(movies[0].Year, 1990);
        Assert.AreEqual(movies[1].Name, "A");
        Assert.AreEqual(movies[1].Year, 2001);
        Assert.AreEqual(movies[2].Name, "A");
        Assert.AreEqual(movies[2].Year, 2000);
    }

    [TestMethod]
    public void SortByNameThenYearDescAlt()
    {
        var instructions = new List<SortInstrcution>()
                               {
                                   new SortInstrcution() {Name = "Name", Direction = SortDirection.Descending},
                                   new SortInstrcution() {Name = "Year"}
                               };
        var sorted = sorter.SortBy(unsorted, instructions);
        var movies = sorted.ToArray();

        Assert.AreEqual(movies[0].Name, "B");
        Assert.AreEqual(movies[0].Year, 1990);
        Assert.AreEqual(movies[1].Name, "A");
        Assert.AreEqual(movies[1].Year, 2000);
        Assert.AreEqual(movies[2].Name, "A");
        Assert.AreEqual(movies[2].Year, 2001);
    }

    [TestMethod]
    public void SortByYearThenName()
    {
        var instructions = new List<SortInstrcution>()
                               {
                                   new SortInstrcution() {Name = "Year"},
                                   new SortInstrcution() {Name = "Name"}
                               };
        var sorted = sorter.SortBy(unsorted, instructions); 
        var movies = sorted.ToArray();

        Assert.AreEqual(movies[0].Name, "B");
        Assert.AreEqual(movies[1].Year, 2000);
    }

    [TestMethod]
    public void SortByYearOnly()
    {
        var instructions = new List<SortInstrcution>()
                               {
                                   new SortInstrcution() {Name = "Year"} 
                               };
        var sorted = sorter.SortBy(unsorted, instructions); 
        var movies = sorted.ToArray();

        Assert.AreEqual(movies[0].Name, "B");
    }

    private static IQueryable<Movie> Movies
    {
        get { return CreateMovies().AsQueryable(); }
    }

    private static IEnumerable<Movie> CreateMovies()
    {
        yield return new Movie { Name = "B", Year = 1990 };
        yield return new Movie { Name = "A", Year = 2001 };
        yield return new Movie { Name = "A", Year = 2000 };
    }
}


public static class SorterExtension
{
    public static IOrderedQueryable<T> SortBy<T>(this IQueryable<T> source, Sorter<T> sorter, IEnumerable<SortInstrcution> instrcutions)
    {
        return sorter.SortBy(source, instrcutions);
    }
}

public class Sorter<TSource>
{
    private readonly FirstPasses _FirstPasses;
    private readonly FirstPasses _FirstDescendingPasses;
    private readonly NextPasses _NextPasses;
    private readonly NextPasses _NextDescendingPasses; 

    public Sorter()
    {
        this._FirstPasses = new FirstPasses();
        this._FirstDescendingPasses = new FirstPasses();
        this._NextPasses = new NextPasses();
        this._NextDescendingPasses = new NextPasses();
    }


    public void Register<TKey>(string name, Expression<Func<TSource, TKey>> selector)
    {
        this._FirstPasses.Add(name, s => s.OrderBy(selector));
        this._FirstDescendingPasses.Add(name, s => s.OrderByDescending(selector));
        this._NextPasses.Add(name, s => s.ThenBy(selector));
        this._NextDescendingPasses.Add(name, s => s.ThenByDescending(selector));
    }


    public IOrderedQueryable<TSource> SortBy(IQueryable<TSource> source, IEnumerable<SortInstrcution> instrcutions)
    {
        IOrderedQueryable<TSource> result = null;

        foreach (var instrcution in instrcutions) 
            result = result == null ? this.SortFirst(instrcution, source) : this.SortNext(instrcution, result); 

        return result;
    }

    private IOrderedQueryable<TSource> SortFirst(SortInstrcution instrcution, IQueryable<TSource> source)
    {
        if (instrcution.Direction == SortDirection.Ascending)
            return this._FirstPasses[instrcution.Name].Invoke(source);
        return this._FirstDescendingPasses[instrcution.Name].Invoke(source);
    }

    private IOrderedQueryable<TSource> SortNext(SortInstrcution instrcution, IOrderedQueryable<TSource> source)
    {
        if (instrcution.Direction == SortDirection.Ascending)
            return this._NextPasses[instrcution.Name].Invoke(source);
        return this._NextDescendingPasses[instrcution.Name].Invoke(source);
    }

    private class FirstPasses : Dictionary<string, Func<IQueryable<TSource>, IOrderedQueryable<TSource>>> { }

    private class NextPasses : Dictionary<string, Func<IOrderedQueryable<TSource>, IOrderedQueryable<TSource>>> { } 
}


internal class Movie
{
    public string Name { get; set; }
    public int Year { get; set; }
}

public class SortInstrcution
{
    public string Name { get; set; }

    public SortDirection Direction { get; set; }
}

public enum SortDirection   
{
    //Note I have created this enum because the one that exists in the .net 
    // framework is in the web namespace...
    Ascending,
    Descending
}

Note if you didn't want to have a dependency on SortInstrcution it wouldn't be that hard to change.

Hope this helps someone.

Solution 4:

I liked the work above - thank you very much! I took the liberty to add a couple things:

  1. Added sort direction.

  2. Made registering and calling two different concerns.

Usage:

var censusSorter = new Sorter<CensusEntryVM>();
censusSorter.AddSortExpression("SubscriberId", e=>e.SubscriberId);
censusSorter.AddSortExpression("LastName", e => e.SubscriberId);

View.CensusEntryDataSource = censusSorter.Sort(q.AsQueryable(), 
    new Tuple<string, SorterSortDirection>("SubscriberId", SorterSortDirection.Descending),
    new Tuple<string, SorterSortDirection>("LastName", SorterSortDirection.Ascending))
    .ToList();



internal class Sorter<E>
{
    public Sorter()
    {
    }
    public void AddSortExpression<P>(string name, Expression<Func<E, P>> selector)
    {
        // Register all possible types of sorting for each parameter
        firstPasses.Add(name, s => s.OrderBy(selector));
        nextPasses.Add(name, s => s.ThenBy(selector));
        firstPassesDesc.Add(name, s => s.OrderByDescending(selector));
        nextPassesDesc.Add(name, s => s.OrderByDescending(selector));
    } 

    public IOrderedQueryable<E> Sort(IQueryable<E> list, 
                                     params Tuple<string, SorterSortDirection>[] names) 
    { 
        IOrderedQueryable<E> result = null; 
        foreach (var entry in names)
        {
            result = result == null 
                   ? SortFirst(entry.Item1, entry.Item2, list) 
                   : SortNext(entry.Item1, entry.Item2, result); 
        } 
        return result; 
    } 
    private IOrderedQueryable<E> SortFirst(string name, SorterSortDirection direction, 
                                           IQueryable<E> source) 
    { 
        return direction == SorterSortDirection.Descending
             ? firstPassesDesc[name].Invoke(source) 
             : firstPasses[name].Invoke(source);
    } 

    private IOrderedQueryable<E> SortNext(string name, SorterSortDirection direction, 
                                          IOrderedQueryable<E> source) 
    {
        return direction == SorterSortDirection.Descending
             ? nextPassesDesc[name].Invoke(source) 
             : nextPasses[name].Invoke(source); 
    }

    private readonly FirstPasses firstPasses = new FirstPasses(); 
    private readonly NextPasses nextPasses = new NextPasses();
    private readonly FirstPasses firstPassesDesc = new FirstPasses();
    private readonly NextPasses nextPassesDesc = new NextPasses();

    private class FirstPasses : Dictionary<string, Func<IQueryable<E>, IOrderedQueryable<E>>> { }
    private class NextPasses : Dictionary<string, Func<IOrderedQueryable<E>, IOrderedQueryable<E>>> { }
}