linqsortingdynamic-queriesnested-properties

C#, LINQ a generic sort method to sort a list by object properties and nested properties


I have an entity called User in my EF Model:

public class User
{
    public int UserId { get; set; }
    public Branch HomeLocation{ get; set; }   
    public string CellPhone { get; set; }
    public string UserName { get; set; }
    public string Password { get; set; }
    public string Email { get; set; } 
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string UserCode { get; set; }
}

Branch is another entity in the model:

public class Branch
{    
    public int BranchId { get; set; }
    public string BranchName{ get; set; }
    public string Address { get; set; } 
}

My requirement is to get a users list and display it on a grid, and then sort the list by some of the columns (one at a time). Say, for example, sort by username, firstname, lastname and HomeLocation. When sorting by homelocation, it should be sorted by the branch name.

I have many grids like this displaying other data as well. So I want to develop a generic sort mechanism and I have achieved it using some of the examples found in Google, for example this one:

public class GenericSorter<T>
{
    public IEnumerable<T> Sort(IEnumerable<T> source, string sortBy, string sortDirection)
    {
        var param = Expression.Parameter(typeof(T), "item");

        var sortExpression = Expression.Lambda<Func<T, object>>
            (Expression.Convert(Expression.Property(param, sortBy), typeof(object)), param);

        switch (sortDirection.ToLower())
        {
            case "asc":
                return source.AsQueryable<T>().OrderBy<T, object>(sortExpression);
            default:
                return source.AsQueryable<T>().OrderByDescending<T, object>(sortExpression);

        } 
    }
}

However, sorting by home location fails because it needs to be sorted by an inner property of the user entity. I've tried using Dynamic LINQ library too, but there's no luck.

Update: Note that I have to sort a List, not IQueryable, because my list contains fields encrypted with AE, which don't support DB-level sorting.

Can someone point out to me how to achieve the dynamic sorting from an inner property?

Update2: I followed the example and implemented the sort using the extension methods and this is how it's applied on my list:

var users = (from u in context.Users.Include("Branch")
                    where (u.FkBranchId == branchId || branchId == -1) && u.IsActive
                        && (searchTerm == string.Empty || (u.FirstName.Contains(searchTerm) || u.LastName.Equals(searchTerm)
                            || u.UserName.Contains(searchTerm) || u.UserCode.Contains(searchTerm)))
                    select u).ToList();

        var rowCount = users.Count;

        var orderedList = users.OrderBy(sortInfo.SortColumn).Skip(pageInfo.Skip).Take(pageInfo.PageSize).ToList();

But I get the following error: Object of type 'System.Linq.Expressions.Expression1[System.Func2[ClientData.User,System.String]]' cannot be converted to type 'System.Func`2[ClientData.User,System.String]'.

Error is thrown from the following:

object result = typeof(Enumerable).GetMethods().Single(
                method => method.Name == methodName
                        && method.IsGenericMethodDefinition
                        && method.GetGenericArguments().Length == 2
                        && method.GetParameters().Length == 2)
                .MakeGenericMethod(typeof(T), type)
                .Invoke(null, new object[] { source, lambda });

After this, I'm receiving the following error, in some occasions as explained in the comment: enter image description here


Solution

  • Adapt the code from @MarcGravell found here.

    public static class EnumerableExtensions
    {
        public static IOrderedEnumerable<T> OrderBy<T>(
            this IEnumerable<T> source,
            string property)
        {
            return ApplyOrder<T>(source, property, "OrderBy");
        }
    
        public static IOrderedEnumerable<T> OrderByDescending<T>(
            this IEnumerable<T> source,
            string property)
        {
            return ApplyOrder<T>(source, property, "OrderByDescending");
        }
    
        public static IOrderedEnumerable<T> ThenBy<T>(
            this IOrderedEnumerable<T> source,
            string property)
        {
            return ApplyOrder<T>(source, property, "ThenBy");
        }
    
        public static IOrderedEnumerable<T> ThenByDescending<T>(
            this IOrderedEnumerable<T> source,
            string property)
        {
            return ApplyOrder<T>(source, property, "ThenByDescending");
        }
    
        static IOrderedEnumerable<T> ApplyOrder<T>(
            IEnumerable<T> source,
            string property,
            string methodName)
        {
            string[] props = property.Split('.');
            Type type = typeof(T);
            ParameterExpression arg = Expression.Parameter(type, "x");
            Expression expr = arg;
            foreach (string prop in props)
            {
                // use reflection (not ComponentModel) to mirror LINQ
                PropertyInfo pi = type.GetProperty(prop);
                expr = Expression.Property(expr, pi);
                type = pi.PropertyType;
            }
            Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type);
            LambdaExpression lambda = Expression.Lambda(delegateType, expr, arg);
    
            object result = typeof(Enumerable).GetMethods().Single(
                    method => method.Name == methodName
                            && method.IsGenericMethodDefinition
                            && method.GetGenericArguments().Length == 2
                            && method.GetParameters().Length == 2)
                    .MakeGenericMethod(typeof(T), type)
                    .Invoke(null, new object[] { source, lambda.Compile() });
            return (IOrderedEnumerable<T>)result;
        }
    }
    

    UPDATED

    Use it from a List<> :

    var list = new List<MyModel>();
    list = list.OrderBy("MyProperty");