EF Core 5.0 数组Any搭配Contains/StartsWith等方法无法翻译为SQL问题
EF Core 3.0及以上版本废弃了隐式客户端评估机制,2.2版本中无法翻译的LINQ查询会自动下载全量数据到内存执行过滤,3.0+版本要求所有查询逻辑默认必须能翻译为SQL,否则就抛出你遇到的报错。
你尝试将数组转为ToList()/AsEnumerable()不生效的原因是:你修改的只是筛选条件里的集合类型,整个查询仍然处于IQueryable的服务端评估上下文,EF Core还是会尝试把完整表达式翻译为SQL,自然不会成功。
单元素数组的Any + Equals能正常运行,是因为EF Core做了特殊优化,会把这种表达式简化为等于判断直接生成对应的SQL,但是多元素数组搭配Contains/StartsWith/EndsWith的嵌套逻辑,EF Core 5没有内置对应的翻译规则,所以无法生成SQL。
方案1:动态构建表达式树(推荐,服务端执行性能好)
直接把数组的多个匹配项拼接为OR连接的条件,EF Core可以直接翻译为SQL,示例代码:
// 定义匹配关键词数组 var keywords = new[] { "os", "man" }; // 构建参数表达式 u => var parameter = Expression.Parameter(typeof(User), "u"); // 获取Email属性 var emailProperty = Expression.Property(parameter, nameof(User.Email)); // 拼接OR条件 Expression body = Expression.Constant(false); foreach (var keyword in keywords) { // 构造 u.Email.Contains(keyword) 表达式 var containsMethod = typeof(string).GetMethod(nameof(string.Contains), new[] { typeof(string) }); var keywordConstant = Expression.Constant(keyword); var containsCall = Expression.Call(emailProperty, containsMethod, keywordConstant); // 拼接OR body = Expression.OrElse(body, containsCall); } // 生成最终的Lambda表达式 var lambda = Expression.Lambda<Func<User, bool>>(body, parameter); // 执行查询 var foo = await _baseRepository.GetQueryable<User>().Where(lambda).ToListAsync();
上述代码会生成类似SELECT * FROM User WHERE Email LIKE '%os%' OR Email LIKE '%man%'的SQL,完全在数据库端执行,性能最优。
如果需要用StartsWith/EndsWith/Equals逻辑,只需要替换上述代码中调用的字符串方法即可,比如把nameof(string.Contains)换成nameof(string.StartsWith)。
不等于逻辑的处理
如果你要实现不等于/不包含的匹配,只需要把上述表达式构建中的OrElse换成AndAlso,同时给每个匹配条件加Expression.Not包裹即可,比如要匹配Email既不等于os也不等于man的用户,生成的表达式就是u => u.Email != "os" AND u.Email != "man",EF Core可以正常翻译。
方案2:显式开启客户端评估(仅适合小表场景)
如果你的User表数据量很小,可以直接把数据下载到内存后再过滤,只需要在查询前调用AsAsyncEnumerable切换到客户端评估上下文即可:
var foo = await _baseRepository.GetQueryable<User>() .AsAsyncEnumerable() .Where(i => new[] { "os", "man" }.Any(j => i.Email.Contains(j))) .ToListAsync();
注意这个方案会先把全量User数据加载到内存再执行过滤,数据量大的时候性能极差,不推荐生产环境使用。
方案3:使用EF.Functions.Like拼接条件
如果关键词是固定的,你也可以手动拼接多个Like条件:
var foo = await _baseRepository.GetQueryable<User>() .Where(u => EF.Functions.Like(u.Email, "%os%") || EF.Functions.Like(u.Email, "%man%")) .ToListAsync();
这个方案和表达式树方案效果完全一致。
内容的提问来源于stack exchange,提问作者OMANSAK

