如何在EF Core 3查询中解析int?

5
升级到EF Core 3后,在以下代码处出现以下错误:
System.InvalidOperationException: '无法翻译LINQ表达式'DbSet.Max(c => Convert.ToInt32(c.ClaimNumber.Substring(c.ClaimNumber.Length - 6)))'。要么以可翻译的形式重写查询,要么通过插入对AsEnumerable()、AsAsyncEnumerable()、ToList()或ToListAsync()之一的调用来显式地切换到客户端评估。请参见https://go.microsoft.com/fwlink/?linkid=2101038了解更多信息。'
var maxId = Db.Claims
    .Select(c => c.ClaimNumber.Substring(c.ClaimNumber.Length - 6))
    .Max(x => Convert.ToInt32(x));

我还尝试过使用int.Parse而不是Convert.ToInt32,但仍然出现相同的错误。我理解这个错误信息。然而,在T-SQL中,使用CAST或CONVERT让SQL Server将字符串解析为int非常简单,我希望有一种简单的方法编写查询,以便它转换为服务器端操作,对吗? 更新 在Claudio的出色回答之后,我认为应该添加一些信息供下一个人参考。我认为解析是上述代码的问题所在,原因是以下代码可以正常运行并产生正确的结果:
var maxId = Db.Claims
    .Select(c => c.ClaimNumber.Substring(c.ClaimNumber.Length - 6))
    .AsEnumerable()
    .Max(x => int.Parse(x));

然而,我深入挖掘后发现这是EF从代码中执行的SQL查询:
SELECT [c].[ClaimNumber], CAST(LEN([c].[ClaimNumber]) AS int) - 6
FROM [Claims] AS [c]
WHERE [c].[ClaimNumber] IS NOT NULL

显然,它没有做我想要的任何事情,因此,Claudio正确地指出了调用 Substring 是问题所在。

2个回答

5

免责声明:虽然可行,但我强烈建议您不要在查询中使用类型转换,因为会导致严重的性能下降。

事实是Convert.ToInt(x)部分并不是问题所在。问题出在c.ClaimsNumber.Substring(c.ClaimNumber.Length - 6)上,EF Core翻译器无法将其翻译成T-SQL。

尽管 SQL Server 中存在 RIGHT 函数,您也不能在当前版本的 EF Core(我撰写本文时最新版本为 3.1.2)中使用它。 获取所需结果的唯一解决方案是创建一个 Sql Server 用户函数,并将其映射到 EF Core 中,在查询中使用该函数。

1) 通过迁移创建函数

> dotnet ef migrations add CreateRightFunction

在新创建的迁移文件中加入以下代码:

public partial class CreateRightFunctions : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.Sql(@"
CREATE FUNCTION fn_Right(@input nvarchar(4000), @howMany int)
RETURNS nvarchar(4000)
BEGIN
RETURN RIGHT(@input, @howMany)
END
");
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.Sql(@"
DROP FUNCTION fn_Right
");
    }
}

然后运行数据库更新:

dotnet ef database update

2) 将函数映射到 EF Core 上下文

在你的上下文类中[DbFunction("fn_Right")]

public static string Right(string input, int howMany)
{
    throw new NotImplementedException(); // this code doesn't get executed; the call is passed through to the database function
}

3)在查询中使用函数

var maxId = Db.Claims.Select(c => MyContext.Right(c.ClaimNumber, 6)).Max(x => Convert.ToInt32(x));

生成的查询:

SELECT MAX(CONVERT(int, [dbo].[fn_Right]([c].[ClaimNumber], 6)))
FROM [Claims] AS [c]

再次强调,这远非最佳实践。我认为你应该考虑向表中添加一个int列来存储这个“数字”,无论它在你的领域中代表什么。

此外,如果ClaimNumber的前6个字符中第一次出现非数字字符,那么这种方法将不再起作用。如果ClaimNumber是由人工输入的,那么迟早会发生这种情况。

即使你非常确定这6个字符将永远代表一个数字,也应该编写和设计稳健的数据库和应用程序。它们可能不会永远这样。


谢谢!是的,我完全同意这将是一种可悲的编码量,与我想要实现的目标相比。我在想——你有一个链接到EF文档,说明.NET函数支持转换为SQL吗?我看过这个页面,但找不到它。以后回来检查是否/何时支持.Substring将是很好的。 - pbristow
是的,如果那是人工输入的数据,我现在的代码就是个糟糕的想法。这是由代码生成的数据,而且上面的代码很少被运行,所以越简单易懂、易于维护,越好。 - pbristow
请查看官方 EF Core 文档中的 EF.Functions。我不排除有一些 NuGet 包可以扩展 EF Core 并允许使用其他函数,但我不知道有哪些。 - Claudio Valerio

1
请按照以下方式更改您的代码。它在Dotnet core 3.1版本中为我工作。
var maxId = Db.Claims.Select(c => c.ClaimNumber.Substring(c.ClaimNumber.Length - 6))
    .Max(x => (Convert.ToInt32((x == null)? "0" : x.ToString())));

2
这在 EF Core 3 中不可能起作用。我认为你使用的是 EF Core 2,它会切换到客户端评估,或者 Db.Claims 不是一个 DbSet - Gert Arnold

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接