0

I'm trying to write a class that helps create a LINQ query dynamically.

protected Func<T, TColumn> GetColumn;

public MyClass(Func<T, TColumn> getColumn)
{
    GetColumn = getColumn;
}

public virtual IQueryable<T> ApplyFilter(IQueryable<T> query)
{
    if (FilterMode == FilterModeMatchAny)
        return query.Where(x => FilterIds.Contains(GetColumn(x)));
    return query;
}

This class is called like this:

MyClass<Location, string> myClass = new MyClass<Location, string>(l => l.State);

var locations = myClass.ApplyFilter(DbContext.Locations);

However, the code above fails:

The LINQ expression 'DbSet
.Where(l => Invoke(__GetJoiningTables_0, l[Location])
.Any(xx => __FilterIds_1
.Contains(Invoke(__GetColumn_2, xx)
)))' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

The trouble appears to be the way I'm using GetColumn. So I changed the declaration of GetColumn so that it is now an expression.

protected Expression<Func<T, TColumn>> GetColumn;

The same argument being passed to my constructor can easily be converted to this type. I Only need to change the argument type.

But now how can I use this new GetColumn in my ApplyFilter() method?

Update:

Eventually, I also need to do the same thing to the following two expressions.

// Expressions to incorporate
protected Expression<Func<T, ICollection<TJoinTable>>> GetJoiningTables;
protected new Expression<Func<TJoinTable, TColumn>> GetColumn;

// Match any query
return query.Where(x => GetJoiningTables(x).Any(xx => FilterIds.Contains(GetColumn(xx))));

// Match all query
return query.Where(x => GetJoiningTables(x).Count(xx => FilterIds.Contains(GetColumn(xx))) >= FilterIds.Count());
Jonathan Wood
  • 65,341
  • 71
  • 269
  • 466

1 Answers1

1

Firstly, you need to keep the column as an expression, or c# will compile the lambda function and EF will be unable to extract which column it is;

protected Expression<Func<T, TColumn>> GetColumn;

public MyClass(Expression<Func<T, TColumn>> getColumn)
{
    GetColumn = getColumn;
}

Now, you could use the static methods of Expression to build an entire filter expression by hand. But in your case there's a shortcut. Since you can reuse the parameter and body of the input expression (eg x => x.Column), and wrap it in your Contains call (eg x => FilterIds.Contains(x.Column)).

EDIT:

Since you've now revealed that FilterIds is an IEnumerable<T>, then FilterIds.Contains() is actually the static extension method Enumerable.Contains(). The simplest way to find a matching generic static method is to create a matching delegate.

public virtual IQueryable<T> ApplyFilter(IQueryable<T> query)
{
    if (FilterMode == FilterModeMatchAny)
        return query.Where(
            Expression.Lambda<Func<T, bool>>(
                Expression.Call(
                    null,
                    new Func<IEnumerable<TColumn>,TColumn,bool>(Enumerable.Contains).Method,
                    Expression.Constant(FilterIds),
                    GetColumn.Body),
                GetColumn.Parameters)
        );
    return query;
}

EDIT:

.Where(x => GetJoiningTables(x).Any(...

Ok that's a can of worms.... I assume that what you're trying to do here is take a collection of navigations to other tables & columns, and apply a filter to that?

I find it helps to build an example Expression that you're trying to achieve. I assume you're trying to build expressions like;

Expression<Func<T,bool>> filter = t => 
    Enumerable.Any(t.Child1, c => FilterIds.Contains(c.ChildCol))
    || Enumerable.Any(t.Child2, c => FilterIds.Contains(c.OtherChildCol))
    ... ;

Expression<Func<T,bool>> filter = t => 
    Enumerable.Count(t.Child1, c => FilterIds.Contains(c.ChildCol))
    + Enumerable.Count(t.Child2, c => FilterIds.Contains(c.OtherChildCol))
    ... ;

I suggest that you write this code, let the c# compiler turn them into Expression graphs, and use the debugger to see what those graphs look like.

If possible, I'd recommend trying to find a way to "give to cesar what is caesar's". And assemble fragments of Expressions by inlining or transforming some template Expression.

Jonathan Wood
  • 65,341
  • 71
  • 269
  • 466
Jeremy Lakeman
  • 9,515
  • 25
  • 29
  • Thanks. However `FilterIds.GetType().GetMethod("Contains")` is returning `null`. (Note that `FilterIds` is not `null`.) – Jonathan Wood Jul 13 '20 at 03:08
  • Either needs the right binding flags, or the right argument types. You didn't define `FilterIds` and I didn't test it. – Jeremy Lakeman Jul 13 '20 at 04:01
  • `FilterIds` is of type `IEnumerable`, where `T` is `string` in this case. The `Contains` method is part of `Enumerable`. So I can use `typeof(Enumerable).GetMethod("Contains")`, but that returns an ambiguous match exception. And good luck trying to figure out the type parameters to a generic extension method. What a pain! – Jonathan Wood Jul 13 '20 at 04:04
  • Ah, that's not `FilterIds.Contains` then, it's the static extension method `Enumerable.Contains(FilterIds, ...)`, updating answer. – Jeremy Lakeman Jul 13 '20 at 04:07
  • Damn! That works. Not sure I can even understand it but I'll dig deeper. I need to add a few more. Thanks! – Jonathan Wood Jul 13 '20 at 04:20
  • If you're doing something like this a lot, you could try this; https://stackoverflow.com/questions/62687811/how-can-i-convert-a-custom-function-to-a-sql-expression-for-entity-framework-cor/62708275#62708275 – Jeremy Lakeman Jul 13 '20 at 04:22
  • FYI, I first saw the delegate trick in `GetMethodInfo` here; https://github.com/microsoft/referencesource/blob/master/System.Core/System/Linq/IQueryable.cs#L45 (note that the actual delegate creation is implicit) – Jeremy Lakeman Jul 13 '20 at 04:25
  • I've been programming for decades and have written some fairly sophisticated code. But understanding the these expressions need to be written is coming slowly to me. I looked at your other link, but am not sure what is happening there and wouldn't want to do anything that could break in a future release. What would you charge me to convert two additional, somewhat more complex expressions like you did above? I've added them to my question if you want to see. – Jonathan Wood Jul 13 '20 at 05:07
  • Thanks again for all your help. As much as I love this approach, it's just getting a little too hairy for comfort, and a workaround isn't too bad. If you'd like to earn a few more points, perhaps you could look at my [follow up question](https://stackoverflow.com/questions/62879661/applying-an-expression-to-a-linq-where-clause) to this. I know I need to use `Expression.Lambda<>` somehow but I can't seem to find the right syntax. – Jonathan Wood Jul 13 '20 at 15:58