0

I have a coding problem to solve, I kind of know the path I should be following, but there is still something missing. I got a mission to change a code to make it more generic and reduce the redundance.

The whole class is given below. It seems a bit dummy, the question is the redundance itself.

using System; 
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace TestRedundance
{
    public class EntityRedundance
    {
        public class Entity
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public string Description { get; set; }
            public DateTime ValidFrom { get; set; }
        }

        private void AddSorting(IQueryable<Entity> query, List<string> sortingColumns, bool orderDescending)
        {

            if (!sortingColumns.Any())
            {
                query = query.OrderBy(x => x.Name);
                return;
            }

            foreach (var column in sortingColumns)
            {

                switch (column)
                {

                    case "Name":
                        query = orderDescending ? query.OrderByDescending(x => x.Name) : query.OrderBy(x => x.Name);
                        break;
                    case "Description":
                        query = orderDescending ? query.OrderByDescending(x => x.Description) : query.OrderBy(x => x.Description);
                        break;
                    case "ValidFrom":
                        query = orderDescending ? query.OrderByDescending(x => x.ValidFrom) : query.OrderBy(x => x.ValidFrom);
                        break;

                }
            }
        }
    }
}

As far as I understood, the redundance is on repeting the same exact code in each switch case condition, changing only the column to be sorted by.

My intention was to change it to a single line, comparing the property name of my entity with the column name, using Reflection, and then replacing the switch-case block for this single line. But the lambda expression does not allow me to do that. Something like the line below, which is commented because it's sintax is wrong.

//query = orderDescending ? query.OrderByDescending(x => 
//x.GetType().GetProperties() : query.OrderBy(x => x.Name);

I would appreciate any suggestions.

Many thanks.

Ryan Wilson
  • 10,223
  • 2
  • 21
  • 40
  • 1
    Might want to check out this post: [how-to-order-by-a-dynamic-column-name-in-entityframework](https://stackoverflow.com/questions/38055150/how-to-order-by-a-dynamic-column-name-in-entityframework) – Ryan Wilson Oct 22 '21 at 20:13
  • Your lambdas are just `Func where T : [string | DateTime]`. You could write a selector for them if nothing else. – ChiefTwoPencils Oct 22 '21 at 20:13
  • Note that your code is incorrect - if you use multiple `OrderBy` chained, then the last one is the only sort applied. You must use `ThenBy` if you expect to chain sorting in LINQ. – NetMage Nov 05 '21 at 18:50

1 Answers1

0

There are a few issues with your code. First, preserving strong typing while dynamically adding columns to sort by requires hiding the column selection lambda inside the method so that you have a consistent signature, regardless of the type of the column. Second, LINQ sorting requires OrderBy to be followed by ThenBy to chain sorts, which means you have another type issue as ThenBy expects an IOrderedQueryable<T> so assigning to query won't work. Thirdly, assigning to query doesn't change anything after the method returns, since that is a parameter to the AddSorting method and the OrderBy and ThenBy methods return new objects that will be lost when the method returns.

So to fix these issues, first we can create some helper dictionaries to map a bool (orderDescending) to a particular sorting method for each type of column, one for OrderBy and one for ThenBy:

static Dictionary<bool, Func<IQueryable<Entity>, Expression<Func<Entity, string>>, IOrderedQueryable<Entity>>> stringOrderByDirMap = new() {
    { false, Queryable.OrderBy<Entity, string> },
    { true, Queryable.OrderByDescending<Entity, string> },
};

static Dictionary<bool, Func<IOrderedQueryable<Entity>, Expression<Func<Entity, string>>, IOrderedQueryable<Entity>>> stringThenByDirMap = new() {
    { false, Queryable.ThenBy<Entity, string> },
    { true, Queryable.ThenByDescending<Entity, string> },
};

static Dictionary<bool, Func<IQueryable<Entity>, Expression<Func<Entity, DateTime>>, IOrderedQueryable<Entity>>> dateTimeOrderByDirMap = new() {
    { false, Queryable.OrderBy<Entity, DateTime> },
    { true, Queryable.OrderByDescending<Entity, DateTime> },
};

static Dictionary<bool, Func<IOrderedQueryable<Entity>, Expression<Func<Entity, DateTime>>, IOrderedQueryable<Entity>>> dateTimeThenByDirMap = new() {
    { false, Queryable.ThenBy<Entity, DateTime> },
    { true, Queryable.ThenByDescending<Entity, DateTime> },
};

Using these, you can create a couple of dictionaries (one for OrderBy and one for ThenBy) which map column names to a lambda function that takes a bool and returns a lambda that maps a IQueryable<T> or IOrderedQueryable<T> to an IOrderedQueryable<T> for the appropriate column:

static Dictionary<string, Func<bool, Func<IQueryable<Entity>, IOrderedQueryable<Entity>>>> firstSort = new() {
    { "Name", desc => q => stringOrderByDirMap[desc](q, e => e.Name) },
    { "Description", desc => q => stringOrderByDirMap[desc](q, e => e.Description) },
    { "ValidFrom", desc => q => dateTimeOrderByDirMap[desc](q, e => e.ValidFrom) },
};

static Dictionary<string, Func<bool, Func<IOrderedQueryable<Entity>, IOrderedQueryable<Entity>>>> thenSort = new() {
    { "Name", desc => q => stringThenByDirMap[desc](q, e => e.Name) },
    { "Description", desc => q => stringThenByDirMap[desc](q, e => e.Description) },
    { "ValidFrom", desc => q => dateTimeThenByDirMap[desc](q, e => e.ValidFrom) },
};

With all of these setup, your AddSorting method becomes

private IQueryable<Entity> AddSorting(IQueryable<Entity> query, List<string> sortingColumns, bool orderDescending) {
    if (!sortingColumns.Any())
        return query.OrderBy(x => x.Name);

    IOrderedQueryable<Entity> sortedQuery = null;

    foreach (var column in sortingColumns) {
        if (sortedQuery == null)
            sortedQuery = firstSort[column](orderDescending)(query);
        else
            sortedQuery = thenSort[column](orderDescending)(sortedQuery);
    }
    
    return sortedQuery;
}

Given your example of only three columns, using Reflection to build Expression trees seems like overkill, however, you could populate the firstSort and thenSort dictionaries using Reflection from a type and list of columns instead of coding their initial values. You would need to code appropriate OrderBy and ThenBy maps for each possible column type.

NetMage
  • 26,163
  • 3
  • 34
  • 55