EF 核心在 "Where" lambda 表达式 [EF-CORE 3.1] 中添加函数后出错

EF core gives error after adding a function inside "Where" lambda expression [EF-CORE 3.1]

我添加了 UserHasFilter 功能,这样我就可以过滤并查看用户是否按照您所看到的逻辑进行过滤,但是当我 运行 它给出以下错误时:

我不知道我是否使用了正确的方法来过滤或者是否有更好的方法? 我也不知道错误是怎么发生的。

这是我的代码:

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

编辑:

感谢@MindSwipe,我对查询进行了更改:

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) // checks if a filter user contains the current user
                            || (f.FilterGroups != null && f.FilterGroups.Any(g => g.Group != null && g.Group.UserGroups.Any(ug => ug.UserId == user.Id))))) // checks if user group has the current user
                .Include(f => f.Conditions)
                .SelectMany(f => f.Conditions)
                .Distinct()
                .AsEnumerable();

因为我需要在数据库上执行查询,所以这个查询应该不会花费很多时间(在内存中)。

更新:

它适用于 EF 5,他们添加了这个特定的查询。

并非所有 C# 函数,尤其是没有“自定义”C# 函数可以由 Entity Framework 提供程序翻译成 SQL,并且从 EF Core 3.x Entity Framework 当它试图静默地从服务器端评估切换到客户端评估时会抛出异常。要解决您的问题,有 2 个解决方案。

  1. 通过提前调用 AsEnumerable() 手动切换到客户端评估。
  2. 重写您的 LINQ 查询,以便 EF Core 可以将其转换为 SQL。

#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 会将对象加载到内存中,这意味着您可以随心所欲地使用它们。这充其量是次优的,因为现在它加载到内存中的对象比实际应该的要多,但有时执行更复杂查询的唯一方法是在内存中执行它们*。然而,这个解决方案确实有一个好处:从这个 class 派生的 class 可以覆盖 UserHasFilter 方法,改变查询逻辑而不必重新创建所述查询。


* 并不是说​​仅用 SQL 就无法实现这一点,只是 EF 无法将每个 LINQ 查询转换为 SQL

了解 Entity Framework 提供程序只能将 LINQ 表达式树 转换为 SQL 语句很重要。当涉及像 IEnumerable<T>.Contains 这样的常见 IEnumerable 函数或像 String.ToUpper() 这样的 CLR 函数时,提供程序 maps 这些常见的 C# 函数调用已知 SQL 实施。

这就是为什么您的标准自定义函数不能翻译到SQL的原因,提供者只是没有对应的已知 执行。这样想,如果我使用反射来检查一个方法,我只能得到原型,所以名称、输入和 return 类型,没有办法访问该方法的内部工作。确实并非所有 ALL CLR 函数都已映射,因此当您尝试使用提供程序无法识别的 CLR 函数时,您会看到同样的错误。

即使错误消息提示这些选项:

If this method can be mapped to your custom function, Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'

关于 SO 的许多答案只是告诉 OP 切换到客户端评估,而实际上这是一个 hack 并且通常有更好的解决方案,特别是对于我们封装通用的自定义函数re-use 的业务表达式。我们专门写这些是因为我们希望它们被定义一次并且re-used,现在让我们看看如何正确地写它们!

Client Evaluation of Filter is an Anti-Pattern!
Yes the exception message states client evaluation as a potential option, but don't be fooled. If you materialise the entire dataset into memory and then apply a filter, then you may have wasting network bandwidth, CPU ticks and execution time. If the results is a large set and the filter results in zero records then you have wasted a LOT of resources! In LINQ-to-SQL scenarios we really want to avoid client-side filtering at ALL costs!

Don't be lazy, do it properly! The irony of this is that if you constructed your LINQ expressions correctly you would not have likely thought about client evaluation in the first place which most likely to result in further degrading query performance and can easily spawn off Stack Overflow exceptions or violate other Memory Constraints.

表达式<函数<>>

您可以通过将 return 设为 lambda 表达式 来定义专门用于 LINQ 的自定义 C# 方法表达式树,即Expression<Func<>>.

You need Expression when the code needs to be analyzed, serialized, or optimized before it is run. Expression is for thinking about code, Func/Action is for running it.

这正是我们想要在这里实现的,我们想要启用 LINQ 提供程序以分析 代码以将其转换为SQL!

public virtual Expression<Func<FilterUserGroupDataModel, bool>> UserHasFilter(Guid userId)
{
    return x => x == null ? false :
                x.FilterUsers == null ? false :
                x.FilterUsers.Any(u => u.User.Id == userId);
}

这实际上应该转化为SQL CASE表达式,你可能注意到这里的instance of the FilterUserGroupDataModel是从来没有传递给这个方法!就是这个意思,要在服务器中执行,所以在SQL中,我们需要使用SQL参数references 来表达我们的逻辑,我们不希望 SQL 引擎等待每个执行实例回调到客户端来解析 FilterUserGroupDataModel 的状态,如果它有任何 FilterUsers 匹配我们当前的 userId.

That is in essence what the error message is describing, you have told it to call back to the C# function in the middle of what it is trying to compile into SQL, more on this later...

这个的实现只是略有不同,再次注意我们没有传递对当前的引用

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)
        .Where(UserHasFilter(user.Id)) // <-- this is the custom function call
        .Include(f => f.Conditions)
        .SelectMany(f => f.Conditions)
        .Distinct()
        .AsEnumerable();
    return conditions;
}

Quick Refactor Hack
Getting the method signature correct for these types of expressions is important and hard to get right the first couple of times, one hack is to write your predicate as a complete .Where() clause using Fluent notation. Then highlight the content inside the .Where(), right click and select the Quick Actions and Refactorings... context menu option, then Extract Method. This will create a new method with the correct prototype for a predicate at that point in the query.

映射的用户定义函数

当然还有另一种方法,那就是我们可以在C#中定义一个自定义函数,映射到一个SQL函数。映射意味着根本不会解释实现,数据库中的 Function 将代表要使用的 SQL 实现。

  • 如果您真的需要它们,您可以映射到系统函数,但我们通常只对自定义 UDF 这样做。

这项技术对于遗留应用程序或 DBA 真正想要管理相关逻辑的大型组织很有帮助,使用该技术的应用程序通常会包含大量映射的 存储过程 还有。

In a practical sense, this technique would only be used to implement existing CLR functions or complicated SQL logic.

Map CLR Method to a SQL Function using .NET EF Core 介绍了机制,但基本上我们创建了一个 C# 和函数的 SQL 定义,然后我们可以 Map 函数到DbContext.

  • IQueryable LINQ-to-SQL 提供程序翻译表达式时,它将 完全忽略 C# 实现,但我们仍然进行 C# 实现以防函数由非 LINQ-to-SQL 上下文使用,或者如果查询是在客户端上评估的。

我不会post在这里实现这个方法,因为逻辑需要在外部表中查找数据。这 适合作为 用户定义函数 实现。 UDF 应该是独立的或基于输入和内部常量的静态计算,并且尽可能不影响或 select 来自外部资源,视图和存储过程是封装此类逻辑的更好机制。