'How can I use a built Expression in an Include .Where() to apply an Include Filter when it expects Expression but only allows Func?
I'm getting some errors trying to build expression trees on nested collections. I have classes:
// not DB entity
public FilterTerm
{
public string ComparisonOperatorA { get; set; }
public decimal? ValueA { get; set; }
public string ComparisonOperatorB { get; set; }
public decimal? ValueB { get; set; }
public string ComparisonOperatorC { get; set; }
public decimal? ValueC { get; set; }
}
// not DB entity
public CourseFilter
{
public string Name { get; set; }
public string Type { get; set; }
public List<FilterTerm> FilterTerms { get; set; }
}
// DB entity
public Item
{
public int Id { get; set; }
public string Name { get; set; }
public decimal? SkillLevelA { get; set; }
public decimal? SkillLevelB { get; set; }
public decimal? SkillLevelC { get; set; }
[ForeignKey("Course")]
public int CourseId { get; set; }
public virtual Course Course { get; set; }
}
// DB entity
public Course
{
public int Id { get; set; }
public string Name { get; set; }
public string Type { get; set; }
public virtual ICollection<Item> Items { get; set; }
}
and I'm trying to query my db (via EF context) to collect all of the Course
s that contain items based on a CourseFilter
submitted by a user:
[...]
CourseFilter searchParameters = GetSearchParameters();
var coursesQuery = context.GetAll<Course>(); // reutrns IQueryable<Course>
coursesQuery = coursesQuery.ApplyFilters(searchParameters);
var courses = await coursesQuery.ToListAsync();
[...]
private IQueryable<Course> ApplyFilters(IQueryable<Course> query, CourseFilter searchParameters)
{
query = query.Where(c => c.Name.Contains(searchParameters.Name));
query = query.Where(c => c.Type == searchParameters.Type);
Expression<Func<Item, bool>> superPredicate = PredicateBuilder.False<Item>()
foreach (var filterTerm in searchParameters.FilterTerms)
{
Expression<Func<Item, bool>> subPredicate = PredicateBuilder.True<Item>();
subPredicate = subPredicate.And<Item>(GetDynamicPredicate(i => i.SkillLevelA, filterTerm.ValueA, filterTerm.ComparisonOperatorA));
subPredicate = subPredicate.And<Item>(GetDynamicPredicate(i => i.SkillLevelB, filterTerm.ValueB, filterTerm.ComparisonOperatorB));
subPredicate = subPredicate.And<Item>(GetDynamicPredicate(i => i.SkillLevelC, filterTerm.ValueC, filterTerm.ComparisonOperatorC));
superPredicate = superPredicate.Or(subPredicate);
}
query = query.Include(c => c.Items.Where(superPredicate.Compile()))
.Where(c => c.Items.Count > 0);
return query;
}
private Expression<Func<Item, bool>> GetDynamicPredicate<TValue>(
Expression<Func<Item, decimal?>> property, decimal? value, string comparisonOperator)
{
Expression<Func<Item, bool>> predicate = null;
switch (comparisonOperator)
{
case "<":
predicate = Expression.Lambda<Func<Item, bool>>(
Expression.LessThan(property.Body, Expression.Constant(value, typeof(decimal?))),
property.Parameters[0]);
break;
[...]
}
return predicate;
}
My problem here is that the Include Filter for the query requires the superPredicate.Compile()
or, pre-compile/compile time, Visual Studio provides the error:
`Argument 2: cannot convert from 'System.Linq.Expressions.Expression<System.Func<DTO.Item, bool>>' to 'System.Func<DTO.Item, bool>'`
However, when using the superPredicate.Compile()
during runtime I get the error:
Expression of type 'System.Func`2[DTO.Item,System.Boolean]' cannot be used for parameter of type 'System.Linq.Expressions.Expression`1[System.Func`2[DTO.Item,System.Boolean]]' of method 'System.Linq.IQueryable`1[DTO.Item] Where[DTO.Item](System.Linq.IQueryable`1[DTO.Item], System.Linq.Expressions.Expression`1[System.Func`2[DTO.Item,System.Boolean]])' (Parameter 'arg1')
Same issues if I try to filter this way:
query = query
.Include(d => d.Items)
.Where(d => d.Items.Where(superPredicate.Compile()).Count() > 0);
Is it possible to resolve this error and be able to use the Include Filter? If not, is there a way I can do what I want to do in a single query?
If it helps, this is to convert a SQL Server SP to EF, where the SP is in essence:
@sql = 'SELECT *
FROM dbo.Course c
INNER JOIN dbo.Item i
on i.CourseId = c.Id
WHERE (c.Name LIKE @CourseName)
AND (c.Type = @CourseType)'
[below is added using a cursor over N filters and covers <,<=,>,>=,=
(represented as integers) for SkillLevelA,B,C on Item, and emulated
in the ApplyFilters and GetDynamicQuery methods]
@sql = @sql + 'AND ((' + @CO_A + ' = 1 AND (' + @ValueA' < i.SkillLevelA)) OR ([etc]))'
Solution 1:[1]
As discovered in the comments, the main problem is that the feature you are trying to use ("filtered include") is one that was introduced in EF5, and you are/were evidently on an earlier version.
This feature is what allows you to filter the included relationships by using a .Where
clause. However, even after upgrading, you still need to make sure you are using expression trees all the way through to make EF happy.
So this line here where you are calling Compile()
is going in the wrong direction for you.
query = query
.Include(c => c.Items.Where(superPredicate.Compile()))
.Where(c => c.Items.Count > 0);
Because Items
is seen at compile time as an IEnumerable<T>
instead of an IQueryable<T>
, you naturally tried to make your superPredicate
work by coercing it into a predicate that uses a normal non-expression-tree lambda (Compile
converts the expression into a normal lambda). However, EF won't know what to do with the lambda -- it needs the expression tree to map it to SQL.
So you simply have to go the other way and make c.Items
a queryable instead of an enumerable by calling .AsQueryable
instead:
query.Include(c => c.Items.AsQueryable().Where(superPredicate))
This should now function properly and populate your Items
property with the filtered list of items as defined by your superPredicate
.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Kirk Woll |