如何在DBSet中用关键词列表过滤?EF Core查询翻译异常
我在Controller中编写了如下查询代码:
var query = _context.Jobs.Where(x => x.Category.Equals(filterParams.Category) || filterParams.Query.Any(val => x.Title.Contains(val)));
FilterParams类定义如下:
public class FilterParams { public string[] Query { get; set; } public CategoryEnum Category { get; set; } }
其中Category过滤功能正常,但Title部分的过滤未达到预期效果。我希望当filterParams.Query为['FOO', 'BAR']时,返回标题包含这些关键词的实体(如“foo alice bar”“foo bar bob”)。
当前遇到的错误为:
System.InvalidOperationException: The LINQ expression 'val => EntityShaperExpression:
ynyn_be.Models.Job
ValueBufferExpression:
ProjectionBindingExpression: EmptyProjectionMember
IsNullable: False
.Title.Contains(val)' 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 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'.
我感到困惑的是,在过滤之后我已经调用了Skip()、Take()和ToListAsync(),为何仍被要求显式启用客户端评估?如何实现基于数组的过滤?
EF Core的查询执行逻辑是先解析整个表达式树并转换为SQL语句,再执行查询。你的代码中filterParams.Query.Any(val => x.Title.Contains(val))是嵌套LINQ操作,EF Core无法将这种客户端数组的Any与数据库字段Contains的组合逻辑翻译成对应SQL,因此报错。
至于你提到的Skip()/Take()/ToListAsync(),这些都是查询执行阶段的操作,不会改变EF Core对Where子句的表达式解析规则——只要Where里的逻辑无法被翻译,不管后面加什么执行方法,都会触发错误。
方法一:构建多条件OR查询(推荐,全数据库端执行)
把数组中的每个关键词转换成Title.Contains(keyword)的OR条件,EF Core可直接将这种逻辑翻译成SQL的WHERE (Title LIKE '%FOO%' OR Title LIKE '%BAR%')。
借助LinqKit库可以简化表达式合并:
// 先引用LinqKit.Microsoft.EntityFrameworkCore包 var query = _context.Jobs.AsExpandable() .Where(x => x.Category.Equals(filterParams.Category)); if (filterParams.Query != null && filterParams.Query.Length > 0) { Expression<Func<Job, bool>> titleFilter = null; foreach (var keyword in filterParams.Query) { var currentKeyword = keyword; // 避免闭包陷阱 if (titleFilter == null) { titleFilter = job => job.Title.Contains(currentKeyword); } else { titleFilter = titleFilter.Or(job => job.Title.Contains(currentKeyword)); } } // 合并Category和Title的OR条件 query = query.Where(job => job.Category.Equals(filterParams.Category) || titleFilter.Invoke(job)); } var result = await query.Skip(...).Take(...).ToListAsync();
方法二:客户端评估(仅适合数据量小的场景)
如果符合Category过滤的数据量不大,可以先将数据拉到客户端再做Title过滤。这种方式无需修改表达式,但会加载所有符合Category的数据到内存,数据量大时性能差:
var result = await _context.Jobs .Where(x => x.Category.Equals(filterParams.Category)) .AsEnumerable() // 切换到客户端评估 .Where(x => filterParams.Query.Any(val => x.Title.Contains(val))) .Skip(...) .Take(...) .ToListAsync();
方法三:使用EF.Functions.Like(支持通配符)
如果需要更灵活的匹配规则,可用EF.Functions.Like替代Contains,支持自定义通配符:
var titleFilter = filterParams.Query.Aggregate( (Expression<Func<Job, bool>>)null, (acc, keyword) => { var current = keyword; return acc == null ? (Expression<Func<Job, bool>>)(job => EF.Functions.Like(job.Title, $"%{current}%")) : acc.Or(job => EF.Functions.Like(job.Title, $"%{current}%")); }); var query = _context.Jobs.AsExpandable() .Where(job => job.Category.Equals(filterParams.Category) || titleFilter.Invoke(job));
内容的提问来源于stack exchange,提问作者MA-Moustache

