Построение вложенных выражений с помощью linq и Entity Framework - PullRequest
5 голосов
/ 10 апреля 2019

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

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

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

'LINQ to Entities не распознает метод' System.Linq.IQueryable'1 [App.Data.Models.Subgroup] HasProductsWithState [Подгруппа] (System.Linq.IQueryable'1 [App.Data.Models.Subgroup] , System.Nullable`1 [System.Boolean]) ', и этот метод не может быть преобразован в выражение хранилища.'

Как мне сделать так, чтобы запрос мог быть переведен в выражение магазина. Пожалуйста, не предлагайте .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;
        }
    }

UPDATE

Пропущенные занятия:

    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; }
    }

Ответы [ 2 ]

3 голосов
/ 14 апреля 2019

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

Вы должны знать между 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. Фактически, провайдер платформы сущностей также не знает, как переводить несколько стандартных функций LINQ, и поэтому они не могут использоваться AsQueryable. См. Поддерживаемые и неподдерживаемые методы LINQ .

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

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

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

Очевидно, у DbContext есть как минимум три DbSet: MainGroups, SubGroups и Products.

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

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

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

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

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;}
}

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

В структуре сущностей столбцы таблиц представлены не виртуальными свойствами. Виртуальные свойства представляют отношения между таблицами (один ко многим, многие ко многим).

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

Требования

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

При наличии запрашиваемой последовательности SubGroups и обнуляемого логического значения State, 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 является ненулевым свойством.

Я сделаю нечто подобное с 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() );
}
2 голосов
/ 20 апреля 2019

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

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

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

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

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

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

Тело является деревом выражений, другими словами - кодируется как данные, следовательно, никогда не выполняется (кроме случаев, когда оно скомпилировано вделегировать как в LINQ для объектов).

Это легко пропустить, поскольку визуально лямбда-выражения выглядят как делегаты.Даже Харальд в своем ответе после всех объяснений, что не следует использовать пользовательские методы, так как решение на самом деле предоставляет несколько пользовательских методов с обоснованием "Я не использовал ни одной самоопределяемой функции. Моя функциявызовы только изменят выражение. Измененное выражение может быть переведено в 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.В реальном коде вы должны использовать более подходящие критерии - например, тип определяющего класса или пользовательский атрибут и т. Д.

...