0

Based on API, I can have multiple parameters which can be used in order by. There is a function which creates a dynamic order by parameter as a string. I want to use this in .OrderBy but not sure how to do this.

API Call:

{{url}}?keyword=singer&page=12&size=5&sortby=LastName&sortby=FirstName

Code:

public CallCenterPageResult<CallCenterCustomerSummary> GetCustomers(int page, int pageSize, IEnumerable<SortParameter> sortParameters, string keyword)
{
    using (var ctx = new EFCallCenterContext())
    {
        var customerDetails = ctx.CallCenterCustomers
                                 .Where(ccs => ccs.IsDeleted == false && (ccs.FirstName.Contains(keyword) || ccs.LastName.Contains(keyword) || ccs.Phone.Contains(keyword)))
                                 .OrderBy(sortParameters.ToOrderBy()) // "LastName ASC, FirstName ASC"
                                 .Skip(pageSize * (page - 1)).Take(pageSize)
                                 .ToList();

        return customerDetails;
    }
}

Extension Method to get order by:

static class RepositoryExtensions
{
    public static string ToOrderBy(this IEnumerable<SortParameter> parameters)
    {
        return string.Join(", ", parameters.Select(p => p.SortBy + (p.Descending ? " DESC" : " ASC")));
    }
}

Output:

"LastName ASC, FirstName ASC"

Extension method to accept dynamic LINQ:

public static class Extension
{
    public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string property)
    {
        return ApplyOrder<T>(source, property, "OrderBy");
    }
    public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> source, string property)
    {
        return ApplyOrder<T>(source, property, "OrderByDescending");
    }
    public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> source, string property)
    {
        return ApplyOrder<T>(source, property, "ThenBy");
    }
    public static IOrderedQueryable<T> ThenByDescending<T>(this IOrderedQueryable<T> source, string property)
    {
        return ApplyOrder<T>(source, property, "ThenByDescending");
    }
    static IOrderedQueryable<T> ApplyOrder<T>(IQueryable<T> source, string property, string methodName)
    {
        var props = property.Split('.');
        var type = typeof(T);
        var 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); // Errors out here.
            type = pi.PropertyType;
        }
        var delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type);
        var lambda = Expression.Lambda(delegateType, expr, arg);

        var result = typeof(Queryable).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 });
        return (IOrderedQueryable<T>)result;
    }
}

Error:

System.ArgumentNullException: Value cannot be null.
Parameter name: property

Scrren shot: enter image description here

This is the first time working with this complex query so not sure how to do this. I can add more info if needed.

CSharper
  • 1,632
  • 3
  • 20
  • 40
  • Your code under "Extension method to accept dynamic LINQ" doesn't allow to order by string like "LastName ASC, FirstName DESC", only by simple property name ("LastName" or "FirstName"). But you can improve it to work like you are expecting. – Evk Oct 30 '17 at 16:28
  • This exception is thrown because `pi` is `null`. First, you must ask yourself, Do you expect `pi` to not be null? why? Investigate from there – Sam I am says Reinstate Monica Oct 30 '17 at 16:32
  • @SamIam Let me investigate this further. Thank you for suggestion. – CSharper Oct 30 '17 at 16:37
  • @SamIam I changed `var props = property.Split('.');` to `var props = property.Split(',');`. When I am passing two parameters like `LastName, FirstName`, `pi` only populates for first parameter. For second it fails. Any idea? – CSharper Oct 30 '17 at 17:23

3 Answers3

0

It looks like the error occurs because pi is null. And it is null because, I would assume, the class standing in for the T generic doesn't have a property named LastName ASC, FirstName ASC. I would try something like the following:

