Запрос EF Core с использованием 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.'

В моей целевой таблице миллионы строк, поэтому оценка клиента, к сожалению, невозможна. Команда EF Core утверждает, что string.Contains поддерживается, но я не могу понять, почему мой запрос внезапно дает сбой в EF Core.


person GETah    schedule 13.03.2021    source источник
comment
Я думаю, что на данный момент вам лучше всего повторить trimmedTags и построить дерево выражений, которое вы передаете методу .Where. Вы можете генерировать продукты запроса. .Содержит(,тег3,)); и это должно работать нормально.   -  person dropoutcoder    schedule 14.03.2021
comment
Прошло некоторое время с тех пор, как я использовал EF, но решает ли проблема ToList для trimmedTags (вне запроса)? Это или вы можете попробовать использовать Ef.Functions.Like и как-то упростить сравнение   -  person pinkfloydx33    schedule 14.03.2021
comment
@dropoutcoder Спасибо за быстрое продолжение. Именно так я и решил проблему. Не могли бы вы преобразовать свой комментарий в ответ, чтобы я мог его принять?   -  person GETah    schedule 14.03.2021
comment
@pinkfloydx33 Использование ToList было первым, что я попробовал, но безуспешно :(   -  person GETah    schedule 14.03.2021
comment
@GETah Я не понимаю, как EF.Functions.Like может помочь. Проблема не в функции Contains, а в том, что она является частью вызова trimmedTags.Any(...), где trimmdeTags является коллекцией в памяти. И единственная поддерживаемая операция с коллекциями в памяти — это Coontains с примитивным значением. Итак, вы уверены, что решили свою проблему?   -  person Ivan Stoev    schedule 14.03.2021
comment
@IvanStoev OP отвечал на первый комментарий о построении выражения, а не на мой (это было просто плевок о поиске способа для использования Like, а не то, что это обязательно было решением)   -  person pinkfloydx33    schedule 14.03.2021
comment
@pinkfloydx33 Мои извинения, по какой-то неизвестной причине я предположил, что OP отвечает на ваш комментарий.   -  person Ivan Stoev    schedule 14.03.2021
comment
@IvanStoev, он сделал, строкой ниже :)   -  person pinkfloydx33    schedule 14.03.2021
comment
@GETah Кажется, кто-то уже написал ответ. Я не гонюсь за очками. Я просто рад помочь :)   -  person dropoutcoder    schedule 14.03.2021
comment
@dropoutcoder это дух :) большое спасибо за вашу помощь   -  person GETah    schedule 14.03.2021


Ответы (1)


На SO часто появляются разные варианты этого вопроса, и проблема всегда одна и та же — даже в самой последней версии (5.x) EF Core не поддерживает операторы над коллекциями в памяти, кроме простых Contains с примитивным значением (или Any, который можно превратить в Contains, как x => memValues.Any(v => v == SomeExpr(x)), с оператором ==).

Обходной путь тоже один и тот же - построение динамически выражения - ||(или) на основе Any и &&(и) на основе All.

В этом случае требуется ||, и он похож на Как упростить повторяющееся условие ИЛИ в Where(e =› e.prop1.contains() || e.prop2.contains() || ...), но со значением и полем обмен ролями, поэтому я бы использовал следующий вспомогательный метод:

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 + " ,"));
person Ivan Stoev    schedule 14.03.2021
comment
Потрясающий! Красиво работает. Большое спасибо. - person GETah; 14.03.2021