Общее выражение для фильтрации EF Core 3.1 создает исключение для .Any()

Я пытаюсь создать общий способ получения данных через EF Core 3.1 с одинаковой фильтрацией разных дочерних элементов. Для этого я пытаюсь извлечь искомое выражение внутри Any(...).

public Expression<Func<PraeparatUpdateEntity, bool>> IsToApprovePackungUpdates_Working()
{
   return entity => entity.PackungUpdates.Any(e => !e.IsImported
      && e.UpdateState != EntityUpdateState.Accepted
      && e.UpdateType != EntityUpdateType.Unchanged);
}

public Expression<Func<PraeparatUpdateEntity, bool>> IsToApprovePackungUpdates_NotWorking()
{
   var func = new Func<PackungUpdateEntity, bool>(e => !e.IsImported
      && e.UpdateState != EntityUpdateState.Accepted
      && e.UpdateType != EntityUpdateType.Unchanged);

   return entity => entity.PackungUpdates.Any(func);
}

public new async Task<ICollection<PraeparatUpdateEntity>> GetToApproveAsync(bool trackChanges = false)
{ 
   var query = Set.Include(praeparatUpdateEntity => praeparatUpdateEntity.PackungUpdates)
      .Where(IsToApprovePackungUpdates_NotWorking());
            
   if (!trackChanges)
   {
      query = query.AsNoTracking();
   }

   return await query.ToListAsync();
}

Первая версия работает. Второй терпит неудачу с сообщением об ошибке:

