EF 7.0.9 returns a non-null value despite explicitly selecting for a nullable value.
EF 7.0.9 returns a non-null value despite explicitly selecting for a nullable value.
Seems similar to https://github.com/dotnet/efcore/issues/26744
Here is a complete Console app repro:
Program.cs
using Microsoft.EntityFrameworkCore;
// Reset DB
var db = new TestContext();
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
// Add Customers
{
// #1 - customer with active plan
db.Add(new Customer { Id = 1, ServicePlan = new ServicePlan { TerminationDate = null } });
// #2 - customer with past plan
db.Add(new Customer { Id = 2, ServicePlan = new ServicePlan { TerminationDate = DateTime.Now.AddDays(-1) } });
// #3 - customer who never had a plan
db.Add(new Customer { Id = 3, ServicePlan = null });
db.SaveChanges();
}
// Test queries
// OK
var neverCustomers = db.Customers.Where(c => c.ServicePlan == null).ToList();
PrintCustomerIds(neverCustomers); // "3"
// OK
var customers = db.Customers.Where(c => c.ServicePlan != null).ToList();
PrintCustomerIds(customers); // "1, 2"
// OK
var pastCustomers = db.Customers.Where(c => c.ServicePlan.TerminationDate != null).ToList();
PrintCustomerIds(pastCustomers); // "2"
// OK
var validData = db.Customers
.Select(c => c.ServicePlan == null ? (bool?)null : c.ServicePlan.TerminationDate != null);
PrintNullableBools(validData); // OUTPUT: "False, True, Null"
// BUG. Should produce the same output as the previous query.
var badData = db.Customers
.Select(c => c.ServicePlan == null ? (bool?)null : c.ServicePlan.TerminationDate.HasValue);
PrintNullableBools(badData); // OUTPUT: "False, True, False". EXPECTED: "False, True, Null"
// Helpers
void PrintCustomerIds(IEnumerable<Customer> customers)
=> Console.WriteLine(string.Join(", ", customers.Select(x => x.Id)));
void PrintNullableBools(IEnumerable<bool?> data)
=> Console.WriteLine(string.Join(", ",
data.Select(b => b == null ? "Null" : b.ToString())));
// Model
public class Customer
{
public int Id { get; set; }
public ServicePlan? ServicePlan { get; set; }
}
public class ServicePlan
{
public int Id { get; set; }
public DateTime? TerminationDate { get; set; }
}
// DbContext
public class TestContext : DbContext
{
public DbSet<Customer> Customers { get; set; }
public DbSet<ServicePlan> ServicePlans { get; set; }
public string DbPath { get; }
public TestContext()
{
var folder = Environment.SpecialFolder.LocalApplicationData;
var path = Environment.GetFolderPath(folder);
DbPath = Path.Join(path, "EfCoreNullableQuerying.db");
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseSqlite($"Data Source={DbPath}");
}
EfCoreNullableQuerying.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.9" />
</ItemGroup>
</Project>
I've added the validData query above, to illustrate the different behaviour when selecting a nullable bool vs a non-nullable bool.
@maumar Looks like an issue with .HasValue and null propagation.
NullCheckRemovingExpressionVisitor converts
DbSet<Customer>()
.Select(c => c.ServicePlan == null ? null : (bool?)c.ServicePlan.TerminationDate.HasValue)
into
DbSet<Customer>()
.Select(c => (bool?)c.ServicePlan.TerminationDate.HasValue)
which is correct in general, but not in case of HasValue. However, even if we fix that, there is another issue in the translation phase - we generate:
SELECT CASE
WHEN [s].[TerminationDate] IS NOT NULL THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
FROM [Customers] AS [c]
LEFT JOIN [ServicePlans] AS [s] ON [c].[ServicePlanId] = [s].[Id]
@maumar the first issue affects all of the providers; the second one is yet another occurrence of #34001