使用 String.Contains 的 EF Core 查询

EF Core query using String.Contains

我们需要在逗号分隔的字符串中搜索给定的术语。构建查询以便忽略逗号分隔字符串中可能的前导和尾随空格。 我提出了以下查询,它 运行 适合 EF 6.0

var trimmedTags = tags.Select(t => t.Trim()); // List of tags we need to look for    
return products.Where(p => trimmedTags.Any(t => ("," + p.Categories + ",").Contains("," + t + ",")) ||
                               trimmedTags.Any(t => ("," + p.Categories + ",").Contains(", " + t + ",")) ||
                               trimmedTags.Any(t => ("," + p.Categories + ",").Contains("," + t + " ,")) ||
                               trimmedTags.Any(t => ("," + p.Categories + ",").Contains(", " + t + " ,")));

此查询不再是 EF Core 3.1 中的 运行 并抛出以下错误:

System.InvalidOperationException: 'The LINQ expression 'DbSet<Product>
    .Where(p => __trimmedTags_1
        .Any(t => ("," + p.Categories + ",").Contains("," + t + ",")) || __trimmedTags_1
        .Any(t => ("," + p.Categories + ",").Contains(", " + t + ",")) || __trimmedTags_1
        .Any(t => ("," + p.Categories + ",").Contains("," + t + " ,")) || __trimmedTags_1
        .Any(t => ("," + p.Categories + ",").Contains(", " + t + " ,")))' 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.'

我的目标 table 有数百万行,因此很遗憾客户评估不是一个选项。 EF Core 团队声称 string.Contains 受支持,但我不明白为什么我的查询在 EF Core 中突然失败。

这个问题的不同变体经常出现在 SO 上,问题总是同一个 - 即使是最新版本 (5.x) EF Core 不支持内存集合上的运算符除了具有原始值的简单 Contains(或 Any 可以像 x => memValues.Any(v => v == SomeExpr(x)) 一样变成 Contains== 运算符是必不可少的)。

解决方法也是相同的 - 构建动态表达式 - ||(或)基于 Any&&(和)基于 All

这种情况需要 ||,与 类似,但交换了值和字段角色,因此以下是我将使用的辅助方法:

public static partial class QueryableExtensions
{
    public static IQueryable<T> WhereAnyMatch<T, V>(this IQueryable<T> source, IEnumerable<V> values, Expression<Func<T, V, bool>> match)
    {
        var parameter = match.Parameters[0];
        var body = values
            // the easiest way to let EF Core use parameter in the SQL query rather than literal value
            .Select(value => ((Expression<Func<V>>)(() => value)).Body)
            .Select(value => Expression.Invoke(match, parameter, value))
            .Aggregate<Expression>(Expression.OrElse);
        var predicate = Expression.Lambda<Func<T, bool>>(body, parameter);
        return source.Where(predicate);
    }
}

请注意,这仅适用于顶级查询表达式。如果你需要这样的东西作为查询表达式树的一部分(比如集合导航属性),你需要不同类型的辅助函数或一些允许表达式注入的库。

幸运的是,这里不是这种情况,因此可以通过传递 trimmedTags 和每个标签值的条件直接使用上述辅助方法,例如

return products.WhereAnyMatch(trimmedTags, (p, t) => 
    ("," + p.Categories + ",").Contains("," + t + ",") ||
    ("," + p.Categories + ",").Contains(", " + t + ",") ||
    ("," + p.Categories + ",").Contains("," + t + " ,") ||
    ("," + p.Categories + ",").Contains(", " + t + " ,"));