0

I have a service that must sort in ascending or descending order. For this I receive 2 thing: the order of the sort, the column on which the sort must be done. I try to implement this with linq so that I can sort a BD query directly with EF Core Warning: the column on which I can sort can contain sub-objects: property1.subProperty.subProperty

I found this :

public static class IQueryableExtensions
    {
public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> query, string propertyName, IComparer<object> comparer = null)
        {
            return CallOrderedQueryable(query, "OrderBy", propertyName, comparer);
        }

        public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> query, string propertyName, IComparer<object> comparer = null)
        {
            return CallOrderedQueryable(query, "OrderByDescending", propertyName, comparer);
        }

        public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> query, string propertyName, IComparer<object> comparer = null)
        {
            return CallOrderedQueryable(query, "ThenBy", propertyName, comparer);
        }

        public static IOrderedQueryable<T> ThenByDescending<T>(this IOrderedQueryable<T> query, string propertyName, IComparer<object> comparer = null)
        {
            return CallOrderedQueryable(query, "ThenByDescending", propertyName, comparer);
        }

        /// <summary>
        /// Builds the Queryable functions using a TSource property name.
        /// </summary>
        public static IOrderedQueryable<T> CallOrderedQueryable<T>(this IQueryable<T> query, string methodName, string propertyName,
                IComparer<object> comparer = null)
        {
            var param = Expression.Parameter(typeof(T), "x");

            var body = propertyName.Split('.').Aggregate<string, Expression>(param, Expression.PropertyOrField);

            return comparer != null
                ? (IOrderedQueryable<T>)query.Provider.CreateQuery(
                    Expression.Call(
                        typeof(Queryable),
                        methodName,
                        new[] { typeof(T), body.Type },
                        query.Expression,
                        Expression.Lambda(body, param),
                        Expression.Constant(comparer)
                    )
                )
                : (IOrderedQueryable<T>)query.Provider.CreateQuery(
                    Expression.Call(
                        typeof(Queryable),
                        methodName,
                        new[] { typeof(T), body.Type },
                        query.Expression,
                        Expression.Lambda(body, param)
                    )
                );
        }
    }

But I want to call the right method dynamically according to the sort value (ASC or DESC) and according to the property value (property1 or property1.subproperty or property1.subproperty.subsubproperty, ...)

phuzi
  • 8,111
  • 3
  • 24
  • 43
ddd
  • 51
  • 5

1 Answers1

0

Here's something that might help you. This will give something you can use for dynamic sorting in other projects as well.

The class for containing sorting info:

public class SortRequest
{
   /// <summary>
   /// What field/column to sort by
   /// </summary>
   public string Field { get; set; }

   /// <summary>
   /// Whether to sort by descending or not.
   /// Defaults to false
   /// </summary>
   public bool SortDescending { get; set; } = false;
 }//Cls

