Создание вложенных выражений с помощью linq и Entity Framework

Я пытаюсь сделать сервис, который возвращает каталог на основе фильтров.

Я видел несколько результатов в Интернете, но не совсем моя проблема. Я надеюсь, что вы можете помочь мне с моей.

Проблема в том, что эту сборку запроса нельзя преобразовать в выражение хранилища:

'LINQ to Entities does not recognize the method 'System.Linq.IQueryable'1[App.Data.Models.Subgroup] HasProductsWithState[Subgroup](System.Linq.IQueryable'1[App.Data.Models.Subgroup], System.Nullable`1[System.Boolean])' method, and this method cannot be translated into a store expression.'

Как я могу сделать так, чтобы запрос можно было перевести в выражение хранилища. Пожалуйста, не предлагайте .ToList() в качестве ответа, так как я не хочу, чтобы это работало в памяти.

Итак, что у меня есть:

    bool? isActive = null;
    string search = null;

    DbSet<Maingroup> query = context.Set<Maingroup>();

    var result = query.AsQueryable()
                      .HasProductsWithState(isActive)
                      .HasChildrenWithName(search)
                      .OrderBy(x => x.SortOrder)
                      .Select(x => new CatalogViewModel.MaingroupViewModel()
                              {
                                  Maingroup = x,
                                  Subgroups = x.Subgroups.AsQueryable()
                                               .HasProductsWithState(isActive)
                                               .HasChildrenWithName(search)
                                               .OrderBy(y => y.SortOrder)
                                               .Select(y => new CatalogViewModel.SubgroupViewModel()
                        {
                            Subgroup = y,
                            Products = y.Products.AsQueryable()
                                .HasProductsWithState(isActive)
                                .HasChildrenWithName(search)
                                .OrderBy(z => z.SortOrder)
                                .Select(z => new CatalogViewModel.ProductViewModel()
                                {
                                    Product = z
                                })
                        })
                });         

    return new CatalogViewModel() { Maingroups = await result.ToListAsync() };

В приведенном ниже коде вы можете видеть, что я рекурсивно вызываю расширение, чтобы попытаться сложить выражение. Но когда я просматриваю свой код во время выполнения, он больше не входит в функцию, когда

    return maingroups.Where(x => x.Subgroups.AsQueryable().HasProductsWithState(state).Any()) as IQueryable<TEntity>;

называется.

    public static class ProductServiceExtensions
    {
        public static IQueryable<TEntity> HasProductsWithState<TEntity>(this IQueryable<TEntity> source, bool? state)
        {
            if (source is IQueryable<Maingroup> maingroups)
            {
                return maingroups.Where(x => x.Subgroups.AsQueryable().HasProductsWithState(state).Any()) as IQueryable<TEntity>;
            }
            else if (source is IQueryable<Subgroup> subgroups)
            {
                return subgroups.Where(x => x.Products.AsQueryable().HasProductsWithState(state).Any()) as IQueryable<TEntity>;
            }
            else if (source is IQueryable<Product> products)
            {
                return products.Where(x => x.IsActive == state) as IQueryable<TEntity>;
            }

            return source;
        }

        public static IQueryable<TEntity> HasChildrenWithName<TEntity>(this IQueryable<TEntity> source, string search)
        {
            if (source is IQueryable<Maingroup> maingroups)
            {
                return maingroups.Where(x => search == null || x.Name.ToLower().Contains(search) || x.Subgroups.AsQueryable().HasChildrenWithName(search).Any()) as IQueryable<TEntity>;
            }
            else if (source is IQueryable<Subgroup> subgroups)
            {
                return subgroups.Where(x => search == null || x.Name.ToLower().Contains(search) || x.Products.AsQueryable().HasChildrenWithName(search).Any()) as IQueryable<TEntity>;
            }
            else if (source is IQueryable<Product> products)
            {
                return products.Where(x => search == null || x.Name.ToLower().Contains(search)) as IQueryable<TEntity>;
            }

            return source;
        }
    }

ОБНОВИТЬ

Отсутствующие классы:

    public class Maingroup
    {
        public long Id { get; set; }
        public string Name { get; set; }
        ...
        public virtual ICollection<Subgroup> Subgroups { get; set; }
    }
    public class Subgroup
    {
        public long Id { get; set; }
        public string Name { get; set; }

        public long MaingroupId { get; set; }
        public virtual Maingroup Maingroup { get; set; }
        ...
        public virtual ICollection<Product> Products { get; set; }
    }
    public class Product
    {
        public long Id { get; set; }
        public string Name { get; set; }

        public long SubgroupId { get; set; }
        public virtual Subgroup Subgroup { get; set; }
        ...
        public bool IsActive { get; set; }
    }

Возможный дубликат Метод не может быть переведен в выражение хранилища

Aleks Andreev 10.04.2019 22:20

@AleksAndreev Это определенно та же ошибка, но не та же проблема. Он присоединяется к новым отношениям, и я складываю выражения

Jim 10.04.2019 22:28

Если вы закомментируете HasProductsWithState, это приведет к той же ошибке в HasChildrenWithName или это нормально?

ChrisF 10.04.2019 22:43

Это приведет к той же ошибке, потому что рекурсия, подобная этой, возвращает maingroups.Where(x => x.Subgroups.AsQueryable().HasProductsWithState(state).Any()) as IQueryable<TEntity>; вероятно, это неправильный способ сделать это.

Jim 10.04.2019 22:47

Похоже, у вас много AsQueryable того, что должно быть ненужным - вы должны знать тип на тот момент и есть ли уже IQueryable...

NetMage 10.04.2019 23:19

У меня они .AsQueryable, так как Maingroup.Subgroups является ICollection, а не IQuerable

Jim 11.04.2019 00:39

@ Джим, может ли твое решение использовать SQL?

Andriy Shevchenko 13.04.2019 14:18

@ Джим, не могли бы вы опубликовать свой код для основной группы и других классов?

Andriy Shevchenko 13.04.2019 14:27
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
8
1 843
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Причина вашей проблемы

Вы должны знать между IEnumerable и IQueryable. В объекте IEnumerable есть все для перечисления всех элементов: вы можете запросить первый элемент последовательности, и как только вы получите элемент, вы можете запросить следующий элемент, пока не останется элементов.

IQueryable кажется похожим, однако IQueryable не содержит всего для перечисления последовательности. Он содержит Expression и Provider. Expression — это общая форма того, что должно быть запрошено. Provider знает, кто должен выполнить запрос (обычно это система управления базой данных), как общаться с этим исполнителем и какой язык использовать (обычно что-то вроде SQL).

Как только вы начинаете перечислять, либо явно, вызывая GetEnumerator и MoveNext, либо неявно, вызывая foreach, ToList, FirstOrDefault, Count и т. д., Expression отправляется в Provider, который преобразует его в SQL и вызывает СУБД. Возвращаемые данные представлены в виде объекта IEnumerable, который перечисляется с помощью GetEnumerator

Поскольку провайдер должен перевести Expression в SQL, Expression может вызывать только те функции, которые могут быть переведены в SQL. Увы, Provider не знает ни HasProductsWithState, ни ваших собственных определенных функций, и поэтому не может перевести их в SQL. На самом деле поставщик Entity Framework также не знает, как перевести несколько стандартных функций LINQ, и поэтому их нельзя использовать AsQueryable. См. Поддерживаемые и неподдерживаемые методы LINQ.

Поэтому вам придется придерживаться функций, которые возвращают IQueryable, где выражение содержит только поддерживаемые функции.

Описание класса

Увы, вы забыли дать нам свои классы сущностей, поэтому мне придется сделать некоторые предположения о них.

По-видимому, у вас есть DbContext как минимум с тремя наборами DbSet: MainGroups, SubGroups и Products.

Похоже, что между MaingGroups и SubGroups существует отношение один ко многим (или, возможно, многие ко многим): каждый MainGroup имеет ноль или более SubGroups.

Похоже, что между SubGroups и Products также существует отношение «один ко многим»: каждый SubGroup имеет ноль или более Products.

Увы, вы забыли упомянуть это отношение возврата: каждый Product принадлежит ровно одному SubGroup (один ко многим) или каждый Product принадлежит нулю или более SubGroups (многие ко многим`)?

