[英]EF core gives error after adding a function inside "Where" lambda expression [EF-CORE 3.1]
我添加了函數
UserHasFilter<\/code>函數,這樣我就可以過濾並查看用戶是否具有按照您所看到的邏輯的過濾器,但是當我運行它時會出現以下錯誤:
我不知道我是否使用了正確的過濾方法或有更好的方法? 我也不知道錯誤是如何發生的。
這是我的代碼:
public async Task<IEnumerable<ConditionDataModel>> GetUserFilters(string pageName)
{
var user = await _configurationService.GetCurrentUser();
if (user == null)
{
return null;
}
var conditions = _context.FilterUserGroups
.Include(f => f.CompanyDataRight).ThenInclude(d => d.Page)
.Include(f => f.FilterUsers).ThenInclude(d => d.User)
.Include(f => f.FilterGroups).ThenInclude(d => d.Group).ThenInclude(g => g.UserGroups).ThenInclude(ug => ug.User)
.Where(f => f.CompanyDataRight.Page.ClassName == pageName && UserHasFilter(user.Id, f))
.Include(f => f.Conditions)
.SelectMany(f => f.Conditions)
.Distinct()
.AsEnumerable();
return conditions;
}
public virtual bool UserHasFilter(Guid userId, FilterUserGroupDataModel filterUserGroup)
{
if(filterUserGroup == null)
{
return false;
}
if (filterUserGroup.FilterUsers?.Any(u => u.User.Id == userId) == true)
{
return true;
}
return false;
}
重要的是要了解實體框架提供程序只能將 LINQ表達式樹<\/em>轉換為 SQL 語句。 當涉及到常見的
IEnumerable<\/code>函數(如
IEnumerable<T>.Contains<\/code> )或 CLR 函數(如
String.ToUpper()<\/code>時,提供程序將這些常見的 C# 函數調用映射<\/em>到已知的 SQL 實現。
這就是為什么您的標准自定義函數無法轉換<\/em>為 SQL 的原因,提供者根本沒有相應的已知<\/em>實現。 這樣想,如果我用反射來檢查一個方法,我只得到原型,所以名稱,輸入和返回類型,沒有辦法訪問該方法的內部工作。 並非所有<\/strong>CLR 函數都已映射也是事實,因此當您嘗試使用提供程序無法識別的 CLR 函數時,您將看到同樣的錯誤。
即使錯誤消息建議了以下選項:
如果此方法可以映射到您的自定義函數,則以可翻譯的形式重寫查詢,或通過插入對“AsEnumerable”、“AsAsyncEnumerable”、“ToList”或“ToListAsync”的調用顯式切換到客戶端評估
關於 SO 的許多答案只是告訴 OP 切換到客戶端評估,而實際上這是一種hack<\/em> ,並且通常有更好的解決方案,特別是對於我們封裝常見業務表達式以供重用的自定義函數。 我們專門編寫這些是因為我們希望它們被定義一次並重復使用,現在讓我們看看如何<\/em>正確編寫它們!
過濾器的客戶評估是一種反模式<\/em>!<\/strong>
是的,異常消息將客戶評估<\/em>列為潛在<\/em>選項,但不要被愚弄。 如果您將整個數據集具體化到內存中,然后應用過濾器,那么您可能會浪費網絡帶寬、CPU 滴答聲和執行時間。 如果結果集很大並且過濾器的結果為零<\/em>記錄,那么您就浪費了很多<\/strong>資源! 在 LINQ-to-SQL 場景中,我們真的<\/em>希望不惜一切代價<\/em>避免<\/em>客戶端過濾!
不要偷懶,正確地去做! 具有諷刺意味的是,如果您正確構建了 LINQ 表達式,您可能一開始就不會考慮客戶端評估<\/em>,這很可能導致進一步降低查詢性能,並且很容易產生堆棧溢出異常或違反其他內存約束。
表達式<Func<>><\/h3>
您可以專門為 LINQ 定義自定義 C# 方法,使它們以表達式樹<\/em>的形式返回<\/em>lambda 表達式<\/em>,即
Expression<Func<>><\/code> 。
當代碼需要在運行之前進行分析、序列化或優化時,您需要 Expression。 表達式用於思考代碼,Func\/Action 用於運行代碼。<\/blockquote>這正是我們在這里要實現的目標,我們希望使<\/em>LINQ 提供程序能夠分析<\/em>代碼以將其轉換為 SQL!
這實際上應該轉換為 SQL
CASE<\/code>表達式,您可能會注意到,
FilterUserGroupDataModel<\/code>的實例<\/em>根本不會傳遞給此方法!
這就是重點,要在服務端執行,所以在 SQL 中,我們需要使用 SQL參數<\/em>和引用<\/em>來表達我們的邏輯,我們不希望 SQL 引擎等待每個執行實例回調客戶端來解析
FilterUserGroupDataModel<\/code>的狀態,以及它是否有任何與我們當前的
userId<\/code>匹配的
FilterUsers<\/code> 。
本質上,這就是錯誤消息所描述的內容,您已經告訴它在嘗試編譯成 SQL 的過程中回調 C# 函數,稍后會詳細介紹...
這個的實現只是略有不同,再次注意我們沒有通過對當前的引用
快速重構黑客<\/strong>為這些類型的表達式獲取正確的方法簽名很重要,並且在前幾次很難正確,一個技巧是使用Fluent<\/em>表示法將您的謂詞編寫為完整的
.Where()<\/code>子句。
然后突出顯示
.Where()<\/code>中的內容,右鍵單擊並選擇Quick Actions and Refactorings...<\/kbd>上下文菜單選項,然后選擇Extract Method<\/kbd> 。
這將在查詢中為謂詞創建一個具有正確原型的新方法。
<\/blockquote>
映射的用戶定義函數<\/h3>
當然還有另一種方式,那就是我們可以在 C# 中定義一個自定義函數並將其映射<\/em>到 SQL 函數。 映射意味着根本不會解釋實現,數據庫中的函數<\/em>將代表要使用的 SQL 實現。
- 如果您確實需要它們,您可以映射到系統函數,但我們通常只對自定義 UDF 執行此操作。<\/li><\/ul>
這種技術對於遺留應用程序或 DBA 真正想要管理相關邏輯的大型組織很有幫助,使用該技術的應用程序通常也包含許多映射的存儲過程<\/em>。
在實際意義上,這種技術只會用於實現現有的 CLR 函數或復雜的 SQL 邏輯。使用 .NET EF Core 將 CLR 方法映射到 SQL 函數<\/a>介紹了機制,但基本上我們創建了一個 C# 和函數的 SQL 定義,然后我們可以將函數映射<\/em>到 DbContext。
當
IQueryable<\/code> LINQ-to-SQL 提供程序轉換表達式時,它將完全忽略<\/em>C# 實現,但我們仍會執行 C# 實現以防非 LINQ-to-SQL 上下文使用該函數或在客戶端上評估查詢.
<\/li><\/ul>
我不會在這里發布此方法的實現,因為邏輯需要在外部表中查找數據。<\/em> 這不<\/strong>適合作為用戶定義的函數<\/strong>來實現。<\/em> UDF 應該是基於輸入和內部常量的自包含或靜態計算,並且不影響或從外部資源中選擇,視圖和存儲過程是封裝該類型邏輯的更好機制。<\/em>
"
並非所有 C# 函數,尤其是“自定義”C# 函數都不能由實體框架提供程序轉換為 SQL,並且從 EF Core 3.x 實體框架開始,當它嘗試從服務器端評估以靜默方式切換到客戶端時將引發異常側面評價。 要解決您的問題,有兩種解決方案。
AsEnumerable()
手動切換到客戶端評估。以下是如何做#2:
var conditions = _context.FilterUserGroups
.Include(f => f.CompanyDataRight).ThenInclude(d => d.Page)
.Include(f => f.FilterUsers).ThenInclude(d => d.User)
.Include(f => f.FilterGroups).ThenInclude(d => d.Group).ThenInclude(g => g.UserGroups).ThenInclude(ug => ug.User)
.Where(f => f.CompanyDataRight.Page.ClassName == pageName && (f.FilterUsers != null && f.FilterUsers.Any(u => u.User.Id == user.Id)))
.Include(f => f.Conditions)
.SelectMany(f => f.Conditions)
.Distinct()
.AsEnumerable();
這應該有效(我目前無法測試)。 我所做的是將您的方法調用內聯重寫為語句,EF Core 應該能夠將其轉換為 SQL。 如果沒有(並且您無法自己修復它),總是有選項 #1:切換到客戶端評估,這就是您“最佳”這樣做的方式:
var conditions = _context.FilterUserGroups
.Include(f => f.CompanyDataRight).ThenInclude(d => d.Page)
.Include(f => f.FilterUsers).ThenInclude(d => d.User)
.Include(f => f.FilterGroups).ThenInclude(d => d.Group).ThenInclude(g => g.UserGroups).ThenInclude(ug => ug.User)
.Include(f => f.Conditions)
.SelectMany(f => f.Conditions)
.Distinct()
.AsEnumerable()
.Where(f => f.CompanyDataRight.Page.ClassName == pageName && UserHasFilter(user.Id, f));
看看我是如何在AsEnumerable
之后移動Where
的,當您調用AsEnumerable
時,EF 將對象加載到內存中,這意味着您可以對它們執行任何操作。 這充其量是次優的,因為現在它加載到內存中的對象比實際應該多,但有時執行更復雜查詢的唯一方法是在內存中執行它們*。 但是,此解決方案確實有一個好處:從此類派生的類可以覆蓋UserHasFilter
方法,更改查詢邏輯而無需重新創建所述查詢。
* 並不是說僅使用 SQL 就無法實現這一點,只是 EF 無法將每個 LINQ 查詢轉換為 SQL
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.