[英]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.