.netlinqentity-framework-corelinq-expressionsexpressionvisitor

Implementing a LINQ Expression Visitor to Remove Pagination


I am trying to implement a LINQ Expression Visitor ([ExpressionVisitor][1]) for the following scenario: remove all pagination-related calls, such as Skip, Take, OrderBy, and OrderByDescending. What I came up so far was this:

public class RemovePaginationExpressionVisitor : ExpressionVisitor
{
    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        if (node.Method.Name == nameof(Enumerable.Skip) || node.Method.Name == nameof(Enumerable.Take) || node.Method.Name == nameof(Enumerable.OrderBy) || node.Method.Name == nameof(Enumerable.OrderByDescending))
        {
            return Expression.Constant(true);
        }    
        return base.VisitMethodCall(node);
    }
}

Here's how I use it:

var query = ctx.MyEntities.Where(x => x.Status == Status.Active).Skip(5).Take(10).OrderBy(x => x.Name);

var interceptor = new RemovePaginationExpressionVisitor();

var expression = interceptor.Visit(query.Expression);

This kind of works, meaning, it does get rid of these calls, but, it also removes any other conditions that I may have, like, in this case, Where(x => x.Status == Status.Active). So, two questions:

  1. How can I remove the method calls I don't want, while keeping everything else?
  2. How can I then use the resulting expression to generate another query?

Solution

  • I ended up implementing like this:

    public class RemovePaginationExpressionVisitor : ExpressionVisitor
    {
        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            if (node.Method.DeclaringType == typeof(Queryable) &&
                (node.Method.Name == nameof(Queryable.Take) ||
                 node.Method.Name == nameof(Queryable.Skip) ||
                 node.Method.Name == nameof(Queryable.OrderBy) ||
                 node.Method.Name == nameof(Queryable.OrderByDescending) ||
                 node.Method.Name == nameof(Queryable.ThenBy) ||
                 node.Method.Name == nameof(Queryable.ThenByDescending)))
            {
                return Visit(node.Arguments[0]);
            }
    
            return base.VisitMethodCall(node);
        }
    }
    

    And using it as this:

    var expression = interceptor.Visit(query.Expression);
    
    var modifiedQuery = query.Provider.CreateQuery<MyEntity>(expression);
    

    And it works like a charm! Beware, however, because it will remove all "paging" method calls, which may or may not be what you want. You may need to add a condition to check if it's at the "external" level, not in some inner query.