Если вы следовали первым соглашениям кода фреймворка сущностей, у вас будут классы, подобные этому:

class MainGroup
{
    public int Id {get; set;}
    ...

    // every MainGroup has zero or more SubGroups (one-to-many or many-to-many)
    public virtual ICollection<SubGroup> SubGroups {get; set;}
}
class SubGroup
{
    public int Id {get; set;}
    ...

    // every SubGroup has zero or more Product(one-to-many or many-to-many)
    public virtual ICollection<Product> Products{get; set;}

    // alas I don't know the return relation
    // one-to-many: every SubGroup belongs to exactly one MainGroup using foreign key
    public int MainGroupId {get; set;}
    public virtual MainGroup MainGroup {get; set;}
    // or every SubGroup has zero or more MainGroups:
    public virtual ICollection<MainGroup> MainGroups {get; set;}
}

Нечто подобное для продукта:

class Product
{
    public int Id {get; set;}
    public bool? IsActive {get; set;} // might be a non-nullable property
    ...

    // alas I don't know the return relation
    // one-to-many: every Productbelongs to exactly one SubGroup using foreign key
    public int SubGroupId {get; set;}
    public virtual SubGroup SubGroup {get; set;}
    // or every Product has zero or more SubGroups:
    public virtual ICollection<SubGroup> SubGroups {get; set;}
}

