Search code examples
c#linqasp.net-corelambda

I have a problem with dynamic sorting, it doesn't work properly


I have a function that I use to OrderBy() dynamically:

internal static Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> GetOrderBy(List<(string, bool)> orderColumnsAndIsDesc)
{

    bool IsFirst = true;
    MethodCallExpression resultExp = null;
    string methodName;
    LambdaExpression finalLambda = null;
    
    foreach (var item in orderColumnsAndIsDesc)
    {
        string prop = item.Item1;
        string orderType = item.Item2 == true ? "asc" : "desc";
        Type typeQueryable = typeof(IQueryable<TEntity>);
        ParameterExpression argQueryable = Expression.Parameter(typeQueryable, "p");
        var outerExpression = Expression.Lambda(argQueryable, argQueryable);
        IQueryable<TEntity> query = new List<TEntity>().AsQueryable<TEntity>();
        Type type = typeof(TEntity);
        ParameterExpression arg = Expression.Parameter(type, "x");
        Expression expr = arg;
        PropertyInfo pi = type.GetProperty(prop, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
        expr = Expression.Property(expr, pi);
        type = pi.PropertyType;
        LambdaExpression lambda = Expression.Lambda(expr, arg);
        
        if (IsFirst)
        {
            methodName = orderType == "asc" ? "OrderBy" : "OrderByDescending";
            resultExp =
            Expression.Call(typeof(Queryable), methodName, new Type[] { typeof(TEntity), type }, outerExpression.Body, Expression.Quote(lambda));
        }
        else
        {
            methodName = orderType == "asc" ? "ThenBy" : "ThenByDescending";
            resultExp =
           Expression.Call(typeof(Queryable), methodName, new Type[] { typeof(TEntity), type }, resultExp, Expression.Quote(lambda));
        }
        
        finalLambda = Expression.Lambda(resultExp, argQueryable);
        
        IsFirst = false;
    }
   
    return (Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>)finalLambda.Compile();
}

It works correctly when there is one item to sort, but it gives an error when there is more than one item. its error:

System.InvalidOperationException: 'variable 'p' of type 'System.Linq.IQueryable'1[CMS.Data.Models.Category]' referenced from scope '', but it is not defined'

I don't know how to fix it, please help me.


Solution

  • Try this:

    public static class IEnumerableExtensions {
        public static IEnumerable<T> OrderBy<T>(this IEnumerable<T> source, params Tuple<string, bool>[] sortDefinitions)
            => OrderBy<T>(source, sortDefinitions.Select(i => new Tuple<Func<T, object>, bool>(GetPropertyLambda<T>(i.Item1), i.Item2)).ToArray());
    
        //you can try use lambda direcly in params
        public static IEnumerable<T> OrderBy<T>(this IEnumerable<T> source, params Tuple<Func<T, object>, bool>[] sortDefinitions) {
            if(source?.Any() ?? false) {
                var items = source;
                foreach(var sortDefinition in sortDefinitions)
                    if(items is IOrderedEnumerable<T> ordered)
                        //thenby
                        items = sortDefinition.Item2 ? ordered.ThenBy(sortDefinition.Item1) : ordered.ThenByDescending(sortDefinition.Item1);
                    else
                        items = sortDefinition.Item2 ? items.OrderBy(sortDefinition.Item1) : items.OrderByDescending(sortDefinition.Item1);
                return items;
            } else
                return source;
        }
    
        //based on your code
        private static Func<T, object> GetPropertyLambda<T>(string propertyName) {
            Type type = typeof(T);
            ParameterExpression arg = Expression.Parameter(type, "x");
            Expression expr = arg;
            PropertyInfo pi = type.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
            expr = Expression.Property(expr, pi);
            return (Func<T, object>) Expression.Lambda(expr, arg).Compile();
        }
    }