EF Core 全文搜索:无法将参数化的 keySelector 翻译成 SQL

EF Core Full-text search: parameterized keySelector could not be translated into SQL

我想创建一个允许我使用全文搜索的通用扩展方法。

● 下面的代码有效:

IQueryable<MyEntity> query = Repository.AsQueryable();

if (!string.IsNullOrEmpty(searchCondition.Name))
    query = query.Where(e => EF.Functions.Contains(e.Name, searchCondition.Name));

return query.ToList();

● 但我想要一种更通用的方式,所以我创建了以下扩展方法

public static IQueryable<T> FullTextContains<T>(this IQueryable<T> query, Func<T, string> keySelector, string value)
{
    return query.Where(e => EF.Functions.Contains(keySelector(e), value));
}

当我像下面这样调用扩展方法时,出现异常

IQueryable<MyEntity> query = Repository.AsQueryable();

if (!string.IsNullOrEmpty(searchCondition.Name))
    query = query.FullTextContains(e => e.Name, searchCondition.Name);

return query.ToList();
> System.InvalidOperationException: 'The LINQ expression 'DbSet
>     .Where(c => __Functions_0
>         .Contains(
>           _: Invoke(__keySelector_1, c[MyEntity])
>           , 
> propertyReference: __value_2))' 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
>

我如何"rewrite the query in a form that can be translated" 按照异常建议?

您正在使用委托 (Func<>) 而不是表达式 (Expression<Func<>) 落入典型的 IQueryable<> 陷阱。您可以看到每个 Queryable 扩展方法的 lambda 参数与相应的 Enumerable 方法的差异。不同之处在于委托不能被翻译(它们就像未知的方法),而表达式可以。

所以为了做你想做的事,你必须更改自定义方法的签名以使用表达式:

public static IQueryable<T> FullTextContains<T>(
    this IQueryable<T> query,
    Expression<Func<T, string>> keySelector, // <--
    string value)

但是现在你遇到了实现问题,因为 C# 不支持类似于委托的 "invoking" 表达式的语法,所以下面

keySelector(e)

不编译。

为此,您至少需要一个小型实用程序来编写如下表达式:

public static partial class ExpressionUtils
{
    public static Expression<Func<TOuter, TResult>> Apply<TOuter, TInner, TResult>(this Expression<Func<TOuter, TInner>> outer, Expression<Func<TInner, TResult>> inner)
        => Expression.Lambda<Func<TOuter, TResult>>(inner.Body.ReplaceParameter(inner.Parameters[0], outer.Body), outer.Parameters);

    public static Expression<Func<TOuter, TResult>> ApplyTo<TInner, TResult, TOuter>(this Expression<Func<TInner, TResult>> inner, Expression<Func<TOuter, TInner>> outer)
        => outer.Apply(inner);

    public static Expression ReplaceParameter(this Expression expression, ParameterExpression source, Expression target)
        => new ParameterReplacer { source = source, target = target }.Visit(expression);

    class ParameterReplacer : ExpressionVisitor
    {
        public ParameterExpression source;
        public Expression target;
        protected override Expression VisitParameter(ParameterExpression node)
            => node == source ? target : node;
    }
}

使用 ApplyApplyTo,具体取决于您的表达式类型。除此之外,他们做同样的事情。

在您的情况下,使用 Expression<Func<T, string>> keySelector 的方法的实现将是

return query.Where(keySelector.Apply(key => EF.Functions.Contains(key, value)));