И, конечно же, ваш DbContext:

class MyDbContext : DbContext
{
    public DbSet<MainGroup> MainGroups {get; set;}
    public DbSet<SubGroup> SubGroups {get; set;}
    public DbSet<Product> Products {get; set;}
}

Это все, что необходимо знать инфраструктуре сущностей для обнаружения ваших таблиц, столбцов в ваших таблицах и отношений между таблицами (один ко многим, многие ко многим, один к нулю или один). Только если вы хотите отклониться от стандартного именования, вам понадобятся атрибуты Fluent API.

In entity framework the columns of the tables are represented by non-virtual properties. The virtual properties represent the relations between the tables (one-to-many, many-to-many).

Обратите внимание, что хотя SubGroups из MainGroup объявлен как коллекция, если вы запросите SubGroups из MaingGroup with Id 10, вы все равно получите IQueryable.

Требования

Учитывая запрашиваемую последовательность Products и логическое значение, допускающее значение null State, HasProductsWithState(products, state) должна возвращать запрашиваемую последовательность Products, которая имеет значение IsActive, равное State

Учитывая запрашиваемую последовательность SubGroups и логическое значение State, допускающее значение NULL, HasProductsWithState(subGroups, state) должна возвращать запрашиваемую последовательность SubGroups, у которой есть хотя бы один Product, который "HasProductsWithState(Product, State)1

Учитывая запрашиваемую последовательность MainGroups и нулевое логическое значение State, HasProductsWithState(mainGroups, state) должна возвращать запрашиваемую последовательность MainGroups, которая содержит все MainGroups, у которых есть хотя бы один SubGroup, который HasProductsWithState(SubGroup, State)

Решение

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

IQueryable<Product> WhereHasState(this IQueryable<Product> products, bool? state)
{
    return products.Where(product => product.IsActive == state);
}

Поскольку эта функция не проверяет, имеет ли продукт это состояние, а возвращает все продукты, которые имеют это состояние, я решил использовать другое имя.

bool HasAnyWithState(this IQueryable<Product> products, bool? state)
{
    return products.WhereHasState(state).Any();
}

Ваш код будет немного отличаться, если IsActive является свойством, не допускающим значение NULL.

