-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Description
Description
After upgrading to .NET 9 and EF Core, I noticed a change in SQL translation for Boolean comparisons. Specifically, EF Core now uses bitwise operations (~ and ^) instead of a CASE statement. This leads to incorrect results when either operand in the comparison is NULL.
Expected Behavior
EF Core should generate SQL that correctly handles NULL values in Boolean comparisons, as was the case in earlier versions, where it used a CASE statement.
Example of the previous SQL generation:
SELECT CASE
WHEN [p].[PayeeId] = [p].[Payerd] THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [IsPayerSameAsPayee]Actual Behavior
After upgrading, EF Core generates SQL using bitwise operations. If either operand is NULL, the result of the operation is NULL, causing the Boolean field in the application to have unexpected values.
Example of the new SQL generation:
SELECT ~CAST([p].[PayeeId] ^ [p].[Payerd] AS bit) AS [IsPayerSameAsPayee]This behavior causes IsPayerSameAsPayee to be NULL instead of TRUE or FALSE.
Steps to Reproduce
Code
Here are two examples that reproduce the issue:
- Boolean Comparison Between Two Fields
public Task<PaymentReceiptDataModel> GetPaymentReceiptModelAsync(long paymentId) =>
UnitOfWork.Query<Payment>(p => p.Id == paymentId)
.Select(payment => new PaymentReceiptDataModel
{
IsPayerSameAsPayee = payment.PayeeId== payment.Payerd, // PayeeId or Payerd are nullable here
//... Some others attributes..
})
.FirstOrDefaultAsync();- Boolean Comparison with Enum
public Task<PlanDataModel> GetInstallmentPlanStatusAsync(long paymentItemId) =>
UnitOfWork.Query<PaymentItem>(p => p.Id == paymentItemId)
.Select(paymentItem => new PlanDataModel
{
IsActive= paymentItem.SomeObjectThatCanBeNullable.Status== PlanStatus.Active,
})
.FirstOrDefaultAsync();SQL Translation Before Upgrade ( was returning true or false always )
SELECT CASE
WHEN [p].[PayeeId] = [p].[Payerd] THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [IsPayerSameAsPayee]
FROM [Payment].[Payment] AS [p]
WHERE [p].[Id] = @paymentIdSQL Translation After Upgrade ( is returning null when either operand in the comparison is NULL )
SELECT ~CAST([p].[PayeeId] ^ [p].[Payerd] AS bit) AS [IsPayerSameAsPayee]
FROM [Payment].[Payment] AS [p]
WHERE [p].[Id] = @paymentIdEnvironment Information
EF Core version: EF Core for .NET 9
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET 9.0
Operating system: Windows 11
IDE: Visual Studio 2022 17.8
Stack Trace
The issue causes the application to throw a System.InvalidOperationException due to the mismatch between the SQL result and the expected C# data type. Since IsPayerSameAsPayee is a non-nullable bool, but the SQL translation can return NULL when either operand in the comparison is NULL, EF Core attempts to assign the NULL result to the bool property, resulting in the following exception:
System.InvalidOperationException: Nullable object must have a value.
This occurs because C# does not allow a null value to be assigned to a bool without explicitly handling it as a nullable type.
Additional Notes
This change in SQL translation does not appear in the breaking change documentation for EF Core, and after upgrading, lots of stuff started to fail a part , so I'm expecting it's a bug now