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.
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 :)