Search code examples
c#genericsreflectionlambdaexpression-trees

Building generic order-by-statement


I have a class with a bunch of properties:

class Foo {
    public string Name {get; set; }
    public int Age {get; set; 
}

and a collection of instances of Foo.

Now I want to order those elements by a property given by the user. So the user selects a property from the type Foo. Now I want to order by elements based on this property.

One approach is a reflection-based one similar to this:

var p = typeof(Foo).GetProperty("Age");
var ordered = fooList.OrderBy(x => (int) p.GetValue(x, null));

This works so far. However I also tried a second one and there I am stuck. It deals by performing an expression-tree as follows:

var f = GetOrderStatement<Foo>("Age");
var ordered = fooList.OrderBy(f)

With

Func<T, int> GetOrderStatement<T>(string attrName)
{
    var type = Expression.Parameter(typeof(T), attrName);
    var property = Expression.PropertyOrField(type, attrName);
    return Expression.Lambda<Func<T, int>>(property).Compile();
}

My question is: As I should return a Func<T, int> where to get the int-part from or in other words where and how do I perform the actual comparison? I suppose I have to make a CallExpression to IComparable.CompareTo but I´m not sure how to do so. I think I need access to the both instances to compare.

EDIT: Complete code-example

static void Main()
{
    var fooList = new[] { new Foo("Hans", 10), new Foo("Georg", 12), new Foo("Birgit", 40) };
    var f = GetOrderStatement<Foo>("Age");
    var ordered = fooList.OrderBy(f);
}

private static Func<T, int> GetOrderStatement<T>(string attrName)
{
    var type = Expression.Parameter(typeof(T), attrName);
    var property = Expression.PropertyOrField(type, attrName);
    return Expression.Lambda<Func<T, int>>(property).Compile();
}

Executing this code will throw an

ArgumentException: Incorrect number of parameters supplied for lambda declaration


Solution

  • The problem is that you're trying to build a Func<T, int> but your call to Expression.Lambda doesn't specify the parameter expression, which means you can't expect it to create a delegate that has any parameters. Just specifying type as a second argument to Expression.Lambda works. Here's a complete example based on your question - note that I've changed the ages to prove that it's actually ordering, and I've updated your fields to read-only properties:

    using System;
    using System.Linq;
    using System.Linq.Expressions;
    
    class Foo 
    {
        public string Name { get; }
        public int Age { get; }
    
        public Foo(string name, int age)
        {
            this.Name = name;
            this.Age = age;
        }
    }
    
    class Test
    {
        static void Main()
        {
            var fooList = new[]
            {
                new Foo("Hans", 12),
                new Foo("Georg", 10),
                 new Foo("Birgit", 40)
            };
            var f = GetOrderStatement<Foo>("Age");
            var ordered = fooList.OrderBy(f);
            foreach (var item in ordered)
            {
                Console.WriteLine($"{item.Name}: {item.Age}");
            }
        }
    
        private static Func<T, int> GetOrderStatement<T>(string attrName)
        {
            var type = Expression.Parameter(typeof(T), attrName);
            var property = Expression.PropertyOrField(type, attrName);
            return Expression.Lambda<Func<T, int>>(property, type).Compile();
        }
    }
    

    Output:

    Georg: 10
    Hans: 12
    Birgit: 40