Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strongly typed dynamic Linq sorting

Tags:

c#

.net

linq

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)

like image 228
David Avatar asked Feb 17 '09 17:02

David


2 Answers

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) 
like image 164
David Wengier Avatar answered Oct 04 '22 15:10

David Wengier


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; }     } } 
like image 43
Thomas Eyde Avatar answered Oct 04 '22 15:10

Thomas Eyde