Inconsistent exceptions while aggregating empty collections
- Dominant language
- C#
- Stars
- 14.8k
- Forks
- 3.4k
- PR merge metrics
- PR metrics pending
Description
The results of aggregation functions (such as `Max`, `Min`, `Avg`) compose in possibly surprising ways with other computations.
An example program that showcases the bug is:
```C#
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using var db = new BloggingContext();
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
db.Blogs
.Select(x => x.Posts.Select(y => y.Id).Max())
.Select(z => z > 123 ? "T" : "F")
.ToList();
db.Blogs
.Select(x => x.Posts.Select(y => y.Id).Max())
.ToList();
public class BloggingContext : DbContext
{
public DbSet Blogs { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options
.LogTo(Console.WriteLine, Microsoft.Extensions.Logging.LogLevel.Information)
.UseSqlite($"Data Source=test.db");
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity().HasData(new Blog { Id = 1 });
}
}
public class Blog
{
public int Id { get; set; }
public Post[] Posts { get; set; } = [];
}
public class Post
{
public int Id { get; set; }
}
```
### Exception
The program terminates with the following exception:
```
fail: 05/24/2024 03:32:33.489 CoreEventId.QueryIterationFailed[10100] (Microsoft.EntityFrameworkCore.Query)
An exception occurred while iterating over the results of a query for context type 'BloggingContext'.
System.InvalidOperationException: Nullable object must have a value.
at lambda_method32(Closure, QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.MoveNext()
Unhandled exception. System.InvalidOperationException: Nullable object must have a value.
at lambda_method32(Closure, QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.MoveNext()
at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
at Program.$(String[] args)
```
because it is running the query
```sql
SELECT (
SELECT MAX("p"."Id")
FROM "Post" AS "p"
WHERE "b"."Id" = "p"."BlogId")
FROM "Blogs" AS "b"
```
In C# `.Max()` returns `int` (and throws an `InvalidOperationException`, just like the iteration did 😅 ) on empty collections, but in SQL `MAX()` returns a `NULL`. In a way, since the exception thrown by EFCore matches the one from the `.Max()` definition, this is a happy result. Unfortunately this is inconsistent with the query which also projects the results through `.Select(z => z > 123 ? "T" : "F")`, which is compiled to
```sql
SELECT CASE
WHEN (
SELECT MAX("p"."Id")
FROM "Post" AS "p"
WHERE "b"."Id" = "p"."BlogId") > 123 THEN 'T'
ELSE 'F'
END
FROM "Blogs" AS "b"
```
in which no exception is thrown and `.Max()` is treated as if it returned `int?`.
I think the mismatch between C# and SQL behavior is known and possibly intended/expected, but the weirdness around further computations on the result of the aggregation is unfortunate.
### Include provider and version information
EF Core version: 8.0.5
Database provider: Microsoft.EntityFrameworkCore.Sqlite
Target framework: .NET 8.0
Operating system: Linux (/WSL)
IDE: Visual Studio Code 1.89.1
Contributor guide
Assessment
This issue has not been assessed yet.