如何使用 Entity Framework 查找所有搜索词都包含在选定列中的行

How to find a row where all search terms are contained within selected columns using Entity Framework

就上下文而言,这个问题与 and . In this case, the user can specify an array of phrases. I'd like to expand upon the 有关,询问我如何创建一种通用方法来查找 所有 单词的实体any 个短语包含在 any 个指定列中。

为了让您更好地理解我在说什么,如果我要将其编写为非泛型方法,它看起来会像这样:

var searchPhrases = new [] {"John Smith", "Smith Bob"};
var searchTermSets = searchPhrases.Select(x => x.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));

contacts.Where(c =>
    searchTermSets.Any(searchTerms =>
        searchTerms.All(searchTerm =>
            c.FullName.Contains(searchTerm)
            || c.FirstName.Contains(searchTerm)
            || c.LastName.Contains(searchTerm))));

我想做的是制作一个扩展方法,我可以在其中做这样的事情:

contact.WhereIn(
    searchPhrases,
    c => c.FullName,
    c => c.FirstName,
    c => c.LastName);

扩展方法签名看起来像这样:

IQueryable<T> WhereIn<T>(this IQueryable<T> source, IEnumerable<string> searchPhrases, params Expression<Func<T, string>>[] propertySelectors)

我尝试按照我链接到的先前问题中的相同模式进行操作,但我并没有走得太远。对 All() 的调用让我很困惑。

像谓词 for

的表达式
contacts.Where(c =>
    searchTermSets.Any(searchTerms =>
        searchTerms.All(searchTerm =>
            c.FullName.Contains(searchTerm)
            || c.FirstName.Contains(searchTerm)
            || c.LastName.Contains(searchTerm)))); 

可以使用 Expression.CallEnumerable.AnyEnumerable.All 动态构建。

首先我们需要一个简单的参数替换器,这样我们就可以将所有传递的 Expression<Func<T, string>> 绑定到一个参数:

public static class ExpressionUtils
{
    public static Expression ReplaceParameter(this Expression expression, ParameterExpression source, Expression target)
    {
        return new ParameterReplacer { Source = source, Target = target }.Visit(expression);
    }

    class ParameterReplacer : ExpressionVisitor
    {
        public ParameterExpression Source;
        public Expression Target;
        protected override Expression VisitParameter(ParameterExpression node)
        {
            return node == Source ? Target : base.VisitParameter(node);
        }
    }
}

那么实现可以是这样的:

public static class QueryableExtensions
{
    public static IQueryable<T> WhereIn<T>(this IQueryable<T> source, IEnumerable<string> searchPhrases, params Expression<Func<T, string>>[] propertySelectors)
    {
        var searchTermSets = searchPhrases.Select(x => x.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));
        var c = Expression.Parameter(typeof(T), "c");
        var searchTerms = Expression.Parameter(typeof(string[]), "searchTerms");
        var searchTerm = Expression.Parameter(typeof(string), "searchTerm");
        var allCondition = propertySelectors
            .Select(propertySelector => (Expression)Expression.Call(
                propertySelector.Body.ReplaceParameter(propertySelector.Parameters[0], c),
                "Contains", Type.EmptyTypes, searchTerm))
            .Aggregate(Expression.OrElse);
        var allPredicate = Expression.Lambda<Func<string, bool>>(allCondition, searchTerm);
        var allCall = Expression.Call(
            typeof(Enumerable), "All", new[] { typeof(string) }, 
            searchTerms, allPredicate);
        var anyPredicate = Expression.Lambda<Func<string[], bool>>(allCall, searchTerms);
        var anyCall = Expression.Call(
            typeof(Enumerable), "Any", new[] { typeof(string[]) },
            Expression.Constant(searchTermSets), anyPredicate);
        var predicate = Expression.Lambda<Func<T, bool>>(anyCall, c);
        return source.Where(predicate);
    }
}

问题是它不起作用。如果你尝试 运行 你的非通用查询,你会得到 EntityCommandCompilationException 内部 NotSupportedException

The nested query is not supported. Operation1='Case' Operation2='Collect'

动态构建的查询也会发生同样的情况。

那我们该怎么办呢?好吧,考虑到 searchPhrases(因此 searchTermSetssearchTerms)是已知的,我们可以将它们视为常量,我们需要获得所需结果的所有内容是替换 AnyOr 表达式和 AllAnd 表达式。

工作解决方案如下所示(使用相同的参数替换器):

public static class QueryableExtensions
{
    public static IQueryable<T> WhereIn<T>(this IQueryable<T> source, IEnumerable<string> searchPhrases, params Expression<Func<T, string>>[] propertySelectors)
    {
        var searchTermSets = searchPhrases.Select(x => x.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));
        var c = Expression.Parameter(typeof(T), "c");
        var body = searchTermSets
            .Select(searchTerms => searchTerms
                .Select(searchTerm => propertySelectors
                    .Select(propertySelector => (Expression)Expression.Call(
                        propertySelector.Body.ReplaceParameter(propertySelector.Parameters[0], c),
                        "Contains", Type.EmptyTypes, Expression.Constant(searchTerm)))
                    .Aggregate(Expression.OrElse))
                .Aggregate(Expression.AndAlso))
            .Aggregate(Expression.OrElse);
        var predicate = Expression.Lambda<Func<T, bool>>(body, c);
        return source.Where(predicate);
    }
}