Используйте LinqKit PredicateBuilder для связанной модели (EF Core)

Я хочу использовать PredicateBuilder от LinqKit и передать предикат в .Any метод для связанной модели.

Итак, я хочу построить предикат:

var castCondition = PredicateBuilder.New<CastInfo>(true);

if (movies != null && movies.Length > 0)
{
    castCondition = castCondition.And(c => movies.Contains(c.MovieId));
}
if (roleType > 0)
{
    castCondition = castCondition.And(c => c.RoleId == roleType);
}

А затем используйте его для фильтрации модели, которая имеет отношение к модели в предикате:

IQueryable<Name> result = _context.Name.AsExpandable().Where(n => n.CastInfo.Any(castCondition));
return await result.OrderBy(n => n.Name1).Take(25).ToListAsync();

Но это вызывает System.NotSupportedException: Could not parse expression 'n.CastInfo.Any(Convert(__castCondition_0, Func``2))': The given arguments did not match the expected arguments: Object of type 'System.Linq.Expressions.UnaryExpression' cannot be converted to type 'System.Linq.Expressions.LambdaExpression'.

Я видел аналогичный вопрос и ответ там предлагает использовать .Compile, Или еще один вопрос, который создает дополнительный предикат.

Поэтому я попытался использовать дополнительный предикат

var tp = PredicateBuilder.New<Name>(true);
tp = tp.And(n => n.CastInfo.Any(castCondition.Compile()));
IQueryable<Name> result = _context.Name.AsExpandable().Where(tp);

Или используйте компиляцию напрямую

IQueryable<Name> result = _context.Name.AsExpandable().Where(n => n.CastInfo.Any(castCondition.Compile()));

Но у меня есть ошибка по поводу компиляции: System.NotSupportedException: Could not parse expression 'n.CastInfo.Any(__Compile_0)'

Так можно ли преобразовать результат из PredicateBuilder для передачи в Any?

Примечание: я смог построить желаемое поведение, комбинируя выражения, но мне не нравится, что мне нужны дополнительные переменные.

System.Linq.Expressions.Expression<Func<CastInfo,bool>> castExpression = (c => true);
if (movies != null && movies.Length > 0)
{
    castExpression = (c => movies.Contains(c.MovieId));
}
if (roleType > 0)
{
    var existingExpression = castExpression;
    castExpression = c => existingExpression.Invoke(c) && c.RoleId == roleType;
}
IQueryable<Name> result = _context.Name.AsExpandable().Where(n => n.CastInfo.Any(castExpression.Compile()));
return await result.OrderBy(n => n.Name1).Take(25).ToListAsync();

Так что я предполагаю, что просто что-то упустил из-за строителя

Обновление о версиях: я использую dotnet core 2.0 и LinqKit.Microsoft.EntityFrameworkCore 1.1.10

2 ответа

Решение

Глядя на код, можно предположить, что тип castCondition переменная Expression<Func<CastInfo, bool>> (как это было в более ранних версиях PredicateBuilder).

Но если бы это было так, то n.CastInfo.Any(castCondition) не должен даже компилироваться (при условии CastInfo это свойство навигации по коллекции, поэтому компилятор Enumerable.Any что ожидает Func<CastInfo, bool> не Expression<Func<CastInfo, bool>>). Так что здесь происходит?

На мой взгляд, это хороший пример неявного злоупотребления оператором в C#. PredicateBuilder.New<T> Метод на самом деле возвращает класс с именем ExpressionStarter<T>, который имеет много методов эмуляции Expression, но что более важно, имеет неявное преобразование в Expression<Func<T, bool>> а также Func<CastInfo, bool>, Последнее позволяет использовать этот класс для верхнего уровня Enumerable / Queryable методы как замена соответствующего лямбда-функции / выражения. Тем не менее, он также предотвращает ошибку времени компиляции при использовании внутри дерева выражений, как в вашем случае - компилятор выдает что-то вроде n.CastInfo.Any((Func<CastInfo, bool>)castCondition) что, конечно, вызывает исключение во время выполнения.

Вся идея LinqKit AsExpandable метод должен позволить "вызывать" выражения через пользовательский Invoke метод расширения, который затем "раскрывается" в дереве выражений. Итак, вернемся к началу, если тип переменной был Expression<Func<CastInfo, bool>>, предполагаемое использование:

_context.Name.AsExpandable().Where(n => n.CastInfo.Any(c => castCondition.Invoke(c)));

Но теперь это не компилируется из-за причины, объясненной ранее. Таким образом, вы должны сначала преобразовать его в Expression<Func<T, bool> вне запроса:

Expression<Func<CastInfo, bool>> castPredicate = castCondition;

а затем использовать

_context.Name.AsExpandable().Where(n => n.CastInfo.Any(c => castPredicate.Invoke(c)));

или же

_context.Name.AsExpandable().Where(n => n.CastInfo.Any(castPredicate.Compile()));

Чтобы позволить компилятору выводить тип выражения, я бы создал собственный метод расширения, например:

using System;
using System.Linq.Expressions;

namespace LinqKit
{
    public static class Extensions
    {
        public static Expression<Func<T, bool>> ToExpression<T>(this ExpressionStarter<T> expr) => expr;
    }
}

а потом просто использовать

var castPredicate = castCondition.ToExpression();

Это все еще должно быть сделано вне запроса, то есть следующее не работает:

_context.Name.AsExpandable().Where(n => n.CastInfo.Any(c => castCondition.ToExpression().Invoke(c)));

Это может быть не совсем связано с исходным вопросом, но с учетом следующей модели:

      public Class Music
{
    public int Id { get; set; }
    public List<Genre> Genres { get; set; }
}
public Class Genre
{
    public int Id { get; set; }
    public string Title { get; set; }
}

List<string> genresToFind = new() {"Pop", "Rap", "Classical"};

Если вы пытаетесь найти все Musicsчто их жанры существуют в genresToFindсписок, вот что вы можете сделать:

Создавать PredicateBuilderцепочка выражений на Genreмодель :

      var pre = PredicateBuilder.New<Genre>();
foreach (var genre in genresToFind)
{
    pre = pre.Or(g => g.Title.Contains(genre));
}

Затем выполните свой запрос следующим образом:

      var result = await _db.Musics.AsExpandable()
    .Where(m => m.Genres
        .Any(g => pre.ToExpression().Invoke(g)))
    .ToListAsync();

ToExpression()— это общий метод расширения, который мы создали для преобразования ExpressionStarter<Genre>введите в Expression<Func<Genre, bool>>:

      public static class ExpressionExtensions
{
    public static Expression<Func<T, bool>> ToExpression<T> (this 
        ExpressionStarter<T> exp) => exp;
}

Также вам понадобится LinqKit.Microsoft.EntityFrameworkCoreпакет для efcore.

Другие вопросы по тегам