2

I am trying to perform a query against a list to give immediate results using an expression that is set elsewhere in the code, while a second thread goes off and uses it to get a full set of results from a database in a Linq query.

I know that the expression itself is OK as when I send it over the wire to the server side and apply it against an IQueryable then it will work. However, when applied on the client side the following error is produced:

System.InvalidOperationException: 'variable 'item' of type 'MissionControlSuite.Shared.Interface.Model.IBox' referenced from scope '', but it is not defined'

This code that performs the filtering:

public abstract class GridWithPaging<T> where T : class
{
    List<T> _items

    public Expression<Func<T, bool>> Filter { get; set; }
    public ObservableCollection<IGridFilter<T>> Filters { get; set; }

    // more code skipped for breveity

    public void FiltersChanged(object sender, EventArgs e)
    {
        Expression<Func<T, bool>> combined = item => true;
        foreach (var filter in Filters)
            combined = Expression.Lambda<Func<T, bool>>(
                Expression.AndAlso(filter.Expression.Body, combined.Body),
                combined.Parameters.Single());

        Filter = combined;
        NotifyPropertyChanged("Filter");
    }

    public void AddFilter(IGridFilter<T> filter)
    {
        Filters.Add(filter);
        NotifyPropertyChanged("Filters");
    }

    private void DoFiltering(int start)
    {
        var newView = _items.Skip(start);
        if(Filter != null)
            newView = newView.AsQueryable().Where(Filter);

        //some more code that acts on the filtered list
    }
}

The expression is set elsewhere like this:

Expression<Func<IBox, bool>> expression = item => item.BoxId.Contains(v);

var filter = new GridFilter<IBox>()
{
    Name = string.Format("{0} contains {1}", Property, Value),
    Expression = expression
};

Grid.AddFilter(filter);
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
c bunker
  • 55
  • 5
  • The method FiltersChanged needs to get the data from the sender parameter and not from the Filters. – jdweng Jun 21 '18 at 09:22

1 Answers1

1

This answer will help you: Combining two expressions (Expression<Func<T, bool>>)

To summarize: The problem is that the parameters, although with the same name, are not the same instance of the ParameterExpression - which is included in the comment to that answer.

The solution is to use a Visitor (code copied over from linked post):

public static Expression<Func<T, bool>> AndAlso<T>(
    this Expression<Func<T, bool>> expr1,
    Expression<Func<T, bool>> expr2)
{
    var parameter = Expression.Parameter(typeof (T));

    var leftVisitor = new ReplaceExpressionVisitor(expr1.Parameters[0], parameter);
    var left = leftVisitor.Visit(expr1.Body);

    var rightVisitor = new ReplaceExpressionVisitor(expr2.Parameters[0], parameter);
    var right = rightVisitor.Visit(expr2.Body);

    return Expression.Lambda<Func<T, bool>>(
        Expression.AndAlso(left, right), parameter);
}



private class ReplaceExpressionVisitor
    : ExpressionVisitor
{
    private readonly Expression _oldValue;
    private readonly Expression _newValue;

    public ReplaceExpressionVisitor(Expression oldValue, Expression newValue)
    {
        _oldValue = oldValue;
        _newValue = newValue;
    }

    public override Expression Visit(Expression node)
    {
        if (node == _oldValue)
            return _newValue;
        return base.Visit(node);
    }
}

Also note: the ORM provider (or another IQueryable you use) probably works because it translates that to text directly, whereas executing (Invokeing) this LambdaExpression causes the error as it's more strict, in a way. Some IQueryables can also accept simple string parameters and parse that on their own, which should indicate that they have a wider range of acceptable inputs.

Amiś
  • 179
  • 1
  • 5