The class for doing the sorting

 public static class SortingExtensions
  {

        //------------------------------------------------------------------------//

        /// <summary>
        /// Sorts <paramref name="list"/> according to the details in <paramref name="sortRequestList"/>
        /// </summary>
        /// <typeparam name="T">The type of objects being sorted</typeparam>
        /// <param name="list">The items to sort</param>
        /// <param name="sortRequestList">List of fields to sort by</param>
        /// <param name="getPropertySelectorLambda">Function to convert string field to a T property</param>
        /// <returns>The same list with sorting appended to it</returns>
        public static IEnumerable<T> AddSorting<T>(this IEnumerable<T> list, IEnumerable<SortRequest> sortRequestList, Func<string, Func<T, object>> getPropertySelectorLambda)
        {

           if (sortRequestList == null || !sortRequestList.Any() || list == null || !list.Any())
              return list;

           var isFirst = true;

           foreach (var sr in sortRequestList)
           {

              Func<T, object> lmda = getPropertySelectorLambda(sr.Field);

              list = list.AddSortLevel(sr.SortDescending, lmda, isFirst);


              isFirst = false;

           }//foreach

           return list;

        }//SortProducts

        //------------------------------------------------------------------------//

        /// <summary>
        /// Adds a level of sorting to the linq queue
        /// </summary>
        /// <typeparam name="T">Type of objects being sorted</typeparam>
        /// <param name="list">The Items being sorted</param>
        /// <param name="sortDescending">What direction to sort</param>
        /// <param name="propertySelectorLambda">Function dexcribing what property to sort on.</param>
        /// <param name="isFirst">Is this the first item in the LINQ sort queue</param>
        /// <returns>The same list with sorting appended to it</returns>
        private static IOrderedEnumerable<T> AddSortLevel<T>(this IEnumerable<T> list, bool sortDescending, Func<T, object> propertySelectorLambda, bool isFirst)
        {
           
           if (isFirst)
              list = FirstSortLevel(list, sortDescending, propertySelectorLambda);
           else
              list = NextSortLevel(list as IOrderedEnumerable<T>, sortDescending, propertySelectorLambda);

           //Will definitely be IOrderedEnumerable here.
           return list as IOrderedEnumerable<T>;

        }//FirstSortLevel

        //------------------------------------------------------------------------//

        /// <summary>
        /// Adds a level of sorting to the linq queue (OrderBy)
        /// </summary>
        /// <typeparam name="T">Type of objects being sorted</typeparam>
        /// <param name="list">The Items being sorted</param>
        /// <param name="sortDescending">What direction to sort</param>
        /// <param name="propertySelectorLambda">Function dexcribing what property to sort on.</param>
        /// <param name="isFirst">Is this the first item in the LINQ sort queue</param>
        /// <returns>The same list with sorting appended to it</returns>
        private static IOrderedEnumerable<T> FirstSortLevel<T>(this IEnumerable<T> list, bool sortDescending, Func<T, object> propertySelectorLambda)
        {
           if (sortDescending)
              list = list.OrderByDescending(propertySelectorLambda);
           else
              list = list.OrderBy(propertySelectorLambda);

           //Will definitely be IOrderedEnumerable here.
           return list as IOrderedEnumerable<T>;
        }//FirstSortLevel

        //------------------------------------------------------------------------//

        /// <summary>
        /// Adds a level of sorting to the linq queue (ThenBy)
        /// </summary>
        /// <typeparam name="T">Type of objects being sorted</typeparam>
        /// <param name="list">The Items being sorted</param>
        /// <param name="sortDescending">What direction to sort</param>
        /// <param name="propertySelectorLambda">Function dexcribing what property to sort on.</param>
        /// <param name="isFirst">Is this the first item in the LINQ sort queue</param>
        /// <returns>The same list with sorting appended to it</returns>
        private static IOrderedEnumerable<T> NextSortLevel<T>(this IOrderedEnumerable<T> list, bool sortDescending, Func<T, object> propertySelectorLambda)
        {
           if (sortDescending)
              list = list.ThenByDescending(propertySelectorLambda);
           else
              list = list.ThenBy(propertySelectorLambda);

           return list;
        }//NextSortLevel

        //------------------------------------------------------------------------//

  }//Cls

How to use it: YOu pass in a list of sortRequests, each one will have the name of your property and the direction to sort.

Then the getPropertySelectorLambda will give you the Expression/Function used in the sorting.

Here's an example of something I use:

  private Func<Nurse, object> GetSortingPropertySelectorLambda(string field)
  {

     return (field.CamelToPascal()) switch
     {
        nameof(Nurse.Id) => p => p.Id,
        nameof(Nurse.PinNumber) => p => p.PinNumber,
        nameof(Nurse.FirstName) => p => p.FirstName,
        nameof(Nurse.LastName) => p => p.LastName,
        nameof(Nurse.AddressLine1) => p => p.AddressLine1,
        nameof(Nurse.AddressLine2) => p => p.AddressLine2,
        nameof(Nurse.AddressLine3) => p => p.AddressLine3,
        nameof(Nurse.AddressLine4) => p => p.AddressLine4,
        nameof(Nurse.PhoneNumber) => p => p.PhoneNumber,
        nameof(Nurse.PhoneNumber2) => p => p.PhoneNumber2,
        nameof(Nurse.EmailAddress) => p => p.EmailAddress,
        nameof(Nurse.DateOfBirth) => p => p.DateOfBirth,
        nameof(Nurse.PayrollNumber) => p => p.PayrollNumber,
        nameof(Nurse.Branch) => p => p.Branch.Name,
        _ => p => p.Id,
     };

  }//GetSortingPropertySelectorLambda

YOu could alternatively build that expression yourself from the sortRequest (Like you've done above).

The reason we have the "FirstSortLevel" method is because the first sort must be OrderBy but all the following sorts must be "ThenBy"s.

So all we're doing is creating/getting the expression which decides what property to filter on. Then just use regular LINQ to so the sorting (We don't build those queries as expressions).

Example Usage:

var sortedData = allData
    .AddSorting(sortList, GetSortingPropertySelectorLambda)
    .ToList();
ShanieMoonlight
  • 1,236
  • 1
  • 12
  • 22