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

问题描述 投票:5回答:2

我正在尝试创建一个基于过滤器返回目录的服务。

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

问题是此查询构建无法转换为商店表达式:

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

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; }
    }
c# entity-framework linq lambda expression
2个回答
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在回答所有解释之后都不应该使用自定义方法,因为解决方案实际上提供了几个自定义方法,理由是“我没有使用任何自定义函数。我的函数调用只会改变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();

您也可以在中间查询中调用它。只要确保调用是在表达式树之外:)

请注意,示例实现正在处理具有第一个参数staticIQueryable<T>方法,返回IQueryable<T>并以Has开头的名称。最后一个是跳过Queryable和EF扩展方法。在实际代码中,您应该使用一些更好的标准 - 例如定义类的类型或自定义属性等。


3
投票

你的问题的原因

您必须知道IEnumerable和IQueryable之间。一个IEnumerable对象包含所有元素的所有内容:你可以要求序列的第一个元素,一旦你有了一个元素,你可以要求下一个元素,直到没有更多的元素。

IQueryable看起来很相似,但是,IQueryable并不能容纳枚举序列的所有内容。它拥有ExpressionProviderExpression是必须查询的通用形式。 Provider知道谁必须执行查询(通常是数据库管理系统),如何与此执行程序通信以及使用哪种语言(通常类似于SQL)。

一旦你开始枚举,或者通过调用GetEnumeratorMoveNext明确地,或通过调用foreachToListFirstOrDefaultCount等隐式地,Expression被发送到ProviderGetEnumerator将其转换为SQL并调用DBMS。返回的数据显示为IEnumerable对象,使用Expression枚举该对象

因为Provider必须将Expression转换为SQL,所以Provider可能只调用可以转换为SQL的函数。唉,HasProductsWithState不知道AsQueryable,也不知道你自己定义的任何函数,因此无法将其转换为SQL。实际上,实体框架提供程序也不知道如何翻译几个标准LINQ函数,因此它们不能用于Supported and Unsupported LINQ methods。见MainGroups

所以你必须坚持返回IQueryable的函数,其中Expression只包含支持的函数。

课程描述

唉,你忘了给我们你的实体课程,所以我必须对它们做一些假设。

显然有一个至少有三个DbSets的DbContext:SubGroupsProductsMaingGroups

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

似乎ProductsSubGroup之间也存在一对多关系:每个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;}
}

类似于产品的东西:

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

当然还有你的DbContext:

SubGroups

这就是实体框架在检测表,表中的列以及表之间的关系(一对多,多对多,一对零或一对)时需要知道的全部内容。只有当您想要偏离标准命名时,才需要使用流畅的API属性。

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

请注意,虽然MainGroupSubGroups被声明为集合,但如果你查询MaingGroup with Id 10Products,你仍然会得到一个IQueryable。

要求

给定一个可查询的State序列和一个可空的布尔值HasProductsWithState(products, state)Products应该返回IsActive的可查询序列,其值为State等于SubGroups

给定一个可查询的State序列和一个可空的布尔值HasProductsWithState(subGroups, state)SubGroups应返回Product的可查询序列,该序列至少有一个MainGroups“HasProductsWithState(Product,State)1

鉴于可查询的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();
}

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

SubGroups

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

我会用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。改变后的HasProductsWithState(this IQueryable<SubGroup>, bool?)可以翻译成SQL。

我把这个函数分成了很多小函数,因为你没有说你是否想要使用HasProductsWithState(this IQueryable<Product>, bool?)HasChildrenWithName

TODO:为类似的HasProductsWithState(this IQueryable<MainGroup>, bool?)执行类似的操作:分成只包含LINQ函数的小函数,没有别的

如果您只调用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() ); } ,可以使用`SelectMany在一个函数中执行:

qazxswpoi
© www.soinside.com 2019 - 2024. All rights reserved.