Я сделаю что-то подобное с SubGroups:

IQueryable<SubGroup> WhereAnyProductHasState(this IQueryable<SubGroup> subGroups, bool? state)
{
    return subgroups.Where(subGroup => subGroup.Products.HasAnyWithState(state));
}

bool HasProductsWithState(this IQueryable<SubGroup> subGroups, bool? state)
{
     return subGroups.WhereAnyProductHasState(state).Any();
}

Что ж, теперь вы знаете упражнение для MainGroups:

IQueryable<MainGroup> WhereAnyProductHasState(this IQueryable<MainGroup> mainGroups, bool? state)
{
    return maingroups.Where(mainGroup => mainGroup.SubGroups.HasProductsWithState(state));
}

bool HasProductsWithState(this IQueryable<MainGroup> mainGroups, bool? state)
{
     return mainGroups.WhereAnyProductHasState(state).Any();
}

Если вы внимательно посмотрите, то увидите, что я не использовал никаких самоопределяемых функций. Мои вызовы функций изменят только Expression. Измененный Expression можно перевести в SQL.

Я разделил функцию на множество более мелких функций, потому что вы не сказали, хотите ли вы использовать HasProductsWithState(this IQueryable<SubGroup>, bool?) и HasProductsWithState(this IQueryable<Product>, bool?).

TODO: сделать что-то подобное для аналогичного для HasChildrenWithName: разделить на более мелкие функции, которые содержат только функции LINQ и ничего больше

Если вы будете вызывать только HasProductsWithState(this IQueryable<MainGroup>, bool?), вы можете сделать это в одной функции, используя `SelectMany:

IQueryable<MainGroup> HasProductsWithState(this IQueryable<MainGroup> mainGroups, bool? state)
{
    return mainGroups
        .Where(mainGroup => mainGroup.SelectMany(mainGroup.SubGroups)
                                     .SelectMany(subGroup => subGroup.Products)
                                     .Where(product => product.IsActive == state)
                                     .Any() );
}

Хотя это улучшает структуру настраиваемого метода расширения, оно не решает/не решает исходную проблему. То есть пользовательские методы нельзя перевести в SQL. Итак, в вашем окончательном «решении» у return maingroups.Where(mainGroup => mainGroup.SubGroups.HasProductsWithState(state)); точно такая же проблема — метод HasProductsWithState не может быть переведен. Обратите внимание, что «вызов» находится внутри дерева выражения запроса, поэтому метод вообще не будет вызываться.

Ivan Stoev 15.04.2019 16:49

@harald, это хорошее объяснение возможного решения, но, к сожалению, оно не сработает. Products в Subgroup является ICollection, поэтому subGroup.Products.HasAnyWithState(state) скажет вам, что ICollection нельзя преобразовать в IQueryable Я не хочу, чтобы загруженные элементы также были отфильтрованы 'ICollection<Subgroup>' does not contain a definition for 'HasProductsWithState' and the best extension method overload 'ProductServiceExtensions.HasProductsWithState(IQueryable<Su‌​bgroup>, bool?)' requires a receiver of type 'IQueryable<Subgroup>'

Jim 16.04.2019 19:51
Ответ принят как подходящий

But when I walk through my code at runtime it does not enter the function again when

   return maingroups.Where(x => x.Subgroups.AsQueryable().HasProductsWithState(state)

Добро пожаловать в мир деревьев выражений!

x => x.Subgroups.AsQueryable().HasProductsWithState(state)

это лямбда-выражение (Expression<Func<...>) с телом

x.Subgroups.AsQueryable().HasProductsWithState(state)

Тело — это дерево выражений, другими словами — код как данные, следовательно, выполняется никогда (за исключением случая, когда он скомпилирован для делегирования, как в LINQ to Objects).

Это легко упустить из виду, поскольку визуально лямбда-выражения выглядят как делегаты. Даже Харальд в своем ответе после всех объяснений, что не следует использовать пользовательские методы, поскольку решение фактически предоставляет несколько методов обычай с обоснованием «Я не использовал какую-либо самоопределяемую функцию. Мои вызовы функций изменяют только выражение. Измененное выражение можно преобразовать в SQL». Конечно, но если ваши функции называется! Чего, конечно же, не происходит, когда они находятся внутри дерева выражений.

С учетом сказанного, хорошего общего решения не существует. Что я могу предложить, так это решение вашей конкретной проблемы - преобразование пользовательских методов, которые получают IQueryable<T> плюс другие параметры просто и возвращают IQueryable<T>.

Идея состоит в том, чтобы использовать пользовательский ExpressionVisitor, который идентифицирует «вызовы» такого метода внутри дерева выражений, фактически вызывает их и заменяет результатом вызова.

Проблема в том, чтобы вызвать

x.Subgroups.AsQueryable().HasProductsWithState(state)

когда у нас нет реального x объекта. Хитрость заключается в том, чтобы вызвать их с поддельным запрашиваемым выражением (например, LINQ to Objects Enumerable<T>.Empty().AsQueryble()), а затем использовать другой посетитель выражения, чтобы заменить поддельное выражение исходным выражением в результате (почти как string.Replace, но для выражений).

Вот пример реализации вышеизложенного:

public static class QueryTransformExtensions
{ 
    public static IQueryable<T> TransformFilters<T>(this IQueryable<T> source)
    {
        var expression = new TranformVisitor().Visit(source.Expression);
        if (expression == source.Expression) return source;
        return source.Provider.CreateQuery<T>(expression);
    }

    class TranformVisitor : ExpressionVisitor
    {
        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            if (node.Method.IsStatic && node.Method.Name.StartsWith("Has")
                && node.Type.IsGenericType && node.Type.GetGenericTypeDefinition() == typeof(IQueryable<>)
                && node.Arguments.Count > 0 && node.Arguments.First().Type == node.Type)
            {
                var source = Visit(node.Arguments.First());
                var elementType = source.Type.GetGenericArguments()[0];
                var fakeQuery = EmptyQuery(elementType);
                var args = node.Arguments
                    .Select((arg, i) => i == 0 ? fakeQuery : Evaluate(Visit(arg)))
                    .ToArray();
                var result = (IQueryable)node.Method.Invoke(null, args);
                var transformed = result.Expression.Replace(fakeQuery.Expression, source);
                return Visit(transformed); // Apply recursively
            }
            return base.VisitMethodCall(node);
        }

        static IQueryable EmptyQuery(Type elementType) =>
            Array.CreateInstance(elementType, 0).AsQueryable();

        static object Evaluate(Expression source)
        {
            if (source is ConstantExpression constant)
                return constant.Value;
            if (source is MemberExpression member)
            {
                var instance = member.Expression != null ? Evaluate(member.Expression) : null;
                if (member.Member is FieldInfo field)
                    return field.GetValue(instance);
                if (member.Member is PropertyInfo property)
                    return property.GetValue(instance);
            }
            throw new NotSupportedException();
        }
    }

    static Expression Replace(this Expression source, Expression from, Expression to) =>
        new ReplaceVisitor { From = from, To = to }.Visit(source);

    class ReplaceVisitor : ExpressionVisitor
    {
        public Expression From;
        public Expression To;
        public override Expression Visit(Expression node) =>
            node == From ? To : base.Visit(node);
    }
}

Теперь все, что вам нужно, это вызывать .TransformFilters() методы расширения в конце ваших запросов, например, в вашем образце.

var result = query.AsQueryable()
    // ...
    .TransformFilters();

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

Обратите внимание, что пример реализации обрабатывает static методы, имеющие первый параметр IQueryable<T>, возвращая IQueryable<T> и имя, начинающееся с Has. Последнее — пропустить Queryable и методы расширения EF. В реальном коде вы должны использовать некоторые лучшие критерии — например, тип определяющего класса или пользовательский атрибут и т. д.

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