0

I want to create an Extension method which mimics this, https://dejanstojanovic.net/aspnet/2019/january/filtering-and-paging-in-aspnet-core-web-api/

However, I want to add an OrderBy (for ColumnName) after StartsWith, how would I conduct this?

tried adding following and did not work .OrderBy(parameter)

Example:

return persons.Where(p => p.Name.StartsWith(filterModel.Term ?? String.Empty, StringComparison.InvariantCultureIgnoreCase))
   .OrderBy(c=>c.Name)  
   .Skip((filterModel.Page-1) * filter.Limit)  
   .Take(filterModel.Limit);  


public static class PaginateClass
{
    static readonly MethodInfo startsWith = typeof(string).GetMethod("StartsWith", new[] { typeof(string), typeof(System.StringComparison) });

    public static IEnumerable<T> Paginate<T>(this IEnumerable<T> input, PageModel pageModel, string columnName) where T : class
    {
        var type = typeof(T);
        var propertyInfo = type.GetProperty(columnName);
        //T p =>
        var parameter = Expression.Parameter(type, "p");
        //T p => p.ColumnName
        var name = Expression.Property(parameter, propertyInfo);
        // filterModel.Term ?? String.Empty
        var term = Expression.Constant(pageModel.Term ?? String.Empty);
        //StringComparison.InvariantCultureIgnoreCase
        var comparison = Expression.Constant(StringComparison.InvariantCultureIgnoreCase);
        //T p => p.ColumnName.StartsWith(filterModel.Term ?? String.Empty, StringComparison.InvariantCultureIgnoreCase)
        var methodCall = Expression.Call(name, startsWith, term, comparison);

        var lambda = Expression.Lambda<Func<T, bool>>(methodCall, parameter);


            return input.Where(lambda.Compile()) //tried adding this and did not work .OrderBy(parameter)  
            .Skip((pageModel.Page - 1) * pageModel.Limit)
            .Take(pageModel.Limit);

    }

Other items PageModel:

public class PageModel
{

    public int Page { get; set; }
    public int Limit { get; set; }
    public string Term { get; set; }

    public PageModel()
    {
        this.Page = 1;
        this.Limit = 3;
    }

    public object Clone()
    {
        var jsonString = JsonConvert.SerializeObject(this);
        return JsonConvert.DeserializeObject(jsonString, this.GetType());
    }
}

Dynamic Linq to Entities Orderby with Pagination

