当需要 Expression 但只允许 Func 时,如何在 Include .Where() 中使用内置的 Expression 来应用 Include Filter?

How can I use a built Expression in an Include .Where() to apply an Include Filter when it expects Expression but only allows Func?

我在尝试在嵌套集合上构建表达式树时遇到一些错误。我有 类:

// 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; }
}

并且我正在尝试查询我的数据库(通过 EF 上下文)以收集包含基于用户提交的 CourseFilter 的项目的所有 Courses:

[...]

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;
}

我的问题是查询的包含过滤器需要 superPredicate.Compile() 或 pre-compile/compile 时间,Visual Studio 提供错误:

`Argument 2: cannot convert from 'System.Linq.Expressions.Expression<System.Func<DTO.Item, bool>>' to 'System.Func<DTO.Item, bool>'`

但是,在运行时使用 superPredicate.Compile() 时出现错误:

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')

如果我尝试以这种方式过滤,同样的问题:

query = query
    .Include(d => d.Items)
    .Where(d => d.Items.Where(superPredicate.Compile()).Count() > 0);

是否可以解决此错误并能够使用包含过滤器?如果没有,有没有一种方法可以在单个查询中完成我想做的事情?

如果有帮助,这是将 SQL 服务器 SP 转换为 EF,其中 SP 本质上是:

@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]))'

正如在评论中发现的那样,主要问题是您尝试使用的功能(“过滤后的包含”)是 introduced in EF5,而您 are/were 显然是在较早的版本上版本。

此功能允许您使用 .Where 子句过滤包含的关系。但是,即使在升级之后,您仍然需要确保一直使用表达式树以使 EF 满意。

所以你在这里打电话的这条线 Compile() 对你来说方向错了。

query = query
    .Include(c => c.Items.Where(superPredicate.Compile()))
    .Where(c => c.Items.Count > 0);

因为 Items 在编译时被视为 IEnumerable<T> 而不是 IQueryable<T>,您自然会尝试通过将其强制转换为谓词来使 superPredicate 工作使用普通的 non-expression-tree lambda(Compile 将表达式转换为普通的 lambda)。但是,EF 不知道如何处理 lambda——它需要表达式树将其映射到 SQL.

所以你只需要走另一条路,通过调用 .AsQueryable 来使 c.Items 成为可查询的而不是可枚举的:

query.Include(c => c.Items.AsQueryable().Where(superPredicate))

这现在应该可以正常运行,并使用您的 superPredicate.

定义的过滤后的项目列表填充您的 Items 属性