System.ArgumentException : Expression of type 'System.Func`2[MyProject.Data.Common.Entities.Update.PackungUpdateEntity,System.Boolean]' cannot be used for parameter of type 'System.Linq.Expressions.Expression`1[System.Func`2[MyProject.Data.Common.Entities.Update.PackungUpdateEntity,System.Boolean]]' of method 'Boolean Any[PackungUpdateEntity](System.Linq.IQueryable`1[MyProject.Data.Common.Entities.Update.PackungUpdateEntity], System.Linq.Expressions.Expression`1[System.Func`2[MyProject.Data.Common.Entities.Update.PackungUpdateEntity,System.Boolean]])' (Parameter 'arg1')
   at System.Dynamic.Utils.ExpressionUtils.ValidateOneArgument(MethodBase method, ExpressionType nodeKind, Expression arguments, ParameterInfo pi, String methodParamName, String argumentParamName, Int32 index)
   at System.Linq.Expressions.Expression.Call(Expression instance, MethodInfo method, Expression arg0, Expression arg1)
   at System.Linq.Expressions.Expression.Call(Expression instance, MethodInfo method, IEnumerable`1 arguments)
   at Microsoft.EntityFrameworkCore.Query.Internal.EnumerableToQueryableMethodConvertingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at System.Linq.Expressions.ExpressionVisitor.VisitLambda[T](Expression`1 node)
   at System.Linq.Expressions.Expression`1.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at System.Linq.Expressions.ExpressionVisitor.VisitUnary(UnaryExpression node)
   at System.Linq.Expressions.UnaryExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at System.Dynamic.Utils.ExpressionVisitorUtils.VisitArguments(ExpressionVisitor visitor, IArgumentProvider nodes)
   at System.Linq.Expressions.ExpressionVisitor.VisitMethodCall(MethodCallExpression node)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.QueryTranslationPreprocessor.Process(Expression query)
   at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass12_0`1.<ExecuteAsync>b__0()
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1.GetAsyncEnumerator(CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.ConfiguredCancelableAsyncEnumerable`1.GetAsyncEnumerator()
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
   at MyProject.Data.Repositories.PraeparatUpdateRepository.GetToApproveAsync(Boolean trackChanges) in C:\git\MyProject\Source\MyProject.Data\Repositories\PraeparatUpdateRepository.cs:line 156
   at MyProject.Data.Tests.Integration.RepositoryNavigationPropertyLoadingTests.GetAllPraeparatUpdates_WhereToApprove_WithNavigationProperties_OK_Test() in C:\git\MyProject\Source\MyProject.Data.Tests.Integration\RepositoryNavigationPropertyLoadingTests.cs:line 328
--- End of stack trace from previous location where exception was thrown ---

******** ОБНОВИТЬ ********

Если я добавлю AsQueryable() к своим дочерним элементам базы данных IEnumerable, я могу добавить свои выражения следующим образом:

var query = Set.Include(praeparatUpdateEntity => praeparatUpdateEntity.PackungUpdates)
   .Include(praeparatUpdateEntity => praeparatUpdateEntity.SequenzUpdates)
   .ThenInclude(sequenzUpdateEntity => sequenzUpdateEntity.ApplikationsartUpdates)
   .Include(praeparatUpdateEntity => praeparatUpdateEntity.SequenzUpdates)
   .ThenInclude(sequenzUpdateEntity => sequenzUpdateEntity.DeklarationUpdates)
   .Where(IsToApprove<PraeparatUpdateEntity>()
      .OrElse(entity => entity.PackungUpdates.AsQueryable().Any(IsToApprove<PackungUpdateEntity>()))
      .OrElse(entity => entity.SequenzUpdates.AsQueryable().Any(IsToApprove<SequenzUpdateEntity>()))
      .OrElse(entity => entity.SequenzUpdates.SelectMany(sequenzUpdateEntity => sequenzUpdateEntity.ApplikationsartUpdates).AsQueryable()
         .Any(IsToApprove<ApplikationsartUpdateEntity>()))
      .OrElse(entity => entity.SequenzUpdates.SelectMany(sequenzUpdateEntity => sequenzUpdateEntity.DeklarationUpdates).AsQueryable()
         .Any(IsToApprove<DeklarationUpdateEntity>())));

и мое общее выражение:

public Expression<Func<T, bool>> IsToApprove<T>() where T : class, IUpdateEntity
{
   return entity => !entity.IsImported && entity.UpdateState != EntityUpdateState.Accepted
   && entity.UpdateType != EntityUpdateType.Unchanged;
}

который, кажется, на данный момент работает... Идут испытания


person Angelika    schedule 31.08.2020    source источник
comment
If I add AsQueryable() to my IEnumerable database children они уже IQueryable, а не IEnumerable. DbSet<T> реализует IQueryable<T>, поэтому с ним можно использовать LINQ. Проблема в том, что ваше универсальное выражение на самом деле представляет собой вызов конкретной функции с использованием Any, который нельзя преобразовать в SQL. На самом деле работает это выражение для конкретной сущности. Что ты пытаешься сделать? Что бы это ни было, оно не требует таких сложных выражений   -  person Panagiotis Kanavos    schedule 31.08.2020
comment
Вы пытаетесь фильтровать дочерние сущности? Вы не можете сделать это в EF Core 3.1, это появится в EF Core 5. Фильтрация дочерних сущностей не аналогична применению всех фильтров в запросе, после СОЕДИНЕНИЯ произвели много пустых или повторяющихся строк. Вам нужно будет отфильтровать сами предложения JOIN, что невозможно в EF Core 3.1. В SQL вам понадобится parent LEFT JOIN (select ... from child where child.IsApproved =10 c on c.ParentID=parent.ID   -  person Panagiotis Kanavos    schedule 31.08.2020
comment
Учитывая, что все эти условия жестко закодированы, вы можете создавать представления или функции SQL Server, которые возвращают только соответствующие объекты и сопоставляют с ними ваши сущности.   -  person Panagiotis Kanavos    schedule 31.08.2020
comment
Я пытаюсь получить список PraeparateUpdateEntities (со всеми дочерними элементами), где должен быть одобрен сам объект или любой его дочерний элемент. Поэтому мне не нужно фильтровать дочерние объекты.   -  person Angelika    schedule 01.09.2020


Ответы (1)


Entity Framework построен на основе IQueryable, который использует деревья выражений. Они необходимы для анализа вашего кода во время выполнения и преобразования его в SQL. В первом фрагменте компилятор берет на себя всю тяжелую работу по построению деревьев выражений. Вы можете попробовать построить дерево выражений самостоятельно, но обычно это не такая простая задача. В этом случае вы можете попробовать что-то вроде этого:

Expression<Func<PackungUpdateEntity, bool>> exp = e => !e.IsImported
      && e.UpdateState != EntityUpdateState.Accepted
      && e.UpdateType != EntityUpdateType.Unchanged;

var any = typeof(Enumerable)
    .GetMethods()
    .Where(mi => mi.Name == nameof(Enumerable.Any) && mi.GetParameters().Length == 2)
    .Single()
    .MakeGenericMethod(typeof(PackungUpdateEntity));
var param = Expression.Parameter(typeof(PraeparatUpdateEntity));
var toAny = Expression.PropertyOrField(param, nameof(PraeparatUpdateEntity.PackungUpdates));
var call = Expression.Call(any, toAny, exp); 
var result = Expression.Lambda<Func<PraeparatUpdateEntity, bool>>(call, param);

return result;
person Guru Stron    schedule 31.08.2020
comment
Это приводит к ошибке компилятора: CS1503 Аргумент 2: невозможно преобразовать из «System.Linq.Expressions.Expression‹System.Func‹MyProject.Data.Common.Entities.Update.PackungUpdateEntity, bool››» в «System.Func‹MyProject .Data.Common.Entities.Update.PackungUpdateEntity, bool›' - person Angelika; 31.08.2020
comment
Вам также необходимо сохранить возвращаемую переменную в выражение. - person Maarten; 31.08.2020
comment
@Анжелика entity.PackungUpdates должна быть IQueryable<PackungUpdateEntity> - person Guru Stron; 31.08.2020
comment
похоже, это сработает, если я добавлю AsQueryable() вот так public Expression<Func<PraeparatUpdateEntity, bool>> IsToApprovePackungUpdates_MayBeWorking() { Expression<Func<PackungUpdateEntity, bool>> func = e => !e.IsImported && e.UpdateState == EntityUpdateState.Accepted && e.UpdateType != EntityUpdateType.Unchanged; return entity => entity.PackungUpdates.AsQueryable().Any(func); } Но я не уверен, насколько это ЗЛО... Я не хочу менять PackungsUpdates с ICollection на IQueryable, поскольку я не хочу выставлять IQueryable в более высокие слои... - person Angelika; 31.08.2020
comment
@Мартен, я не понял, что ты имел в виду под своим комментарием. Если я сохраню entity => entity.PackungUpdates.Any(func) в переменной Expression, это не изменит мою ошибку компилятора для части Any(func)-... - person Angelika; 31.08.2020
comment
@Анжелика, мое первое предложение было неверным, пожалуйста, смотрите пост. Также почему вы не можете использовать первый метод? - person Guru Stron; 31.08.2020
comment
@GuruStron Мне нужно сделать эту проверку не только для PackungUpdates. Мне нужно проверить 5 раз. С моей 1. версией я не могу повторно использовать функциональность. Вскоре я опубликую обновление, чтобы вы могли видеть мое текущее состояние. - person Angelika; 31.08.2020
comment
@Angelika, если вы действительно хотите отфильтровать дочерние объекты, вы не сможете сделать это в EF Core 3.1. .Where() применяется к выходным данным предложений LEFT JOIN, сгенерированных для извлечения связанных объектов. Это означает, что уже есть много повторяющихся или нулевых строк. EF Core попытается воссоздать дочерние объекты на основе их идентификаторов, но не будет повторно применять выражения фильтра после их загрузки. Чтобы действительно отфильтровать дочерние объекты, вам нужно отфильтровать в предложении LEFT JOIN, то есть LEFT JOIN (select .... where ....) child1 - person Panagiotis Kanavos; 31.08.2020
comment
@Angelika, если вы хотите фильтровать связанные объекты, вам придется использовать EF Core 5 (все еще в предварительной версии, вероятно, в следующем месяце появится RC) - person Panagiotis Kanavos; 31.08.2020