使用LINQ和Entity Framework构建嵌套表达式。

5

我试图创建一个基于筛选器返回目录的服务。

在网络上看到了一些结果,但并没有解决我的问题。希望你能帮助我。

问题是,这个查询生成器无法转换为存储表达式:

'LINQ to Entities不识别'System.Linq.IQueryable`1[App.Data.Models.Subgroup] HasProductsWithState [Subgroup](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;
        }
    }

更新

缺失的类:

    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
可能是重复的问题:Method cannot be translated into a store expression - Aleks Andreev
@AleksAndreev 这绝对是同样的错误,但不是同一个问题。他正在加入新的关系,而我正在堆叠表达式。 - Jim
它会产生相同的错误,因为像这样递归地返回maingroups.Where(x => x.Subgroups.AsQueryable().HasProductsWithState(state)。Any())作为IQueryable<TEntity>可能是错误的方法。 - Jim
你似乎有很多不必要的"AsQueryable" - 你应该在那个点上知道类型和它是否已经是"IQueryable"。 - NetMage
@Jim,你的解决方案可以使用SQL吗? - Andriy Shevchenko
显示剩余3条评论
2个回答

3

问题的原因

您需要了解 IEnumerable 和 IQueryable 之间的区别。IEnumerable 对象包含枚举所有元素所需的所有内容:您可以请求序列的第一个元素,一旦获得元素,您可以请求下一个元素,直到没有更多元素。

IQueryable 看起来类似,但是 IQueryable 并不持有枚举序列所需的所有内容。它持有一个 Expression 和一个 ProviderExpression 是必须查询的泛型形式。 Provider 知道谁必须执行查询(通常是数据库管理系统),如何与此执行器通信以及要使用哪种语言(通常类似于 SQL 的东西)。

只要您开始枚举,无论是通过显式调用 GetEnumeratorMoveNext 还是隐式地通过调用 foreachToListFirstOrDefaultCount 等,Expression 就会被发送到 Provider,后者将其转换为 SQL 并调用 DBMS。返回的数据呈现为 IEnumerable 对象,该对象使用 GetEnumerator 进行枚举。

由于 Provider 必须将 Expression 转换为 SQL,因此 Expression 只能调用可转换为 SQL 的函数。可惜,Provider 不知道 HasProductsWithState 或您自己定义的任何函数,因此无法将其转换为 SQL。实际上,Entity Framework 提供程序还不知道如何转换多个标准的 LINQ 函数,因此它们不能使用 AsQueryable。请参阅 支持和不支持的 LINQ 方法

因此,您必须坚持使用仅包含受支持函数的 Expression 的函数返回 IQueryable。

类描述

很遗憾,您忘记提供实体类信息,因此我必须对它们进行一些假设。

显然有一个 DbContext 至少有三个 DbSets:MainGroupsSubGroupsProducts

MainGroupsSubGroups 之间似乎存在一对多(或可能是多对多)关系:每个 MainGroup 都有零个或多个 SubGroups

似乎还存在一个 SubGroupsProducts 之间的一对多关系:每个 SubGroup 都有零个或多个 Products

遗憾的是,你忘了提到返回关系:每个产品是否属于一个子组(一对多),还是每个产品是否属于零个或多个子组(多对多)?

如果你遵循实体框架的代码优先约定,你会有类似这样的类:

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

这是Entity Framework需要了解的所有内容,以便检测您的表、表中的列和表之间的关系(一对多,多对多,一对零或一)。只有当您想要偏离标准命名时,才需要使用属性或流畅API。在实体框架中,表的列由非虚拟属性表示。虚拟属性表示表之间的关系(一对多,多对多)。请注意,尽管“MainGroup”的“SubGroups”被声明为集合,但如果查询“Id 10”的“MainGroup”的“SubGroups”,您仍将获得一个IQueryable。给定可查询的“Products”序列和可空布尔值“State”,“HasProductsWithState(products, state)”应返回具有“IsActive”值等于“State”的产品序列。给定可查询的“SubGroups”序列和可空布尔值“State”,“HasProductsWithState(subGroups, state)”应返回至少有一个“Product”具有“HasProductsWithState(Product, State)1”的子组序列。给定可查询的“MainGroups”序列和可空布尔值“State”,“HasProductsWithState(mainGroups, state)”应返回包含至少一个“SubGroup”具有“HasProductsWithState(SubGroup, State)”的所有“MainGroups”的可查询序列。如果将要求编写成这样,那么扩展方法就很容易:
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?)
待办事项:对于类似 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() );
}

1
虽然这改进了自定义扩展方法的结构,但并没有解决原始问题。即自定义方法无法转换为SQL。因此,在您的最终“解决方案”中,return maingroups.Where(mainGroup => mainGroup.SubGroups.HasProductsWithState(state));存在完全相同的问题 - 方法HasProductsWithState无法被转换。请注意,“调用”在查询表达式树内部,因此该方法将根本不会被调用。 - Ivan Stoev
@harald,这是一个可能解决方案的好解释,但不幸的是它行不通。Subgroup中的Products是一个ICollection,因此subGroup.Products.HasAnyWithState(state)会告诉你ICollection无法转换为IQueryable我希望急切加载的项目也能被过滤'ICollection<Subgroup>'没有包含 'HasProductsWithState' 的定义,最佳扩展方法重载 'ProductServiceExtensions.HasProductsWithState(IQueryable<Subgroup>, bool?)' 需要类型为 'IQueryable<Subgroup>' 的接收器 - Jim

2

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)

带有主体的lambda表达式(Expression<Func<...>

的含义为"最初的回答"。

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

身体是表达树,换句话说-代码作为数据,因此永远不会被执行(除非编译为委托如LINQ到对象)。
这很容易被忽视,因为视觉上lambda表达式看起来像委托。即使Harald在他们的回答中经过所有解释后指出不应使用自定义方法作为解决方案,实际上仍然提供了几个自定义方法,并解释道:“我没有使用任何自定义函数。我的函数调用只会更改Expression。更改后的Expression可以转换为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();

你也可以在中间查询上调用它。只需确保调用位于表达式树之外即可 :)。
请注意,示例实现正在处理具有第一个参数为 IQueryable<T>、返回类型为IQueryable<T> 并且名称以 Has 开头的static方法。最后一步是跳过 Queryable 和 EF 扩展方法。在实际代码中,您应该使用更好的标准 - 比如定义类的类型或自定义属性等。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接