Skip to content

UseRelationalNulls() works incorrectly when comparing nullable bools #35277

@badrshs

Description

@badrshs

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:

  1. 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();
  1. 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] = @paymentId

SQL 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] = @paymentId

Environment 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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions