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

时间:2019-04-10 20:16:19

标签: c# entity-framework linq lambda expression

我正在尝试提供一种基于过滤器返回目录的服务。

我在互联网上看到了一些结果,但不是我的问题。我希望你能帮助我。

问题在于此查询版本无法转换为商店表达式:

  

'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 个答案:

答案 0 :(得分:3)

问题原因

您必须知道IEnumerable和IQueryable之间。 IEnumerable对象包含所有要枚举所有元素的内容:您可以要求序列中的第一个元素,一旦有了一个元素,就可以要求下一个元素,直到没有其他元素为止。 / p>

一个IQueryable看起来很相似,但是IQueryable并不保存所有枚举序列。它包含一个Expression和一个ProviderExpression是必须查询内容的通用形式。 Provider知道谁必须执行查询(通常是数据库管理系统),如何与该执行程序通信以及使用哪种语言(通常是类似SQL的语言)。

一旦您开始枚举,要么通过调用GetEnumeratorMoveNext显式,要么通过调用foreachToListFirstOrDefault,{{ 1}}等,Count被发送到Expression,后者将其转换为SQL并调用DBMS。返回的数据显示为IEnumerable对象,该对象使用Provider

进行枚举。

由于提供者必须将GetEnumerator转换为SQL,因此Expression只能调用可以转换为SQL的函数。 {,Expression不知道Provider,也不知道您自己定义的任何函数,因此无法将其转换为SQL。实际上,实体框架提供者也不知道如何转换几个标准LINQ函数,因此不能HasProductsWithState使用它们。参见Supported and Unsupported LINQ methods

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

类说明

A,您忘了给我们提供您的实体类,所以我必须对它们进行一些假设。

显然具有至少三个DbSet的DbContext:AsQueryableMainGroupsSubGroups

ProductsMaingGroups之间似乎存在一对多(或可能的多对多)关系:每个SubGroups都有零个或多个{{1} }。

MainGroupSubGroups之间似乎也存在一对多的关系:每个SubGroups都有零个或多个Products

las,您忘了提到返回关系:每个SubGroup恰好属于一个Products(一对多),还是每个Product属于零个或多个{ {1}}(多对多)?

如果您遵循实体框架代码的第一约定,那么您将拥有与以下类似的类:

SubGroup

与产品类似的事物:

Product

还有您的DbContext:

SubGroups

这是实体框架检测表,表中的列以及表之间的关系(一对多,多对多,一对零或一个)所需了解的所有信息)。仅当您想要偏离标准命名时,才需要fluent API的属性。

  

在实体框架中,表的列由非虚拟属性表示。虚拟属性表示表之间的关系(一对多,多对多)。

请注意,尽管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;} } 的{​​{1}}被声明为集合,但是如果您查询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;} } 的{​​{1}},您仍然会得到一个IQueryable。 / p>

要求

鉴于class MyDbContext : DbContext { public DbSet<MainGroup> MainGroups {get; set;} public DbSet<SubGroup> SubGroups {get; set;} public DbSet<Product> Products {get; set;} } 的可查询序列和可为空的布尔值SubGroupsMainGroup应该返回SubGroups的可查询序列,其值MaingGroup with Id 10等于到Products

给出State的可查询序列和可为空的布尔值HasProductsWithState(products, state)Products应该返回IsActive的可查询序列,其中至少有一个State “ HasProductsWithState(Product,State)1

给出SubGroups的可查询序列和可为空的布尔值StateHasProductsWithState(subGroups, state)应该返回SubGroups的可查询序列,其中包含所有具有以下内容的Product至少有一个MainGroups

解决方案

好吧,如果您这样编写需求,扩展方法很简单:

State

因为此函数不检查产品是否具有此状态,而是返回所有具有此状态的产品,所以我选择使用其他名称。

HasProductsWithState(mainGroups, state)

如果IsActive是不可为空的属性,则您的代码将略有不同。

我将对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(); } 可以翻译成SQL。

我将功能分为许多较小的功能,因为您没有说是否要使用SubGroupsIQueryable<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(); }

TODO:对MainGroups做类似的事情:分成一些仅包含LINQ函数的小函数,

如果仅调用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(); } ,则可以使用SelectMany:

Expression

答案 1 :(得分:2)

  

但是当我在运行时浏览代码时,

不会再次输入该函数
   return maingroups.Where(x => x.Subgroups.AsQueryable().HasProductsWithState(state)

欢迎来到表达树的世界!

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

是带有正文的lambda表达式(Expression<Func<...>

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

主体是表达式树,换句话说,就是作为数据的代码,因此从不执行(除非按照LINQ to Objects中的委托进行编译)。

由于视觉上的lambda表达式看起来像委托,因此它很容易被忽略。甚至Harald在回答所有解释后都回答说不应该使用自定义方法,因为一种解决方案实际上提供了几种 custom 方法,其原理是“我没有使用任何自定义函数。函数调用只会更改表达式。更改后的表达式可以转换为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的{​​{1}}方法,返回IQueryable<T>和以IQueryable<T>开头的名称。最后是跳过Has和EF扩展方法。在实际代码中,您应该使用一些更好的条件-例如,定义类的类型或自定义属性等。