var props = property.Split(",");
... //this code stays the same
foreach(string prop in props) {
    var propNameAndDirection = prop.Split(" ");
    PropertyInfo pi = type.GetProperty(propNameAndDirection[0]);
    ... //continue as necessary, using propNameAndDirection[1] 
    ... //to decide OrderBy or OrderByDesc call

Hopefully this sets you in the right direction.

ScoobyDrew18
  • 613
  • 9
  • 20
  • That's what I am doing plus instead of `LastName ASC, FirstName ASC`, I am just using `LastName, FirstName`. Here, for `LastName` pi populates but second time fails for `FirstName`. Working on that one. – CSharper Oct 30 '17 at 17:13
  • Surprisingly, whatever is first property that only works. i.e. `LastName, FirstName` than `LastName` will populate. If `FirstName, LastName` than `FirstName` will populate and `LasName` will fail. – CSharper Oct 30 '17 at 17:18
  • Hard to tell from your screenshot, but is there whitespace after the comma in the `property` variable? – ScoobyDrew18 Oct 30 '17 at 17:24
  • Nope. it is `Lastname,FirstName`. I am also using `prop.Trim()` just for precaution but same error. `foreach (string prop in props) { // use reflection (not ComponentModel) to mirror LINQ PropertyInfo pi = type.GetProperty(prop.Trim()); expr = Expression.Property(expr, pi); type = pi.PropertyType; }` – CSharper Oct 30 '17 at 17:31
-1

After some trial and error, I am able to find an answer.

Tested with followings:

.OrderBy("LastName ASC, FirstName ASC")
.OrderBy("LastName ASC")
.OrderBy("LastName ASC,FirstName DESC")

Linq:

public CallCenterPageResult<CallCenterCustomerSummary> GetCustomers(int page, int pageSize, IEnumerable<SortParameter> sortParameters, string keyword)
{
    using (var ctx = new EFCallCenterContext())
    {
        var customerDetails = ctx.CallCenterCustomers
                                 .Where(ccs => ccs.IsDeleted == false && (ccs.FirstName.Contains(keyword) || ccs.LastName.Contains(keyword) || ccs.Phone.Contains(keyword)))
                                 .OrderBy(o => o.Equals(sortParameters.ToOrderBy()))
                                 .Skip(pageSize * (page - 1)).Take(pageSize)
                                 .ToList();

        return customerDetails;
    }
}

Helper Class:

public static class OrderByHelper
{
    public static IEnumerable<T> OrderBy<T>(this IEnumerable<T> enumerable, string orderBy)
    {
        return enumerable.AsQueryable().OrderBy(orderBy).AsEnumerable();
    }

    public static IQueryable<T> OrderBy<T>(this IQueryable<T> collection, string orderBy)
    {
        foreach (var orderByInfo in ParseOrderBy(orderBy))
        {
            collection = ApplyOrderBy(collection, orderByInfo);
        }

        return collection;
    }

    private static IQueryable<T> ApplyOrderBy<T>(IQueryable<T> collection, OrderByInfo orderByInfo)
    {
        var props = orderByInfo.PropertyName.Split('.');
        var type = typeof (T);

        var arg = Expression.Parameter(type, "x");
        Expression expr = arg;
        foreach (var prop in props)
        {
            var pi = type.GetProperty(prop);
            expr = Expression.Property(expr, pi);
            type = pi.PropertyType;
        }
        var delegateType = typeof (Func<,>).MakeGenericType(typeof (T), type);
        var lambda = Expression.Lambda(delegateType, expr, arg);
        string methodName;

        if (!orderByInfo.Initial && collection is IOrderedQueryable<T>)
        {
            methodName = orderByInfo.Direction == SortDirection.Ascending ? "ThenBy" : "ThenByDescending";
        }
        else
        {
            methodName = orderByInfo.Direction == SortDirection.Ascending ? "OrderBy" : "OrderByDescending";
        }

        return (IOrderedQueryable<T>) typeof (Queryable).GetMethods().Single(
            method => method.Name == methodName
                      && method.IsGenericMethodDefinition
                      && method.GetGenericArguments().Length == 2
                      && method.GetParameters().Length == 2)
            .MakeGenericMethod(typeof (T), type)
            .Invoke(null, new object[] {collection, lambda});
    }

    private static IEnumerable<OrderByInfo> ParseOrderBy(string orderBy)
    {
        if (string.IsNullOrEmpty(orderBy))
        {
            yield break;
        }

        var items = orderBy.Split(',');
        var initial = true;
        foreach (var item in items)
        {
            var pair = item.Trim().Split(' ');

            if (pair.Length > 2)
            {
                throw new ArgumentException(
                    $"Invalid OrderBy string '{item}'. Order By Format: Property, Property2 ASC, Property2 DESC");
            }

            var prop = pair[0].Trim();

            if (string.IsNullOrEmpty(prop))
            {
                throw new ArgumentException(
                    "Invalid Property. Order By Format: Property, Property2 ASC, Property2 DESC");
            }

            var dir = SortDirection.Ascending;

            if (pair.Length == 2)
            {
                dir = "desc".Equals(pair[1].Trim(), StringComparison.OrdinalIgnoreCase)
                    ? SortDirection.Descending
                    : SortDirection.Ascending;
            }

            yield return new OrderByInfo {PropertyName = prop, Direction = dir, Initial = initial};

            initial = false;
        }
    }

    private class OrderByInfo
    {
        public string PropertyName { get; set; }
        public SortDirection Direction { get; set; }
        public bool Initial { get; set; }
    }

    private enum SortDirection
    {
        Ascending = 0,
        Descending = 1
    }

Referances:

Dynamic LINQ OrderBy on IEnumerable<T>

http://aonnull.blogspot.com/2010/08/dynamic-sql-like-linq-orderby-extension.html

CSharper
  • 1,632
  • 3
  • 20
  • 40
-1

If I understood the problem correctly.

Expression<Func<TEntity, TKey>> genericParameter = null;

genericParameter = x => x.foo;

var customerDetails = ctx.CallCenterCustomers
                                 .Where(ccs => ccs.IsDeleted == false && (ccs.FirstName.Contains(keyword) || ccs.LastName.Contains(keyword) || ccs.Phone.Contains(keyword)))
                                 .OrderBy(genericParameter)