【问题标题】:EF core gives error after adding a function inside "Where" lambda expression [EF-CORE 3.1]在“Where”lambda 表达式 [EF-CORE 3.1] 中添加函数后,EF 核心出错
【发布时间】:2021-10-15 16:50:42
【问题描述】:

我添加了函数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,他们添加了这个特定的查询。

【问题讨论】:

  • 能否将错误详细信息作为文本添加到问题中,而不是作为屏幕截图?
  • 因为UserHasFilter 是一个无法转换为 SQL 的 C# 函数,这就是您收到此错误的原因。
  • 我无法添加文本错误,因为我输入的是非格式化代码,所以它在 stackoverflow 中出现错误
  • 如错误所示,UserHasFilter 无法转换为 SQL。尝试将Where 移动到.Include(f =&gt; f.Conditions) 之后,否则,您将不得不重写查询的逻辑。
  • @Chetan 如何添加一个做同样事情的函数?

标签: c# entity-framework entity-framework-core ef-core-3.1


【解决方案1】:

并非所有 C# 函数,尤其是“自定义”C# 函数都不能由实体框架提供程序转换为 SQL,并且从 EF Core 3.x 开始,实体框架在尝试从服务器端静默切换时会引发异常评估到客户端评估。要解决您的问题,有 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 会将对象加载到内存中,这意味着你可以用它们做任何你想做的事情。这充其量是次优的,因为现在它正在将更多的对象加载到内存中,但有时执行更复杂查询的唯一方法是在内存中执行它们*。然而,这个解决方案确实有一个好处:派生自该类的类可以覆盖 UserHasFilter 方法,从而更改查询逻辑而无需重新创建所述查询。


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

【讨论】:

  • 你实际上可以这样做,但是你必须将你的方法调用定义为一个表达式,而不是一个函数。
  • 嗯,这真的很酷。让我来定义表达式
  • 谢谢,我会进行编辑并解释它是如何工作的
  • @ChrisSchaller 你能分享一个例子吗?
  • @alexalok 完成,关键是您的自定义函数上的 Expression&lt;Func&lt;&gt;&gt; 返回类型。
【解决方案2】:

了解实体框架提供程序只能将 LINQ 表达式树 转换为 SQL 语句,这一点很重要。当涉及到常见的 IEnumerable 函数(如 IEnumerable&lt;T&gt;.Contains)或 CLR 函数(如 String.ToUpper())时,提供程序映射这些常见的 C# 函数调用到已知的 SQL 实现。

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

即使错误消息提示了以下选项:

如果此方法可以映射到您的自定义函数,请以可翻译的形式重写查询,或通过插入对“AsEnumerable”、“AsAsyncEnumerable”、“ToList”或'ToListAsync'

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

过滤器的客户端评估是一种反模式
是的,异常消息将客户评估声明为潜在选项,但不要上当。如果您将整个数据集具体化到内存中,然后应用过滤器,那么您可能会浪费网络带宽、CPU 滴答声和执行时间。如果结果是一个大集合并且过滤器的结果是 条记录,那么您就浪费了 LOT 的资源!在 LINQ-to-SQL 场景中,我们真的想要避免所有成本的客户端过滤!

别偷懒,好好做!具有讽刺意味的是,如果您正确构建了 LINQ 表达式,您可能一开始就不会考虑 客户端评估,这很可能导致查询性能进一步下降,并且很容易产生 Stack Overflow异常或违反其他内存约束。

表达式>

您可以专门为 LINQ 定义自定义 C# 方法,方法是让它们return 成为 表达式树 形式的 lambda 表达式,即Expression&lt;Func&lt;&gt;&gt;.

当代码需要在运行前进行分析、序列化或优化时,您需要 Expression。 Expression是思考代码,Func/Action是运行。

这正是我们想要在这里实现的,我们希望启用 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 表达式,您可能会注意到,FilterUserGroupDataModelinstance 根本不会传递到此方法中!就是这个的重点,要在服务端执行,所以在SQL中,我们需要用SQL的parametersreferences来表达我们的逻辑,我们不要SQL 引擎等待每个执行实例回调到客户端以解析FilterUserGroupDataModel 的状态以及是否有任何与我们当前的userId 匹配的FilterUsers

本质上这就是错误消息所描述的内容,您已经告诉它在尝试编译成 SQL 的过程中回调 C# 函数,稍后会详细介绍...

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

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

快速重构技巧
为这些类型的表达式获取正确的方法签名很重要,并且在前几次很难正确,一个技巧是使用 Fluent 表示法将您的谓词编写为完整的.Where() 子句。然后突出显示.Where() 内的内容,右键单击并选择Quick Actions and Refactorings... 上下文菜单选项,然后选择Extract Method。这将在查询中为谓词创建一个具有正确原型的新方法。

映射的用户定义函数

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

  • 如果您确实需要它们,您可以映射到系统函数,但我们通常只对自定义 UDF 执行此操作。

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

在实际意义上,这种技术只会用于实现现有的 CLR 函数或复杂的 SQL 逻辑。

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-09-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-24
    相关资源
    最近更新 更多