How to Parse an int in an EF Core 3 Query?
Asked Answered
S

2

5

Upon upgrading to EF Core 3, I am getting the following error at the following code:

System.InvalidOperationException: 'The LINQ expression 'DbSet .Max(c => Convert.ToInt32(c.ClaimNumber.Substring(c.ClaimNumber.Length - 6)))' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.'

var maxId = Db.Claims
    .Select(c => c.ClaimNumber.Substring(c.ClaimNumber.Length - 6))
    .Max(x => Convert.ToInt32(x));

I have also tried using int.Parse instead of Convert.ToInt32, and it produces the same error. I understand the error message. However, it's trivial to get SQL Server to parse a string to an int in T-SQL with CAST or CONVERT, I would hope there's a simple way to write the query so that it translates to a server-side operation right?

UPDATE After Claudio's excellent answer, I thought I should add some info for the next person who comes along. The reason I believed the parsing was the problem with the above code is because the following runs without error and produces the right result:

var maxId = Db.Claims
    .Select(c => c.ClaimNumber.Substring(c.ClaimNumber.Length - 6))
    .AsEnumerable()
    .Max(x => int.Parse(x));

However, I dug deeper and found that this is the SQL query EF is executing from that code:

SELECT [c].[ClaimNumber], CAST(LEN([c].[ClaimNumber]) AS int) - 6
FROM [Claims] AS [c]
WHERE [c].[ClaimNumber] IS NOT NULL

That is clearly not doing anything like what I wanted, and therefore, Claudio is right that the call to Substring is, in fact, the problem.

Schmidt answered 2/3, 2020 at 22:31 Comment(0)
T
5

Disclaimer: although feasable, I strongly recommed you do not use type conversion in your query, because causes heavy query performance degradation.

Fact is that Convert.ToInt(x) part is not the problem here. It is c.ClaimsNumber.Substring(c.ClaimNumber.Length - 6), that the EF Core translator isn't able to translate in T-SQL.

Despite RIGHT function exists in Sql Server, also, you won't able to use it with current versions of EF Core (last version is 3.1.2 at the moment I'm writing). Only solution to get what you want is to create a Sql Server user function, map it with EF Core and use it in your query.

1) Create function via migration

> dotnet ef migrations add CreateRightFunction

In newly created migration file put this code:

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

Then run db update:

dotnet ef database update

2) Map function to EF Core context

In your context class[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) Use function in your query

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

Generated query:

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

Again, this is far from best practice, I think you should consider to add an int column to your table to store this "number", whatever it represents in your domain.

Also, first time last 6 characters of ClaimNumber contain a non-digit character, this won't work anymore. If the ClaimNumber is input by a human, sooner or later this will happen.

You should code and design your database and application for robustness, even if you're super sure that those 6 characters will always represent a number. They could not do it forever :)

Tosspot answered 3/3, 2020 at 0:13 Comment(3)
Thank you! Yes, I completely agree this would be a tragic amount of code to write for what I'm trying to accomplish. I'm wondering - do you have a link to the EF documentation that says what .NET functions are supported for translation to SQL? I have seen the page but couldn't find it. It would be nice to check back later to see if/when .Substring becomes supported.Schmidt
And yes, if that were human-entered data, this code I have would be a terrible idea. It is code-generated data, and it is quite rare that the code above is ever run, so the simpler and easier to understand, and maintain, the better.Schmidt
Check out EF.Functions on official ef core documentation. I don't exclude there is some nuget package that extends EF Core and allows to use some other functions, but I know none.Tosspot
P
1

Please change your code as below. It's working for me in Dotnet core 3.1 version

var maxId = Db.Claims.Select(c => c.ClaimNumber.Substring(c.ClaimNumber.Length - 6))
    .Max(x => (Convert.ToInt32((x == null)? "0" : x.ToString())));
Pistole answered 6/10, 2021 at 15:11 Comment(1)
This can't possibly work in EF core 3. I think you're in EF core 2 which switches to client-side evaluation, or Db.Claims is not a DbSet.Cheviot

© 2022 - 2024 — McMap. All rights reserved.