Search code examples
c#linqlinq-to-entitiesexpression-trees

LINQ OrderBy with index ; getting error using Generic TKey


I am trying to order an IEnumerable<MyClass> based on the index of the selected property:

class MyClass 
{
 public string Name;
 public string Value;
}

I have a helper method to get an Expression<> of the criteria.
It is compiled to create the Func<> to help with the ordering.
I'm having trouble trying to make the Func<> return a Generic TKey to be ordered.

public Expression<Func<T, TKey>> Helper<T>(int propIdx)
{
    var param = Expression.Parameter(typeof(T), "record");
    var props = typeof(T).GetProperties();

    if(propIdx > props.Count()) 
        return Expression.Lambda<Func<T, TKey>>)(Expression.Constant(true), param);

    Expression propExp = Expression.Property(param, props[propIdx].Name);

    return lambda = Expression.Lambda<Func<T, TKey>>(propExp, param);
}

This is used to help with dynamic order criteria.

OrderByIdx(IEnumerable<MyClass> input, int propIdx)
{
     Expression<Func<T, TKey>> exp = Helper(propIdx);
     
     var funct = exp.Compile();

     return input.OrderBy(funct);
}

I am getting an error The type or namespace TKey could not be found

How can I use a Generic<TKey> to help with the ordering ?


Solution

  • First of all: Type.GetProperties doesn't have a defined order.

    var props = typeof(T).GetProperties();
    

    Which property will be in props[0]? And are all properties readable?

    So if you want to get a defined order, my advice would be to do some sorting. For instance get the public readable properties ordered by name.

    Furthermore, isn't it a bit weird if users of your code (= software, not operators) want to use your generic code based on an index?

    • They have a class MyClass with a property Date
    • They have a sequence of objects of this class.
    • They want to order by property Date
    • Instead of ordering by property Date, or by property named "Date", They have to determine the index of property Date: Date seems to be the fourth property
    • Then they have to call your method using this index: "OrderBy index 4"

    Wouldn't it be easier if they could just say: "order by property Date", or "order by the property that is named 'Date'?

    IEnumerable<MyClass> source = ...
    var orderedSource = source.OrderBy(t => t.Date);
    

    Sometimes your users don't have access to the property, they only know the name of the property. In that case you could create an extension method where you provide the name of a property, and that returns the ordered source.

    If you are not familiar with extension methods. See extension methods demystified

    Usage would be something like:

    var orderedSource = source.OrderBy(nameof(MyClass.Date));
    

    or for instance: "order by the selected column in my DataGridView".

    string propertyName = this.DataGridView.Columns.Cast<DataGridViewColumn>()
        .Where(column => column.IsSelected)
        .Select(column => column.DataPropertyName)
        .FirstOrDefault();
    var orderedSource = source.OrderBy(propertyName);
    

    Such a procedure would be easy to make, easy to use and reuse, easy to test, easy to maintain. And above all: they are one-liners:

    public static IOrderedEnumerable<T> OrderBy<T>(
        this IEnumerable<T> source,
        string propertyName)
    {
        // TODO: handle invalid input
        PropertyInfo propertyInfo = typeof(T).GetProperty(propertyName);
        // TODO: handle invalid propertyname
    
        return source.OrderBy(propertyInfo);
    }
    
    public static IOrderedEnumerable<T> OrderBy<T>(
        this IEnumerable<T> source,
        PropertyInfo propertyInfo)
    {
        // TODO: handle invalid parameters
        return source.OrderBy(t => propertyInfo.GetValue(t) as propertyInfo.PropertyType);
    }
    

    If desired you could create overloads for ThenBy(this IEnumerable<T>, ...).

    But I want to order by Index, not by Name!

    If you really want to sort by index, it is easier to create extension methods with Lambda expressions, then to fiddle with Expressions.

    Consider the following extension methods:

    public static IOrderedEnumerable<T> OrderBy<T>(
        IEnumerable<T> source,
        int propertyIndex)
    {
        return source.OrderBy(propertyIndex, GetDefaultPropertyOrder(typeof(T));
    }
    
    public static IOrderedEnumerable<T> OrderBy<T>(
        IEnumerable<T> source,
        int propertyIndex,
        IReadOnlyList<PropertyInfo> properties)
    {
        // TODO: handle null and out-of-range parameters
        PropertyInfo sortProperty = properties[propertyIndex];
        return source.OrderBy(sortProperty);
    }
    
    public static IList<PropertyInfo> GetDefaultPropertyOrder(Type t)
    {
        return t.GetProperties()
            .Where(property => property.CanRead)
            .OrderBy(property => property.Name)
            .ToList();
    }
    

    Usage:

    BindingList<MyClass> myObjects = ...
    
    // display myObjects in a dataGridView
    this.DataGridView.DataSource = myObjects;
    
    // event if operator clicks on column header:
    public void OnColumnHeaderClicked(object sender, ...)
    {
        DataGridViewColumn clickedColumn = GetClickedColumn();
    
        // sort by column index, as you prefer:
        var sortedObjects = myObjects.SortBy(clickedColumn.DisplayIndex);
        ProcessSortedObjects(sortedObjects);
    
        // but of course, you skip some intermediate methods if you sort by property name
        var sortedObject = myObject.SortBy(clickedColumn.DataPropertyName);
        ProcessSortedObjects(sortedObjects);
    }