[英]Nested expression building with linq and 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() };
在下面的代码中,您可以看到我递归调用扩展名以尝试堆叠表达式。 但是当我在运行时浏览代码时,当
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; }
}
您必须知道IEnumerable和IQueryable之间。 IEnumerable对象包含所有要枚举所有元素的内容:您可以要求序列中的第一个元素,一旦有了一个元素,就可以要求下一个元素,直到没有其他元素为止。
一个IQueryable似乎很相似,但是,该IQueryable并不保存所有枚举序列。 它包含一个Expression
和一个Provider
。 Expression
是必须查询的内容的通用形式。 Provider
知道谁必须执行查询(通常是数据库管理系统),如何与该执行程序通信以及使用哪种语言(通常是类似SQL的语言)。
一旦开始枚举,或者通过调用GetEnumerator
和MoveNext
显式地进行枚举,或者通过调用foreach
, ToList
, FirstOrDefault
, Count
等隐式地进行枚举,则将Expression
发送给Provider
,后者将其转换为SQL并调用DBMS。 返回的数据显示为IEnumerable对象,该对象使用GetEnumerator
枚举。
因为提供者必须将Expression
转换为SQL,所以Expression
只能调用可以转换为SQL的函数。 HasProductsWithState
, Provider
不知道HasProductsWithState
,也不知道您自己定义的任何函数,因此无法将其转换为SQL。 实际上,实体框架提供者也不知道如何转换几个标准LINQ函数,因此不能将它们用作AsQueryable
。 请参阅受支持和不受支持的LINQ方法 。
因此,您必须坚持返回IQueryable的函数,其中Expression仅包含受支持的函数。
las,您忘了给我们提供您的实体类,所以我必须对它们进行一些假设。
显然,有至少三个DbSets一个的DbContext: MainGroups
, SubGroups
和Products
。
似乎有之间的一个一对多(或可能的许多一对多)关系MaingGroups
和SubGroups
:每MainGroup
具有零个或多个SubGroups
。
子SubGroups
和Products
之间似乎也存在一对多的关系:每个SubGroup
具有零个或多个Products
。
las,您忘了提到返回关系:每个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;}
}
以及您的DbContext:
class MyDbContext : DbContext
{
public DbSet<MainGroup> MainGroups {get; set;}
public DbSet<SubGroup> SubGroups {get; set;}
public DbSet<Product> Products {get; set;}
}
这是实体框架检测表,表中的列以及表之间的关系(一对多,多对多,一对零或一)所需了解的所有信息。 仅当您想要偏离标准命名时,才需要fluent API的属性。
在实体框架中,表的列由非虚拟属性表示。 虚拟属性表示表之间的关系(一对多,多对多)。
请注意,虽然SubGroups
一的MainGroup
被声明为一个集合,如果查询SubGroups
的的MaingGroup with Id 10
,你会仍然得到一个IQueryable。
给定Products
的可查询序列和可为null的布尔State
, HasProductsWithState(products, state)
应返回IsActive
值等于State
的Products
的可查询序列
鉴于可查询的序列SubGroups
和一个可空布尔State
, HasProductsWithState(subGroups, state)
应该返回的可查询的序列SubGroups
是至少有一个Product
是“HasProductsWithState(产品,国家)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);
}
因为此函数不检查Product是否具有此状态,而是返回所有具有此状态的Product,所以我选择使用其他名称。
bool HasAnyWithState(this IQueryable<Product> products, bool? state)
{
return products.WhereHasState(state).Any();
}
如果IsActive是不可为空的属性,则您的代码将略有不同。
我将对SubGroups
做类似的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() );
}
但是当我在运行时浏览代码时,当
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();
您也可以在中间查询中调用它。 只要确保调用在表达式树之外即可:)
请注意,示例实现正在处理具有第一个参数IQueryable<T>
static
方法,返回IQueryable<T>
和以Has
开头的名称。 最后是跳过Queryable
和EF扩展方法。 在实际代码中,您应该使用一些更好的条件-例如,定义类的类型或自定义属性等。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.