  • you want `OrderBy` on dynamic column name supplied, either create an Expression same as `StartsWith`, it will be `MethodCallExpression` or check `System.Linq.Dynamic`, they provide extension method out of the box – Mrinal Kamboj Aug 21 '19 at 05:38
  • hi @MrinalKamboj company does not allow linqdymiac, however, feel free to give first suggestion in answer, and I can send points, thanks ! –  Aug 21 '19 at 05:42
  • check [System.Linq.Dynamic](https://github.com/kahanu/System.Linq.Dynamic/wiki/Dynamic-Expressions) – Mrinal Kamboj Aug 21 '19 at 05:42
  • that's a very good open source library, you guys have blocked it – Mrinal Kamboj Aug 21 '19 at 05:43
  • ok, I will check it out, is it this? var test = MethodCallExpression.Call(typeof(Queryable), "OrderBy", new[] { type, propertyInfo.PropertyType }); return input.Where(lambda.Compile()).test .Skip((pageModel.Page - 1) * pageModel.Limit) .Take(pageModel.Limit); –  Aug 21 '19 at 05:45
  • It will not work like this, you shall compile this also in Lambda and supply you collection to it, it will get fluent plug in as you are expecting – Mrinal Kamboj Aug 21 '19 at 05:55
  • @MrinalKamboj yeah, totally stuck have this, static readonly MethodInfo orderBy = typeof(string).GetMethod("OrderBy", new[] { typeof(string)}); then trying to call MethodCall orderby var methodCall2 = Expression.Call(name, orderBy, propertyInfo); –  Aug 21 '19 at 06:05
  • checkout **Option2** in the same answer, that may fit your use case better, as it just creates dynamically the Func required by the `IEnumerable.OrderBy` – Mrinal Kamboj Aug 21 '19 at 06:47

1 Answers1

4

Check the sample code for the solution:

void Main()
{
    var queryableRecords = Product.FetchQueryableProducts();

    Expression expression = queryableRecords.OrderBy("Name");

    var func = Expression.Lambda<Func<IQueryable<Product>>>(expression)
                         .Compile();

    func().Dump();
}

public class Product
{
    public int Id { get; set; }

    public string Name { get; set; }

    public static IQueryable<Product> FetchQueryableProducts()
    {
        List<Product> productList = new List<Product>()
        {
          new Product {Id=1, Name = "A"},
          new Product {Id=1, Name = "B"},
          new Product {Id=1, Name = "A"},
          new Product {Id=2, Name = "C"},
          new Product {Id=2, Name = "B"},
          new Product {Id=2, Name = "C"},
        };

        return productList.AsQueryable();
    }
}

public static class ExpressionTreesExtesion
{

    public static Expression OrderBy(this IQueryable queryable, string propertyName)
    {
        var propInfo = queryable.ElementType.GetProperty(propertyName);

        var collectionType = queryable.ElementType;

        var parameterExpression = Expression.Parameter(collectionType, "g");
        var propertyAccess = Expression.MakeMemberAccess(parameterExpression, propInfo);
        var orderLambda = Expression.Lambda(propertyAccess, parameterExpression);
        return Expression.Call(typeof(Queryable),
                               "OrderBy",
                               new Type[] { collectionType, propInfo.PropertyType },
                               queryable.Expression,
                               Expression.Quote(orderLambda));

    }


}

Result

enter image description here

How it Works:

Created an expression using extension method on the Queryable type, which internally calls OrderBy method of the Queryable type, expecting IQueryable to be the Input, along with the field name and thus runs the ordering function and Ordered collection is the final Output

Option 2:

This may fit your use case better, here instead of calling OrderBy method, we are creating the Expression<Func<T,string>> as an extension method to the IEnumerable<T>, which can then be compiled and supplied to the OrderBy Call, as shown in the example and is thus much more intuitive and simple solution:

Creating Expression:

public static class ExpressionTreesExtesion
{
    public static Expression<Func<T,string>> OrderByExpression<T>(this IEnumerable<T> enumerable, string propertyName)
    {
        var propInfo = typeof(T).GetProperty(propertyName);

        var collectionType = typeof(T);

        var parameterExpression = Expression.Parameter(collectionType, "x");
        var propertyAccess = Expression.MakeMemberAccess(parameterExpression, propInfo);
        var orderExpression = Expression.Lambda<Func<T,string>>(propertyAccess, parameterExpression);
        return orderExpression;
    }
}

How to Call:

var ProductExpression = records.OrderByExpression("Name");

var result  = records.OrderBy(ProductExpression.Compile());

ProductExpression.Compile() above will compile into x => x.Name, where column name is supplied at the run-time

Please note in case the ordering field can be other types beside string data type, then make that also generic and supply it while calling extension method, only condition being property being called shall have the same type as supplied value, else it will be a run-time exception, while creating Expression

Edit 1, how to make the OrderType field also generic

public static Expression<Func<T, TField>> OrderByFunc<T,TField>(this IEnumerable<T> enumerable, string propertyName)
    {
        var propInfo = typeof(T).GetProperty(propertyName);

        var collectionType = typeof(T);

        var parameterExpression = Expression.Parameter(collectionType, "x");
        var propertyAccess = Expression.MakeMemberAccess(parameterExpression, propInfo);
        var orderExpression = Expression.Lambda<Func<T, TField>>(propertyAccess, parameterExpression);
        return orderExpression;
    }

How to call:

Now both the types need to be explicitly supplied, earlier were using generic type inference from IEnumerable<T>:

// For Integer Id field

var ProductExpression = records.OrderByFunc<Product,int>("Id");

// For string name field

var ProductExpression = records.OrderByFunc<Product,string>("Name");

Mrinal Kamboj
  • 11,300
  • 5
  • 40
  • 74
  • ok, we're actually using IEnumerable, but I will check it out, IEnumerable does not contain definition for ElementType, thanks! –  Aug 21 '19 at 06:13
  • Check Option 2, that shall work, ideally you shall use `IEnumerable`, then you don't need Element type, since we can directly access the Type. Also please note `IQueryable is a type of `IEnumerable`, which is the main base interface – Mrinal Kamboj Aug 21 '19 at 06:55
  • hi Mrinal, I apologize, how do I make the order by variable, so it accepts, ints, strings, and other data types? should I just convert everything to string in method, or is there a better way to generalize? Let me know if I should open up another question, I had coworkers thumbs up this question also :) –  Aug 22 '19 at 22:09
  • @MattSmith check the `Edit 1, how to make the OrderType field also generic` update and let me know if you have any other questions – Mrinal Kamboj Aug 23 '19 at 03:49