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

Jim*_*Jim 5 c# linq lambda expression entity-framework

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

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

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

“ LINQ to Entities无法识别方法'System.Linq.IQueryable'1 [App.Data.Models.Subgroup] HasProductsWithState [Subgroup](System.Linq.IQueryable'1 [App.Data.Models.Subgroup],系统。 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() };
Run Code Online (Sandbox Code Playgroud)

在下面的代码中,您可以看到我递归调用扩展名以尝试堆叠表达式。但是当我在运行时浏览代码时,当

    return maingroups.Where(x => x.Subgroups.AsQueryable().HasProductsWithState(state).Any()) as IQueryable<TEntity>;
Run Code Online (Sandbox Code Playgroud)

叫做。

    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;
        }
    }
Run Code Online (Sandbox Code Playgroud)

更新

缺少课程:

    public class Maingroup
    {
        public long Id { get; set; }
        public string Name { get; set; }
        ...
        public virtual ICollection<Subgroup> Subgroups { get; set; }
    }
Run Code Online (Sandbox Code Playgroud)
    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; }
    }
Run Code Online (Sandbox Code Playgroud)
    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; }
    }
Run Code Online (Sandbox Code Playgroud)

Iva*_*oev 2

但是,当我在运行时遍历代码时,它不会再次进入该函数

   return maingroups.Where(x => x.Subgroups.AsQueryable().HasProductsWithState(state)
Run Code Online (Sandbox Code Playgroud)

欢迎来到表达式树的世界!

x => x.Subgroups.AsQueryable().HasProductsWithState(state)
Run Code Online (Sandbox Code Playgroud)

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

x.Subgroups.AsQueryable().HasProductsWithState(state)
Run Code Online (Sandbox Code Playgroud)

主体是表达式树,换句话说 - 代码即数据,因此永远不会执行(除非编译为委托,如 LINQ to Objects 中)。

它很容易被忽视,因为 lambda 表达式在视觉上看起来像委托。即使 Harald 在他们的回答中解释了不应使用自定义方法,作为一种解决方案,实际上提供了几种自定义方法,其理由是“我没有使用任何自定义函数。我的函数调用只会更改表达式。更改后的结果表达式可以翻译成SQL”。当然可以,但是如果你的函数被调用了!当它们位于表达式树内部时,这当然不会发生。

话虽如此,没有好的通用解决方案。我可以提供的是针对您的特定问题的解决方案 - 转换接收IQueryable<T>其他简单参数并返回的自定义方法IQueryable<T>

这个想法是使用自定义ExpressionVisitor来标识表达式树内此类方法的“调用”,实际调用它们并将它们替换为调用结果。

问题是打电话

x.Subgroups.AsQueryable().HasProductsWithState(state)
Run Code Online (Sandbox Code Playgroud)

当我们没有实际的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);
    }
}
Run Code Online (Sandbox Code Playgroud)

.TransformFilters()现在您需要做的就是在查询结束时调用扩展方法,例如在示例中

var result = query.AsQueryable()
    // ...
    .TransformFilters();
Run Code Online (Sandbox Code Playgroud)

您还可以在中间查询中调用它。只需确保调用在表达式树外部即可:)

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