Skip to content

Unexpected semantics if .All() predicate evaluates to NULL for some rows? #34271

@georg-jung

Description

@georg-jung

I'm not sure whether the following sematics of .All() are by design as EF Core's behaviour differs from LINQs behaviour:

E.g. SomeCollection.All(x => x.NullableInt > 2) returns true in EF Core and false in LINQ if any of the NullableInt values is null, if all non-null values satisfy the condition. If all NullableInt values are null, in EF Core's semantics this also means SomeCollection.All(x => x.NullableInt > 2) == true == SomeCollection.All(x => x.NullableInt <= 2).

Code

using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;

const string ConnectionString = @"Data Source=InMemorySample;Mode=Memory;Cache=Shared";

await using var keepOpenCon = new SqliteConnection(ConnectionString);
await keepOpenCon.OpenAsync();

await using var ctx = new ReproContext(ConnectionString);
await ctx.Database.EnsureDeletedAsync();
await ctx.Database.EnsureCreatedAsync();

Console.WriteLine(ctx.Database.GenerateCreateScript());
Console.WriteLine("----");

var lst = new List<Blog> {
    new() { Name = "FooBlog 1", Rating = null, IsNice = null },
    new() { Name = "FooBlog 2", Rating = 2, IsNice = true },
    new() { Name = "FooBlog 3", Rating = null, IsNice = null },
    new() { Name = "FooBlog 4", Rating = 2, IsNice = true },
};
ctx.Blogs.AddRange(lst);
await ctx.SaveChangesAsync();
Console.WriteLine(JsonSerializer.Serialize(lst));
Console.WriteLine("----");

Console.WriteLine("-- Expected NULL Propagation with int? and == --");
var resp = await ctx.Blogs.AllAsync(b => b.Rating == 2);
Console.WriteLine(JsonSerializer.Serialize(resp));

Console.WriteLine();
Console.WriteLine("-- Unexpected NULL Propagation with int? and > --");
var resp1 = await ctx.Blogs.AllAsync(b => b.Rating >= 1); // wouldn't one expect this to be false, no matter which variant of <, <=, >, >= is used with whatever value?
Console.WriteLine(JsonSerializer.Serialize(resp1));

Console.WriteLine();
Console.WriteLine("-- Local LINQ Evaluation --");
Console.WriteLine(JsonSerializer.Serialize(lst.All(b => b.Rating >= 1)));


Console.WriteLine();
Console.WriteLine("-- Manual Workaround NULL Check in Queryable --");
var resp2 = await ctx.Blogs.AllAsync(b => b.Rating.HasValue && b.Rating >= 1);
Console.WriteLine(JsonSerializer.Serialize(resp2));

Console.WriteLine();
Console.WriteLine("-- Unexpected NULL Propagation with bool? --");
var resp3 = await ctx.Blogs.AllAsync(b => b.IsNice!.Value); // okay, I used dammit here, but maybe one would expect this to be the same as below?
Console.WriteLine(JsonSerializer.Serialize(resp3));

Console.WriteLine();
Console.WriteLine("-- Expected NULL Propagation with bool? --");
var resp4 = await ctx.Blogs.AllAsync(b => b.IsNice == true);
Console.WriteLine(JsonSerializer.Serialize(resp4));


public class ReproContext(string ConnectionString) : DbContext
{
    public DbSet<Blog> Blogs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseSqlite(ConnectionString).LogTo(Console.WriteLine, Microsoft.Extensions.Logging.LogLevel.Information, Microsoft.EntityFrameworkCore.Diagnostics.DbContextLoggerOptions.None);
}

public class Blog
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public required int? Rating { get; set; }
    public required bool? IsNice { get; set; }
}
<Project Sdk="Microsoft.NET.Sdk">
	<PropertyGroup>
		<OutputType>Exe</OutputType>
		<TargetFramework>net8.0</TargetFramework>
		<ImplicitUsings>enable</ImplicitUsings>
		<Nullable>enable</Nullable>
	</PropertyGroup>

	<ItemGroup>
		<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
	</ItemGroup>
</Project>

Output

...
-- Expected NULL Propagation with int? and == --
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT NOT EXISTS (
    SELECT 1
    FROM "Blogs" AS "b"
    WHERE "b"."Rating" <> 2 OR "b"."Rating" IS NULL)
false

-- Unexpected NULL Propagation with int? and > --
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT NOT EXISTS (
    SELECT 1
    FROM "Blogs" AS "b"
    WHERE NOT ("b"."Rating" >= 1))
true

-- Local LINQ Evaluation --
false

-- Manual Workaround NULL Check in Queryable --
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT NOT EXISTS (
    SELECT 1
    FROM "Blogs" AS "b"
    WHERE "b"."Rating" IS NULL OR "b"."Rating" < 1)
false

-- Unexpected NULL Propagation with bool? --
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT NOT EXISTS (
    SELECT 1
    FROM "Blogs" AS "b"
    WHERE NOT ("b"."IsNice"))
true

-- Expected NULL Propagation with bool? --
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT NOT EXISTS (
    SELECT 1
    FROM "Blogs" AS "b"
    WHERE "b"."IsNice" = 0 OR "b"."IsNice" IS NULL)
false

After reading Query null semantics, I wasn't sure if the above behaviour is expected or if anything of that is unexpected/a bug?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions