System.InvalidOperationException: Nullable object must have a value. — an EF Core Max() over an int column ran on a filter that matched no rows, and the empty result could not become an int. Cast the selector to int? (Max(c => (int?)c.Id)) and check the result for null.

The message does not mention Max, the table or the filter, so it is easy to look in the wrong place. The trace in the reproduction ended in Queryable.Max, which is the clue.

The error

PS> dotnet run -- max-bad
...
Unhandled exception. System.InvalidOperationException: Nullable object must have a value.
   at lambda_method42(Closure, QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator)
   at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.MoveNext()
...
   at System.Linq.Queryable.Max[TSource,TResult](IQueryable`1 source, Expression`1 selector)
   at Program.<Main>$(String[] args) in ...\EfLab\Program.cs:line 100

The cuts hide NuGet audit warnings printed by dotnet run and EF Core frames. The top frame takes a DbDataReader: the exception was raised while EF Core read the query result.

Why it happens

int maxId = db.Customers.Where(c => c.City == "Oslo").Max(c => c.Id);

The table held two customers, both in Lima, so no row matched City == "Oslo". With no rows there is no maximum, and the code asked for a plain int, which has no way to say "nothing". The trace shows the exception raised inside the enumerator that reads the query's result, called from Queryable.Max at line 100, the statement that assigns the result of Max() to int maxId.

The fix

int? maxId = db.Customers.Where(c => c.City == "Oslo").Max(c => (int?)c.Id);
Console.WriteLine(maxId is null ? "No customers in Oslo" : maxId);
PS> dotnet run -- max-fix
...
No customers in Oslo

With the selector cast to int?, Max() returned null for the empty filter and the program printed the message instead of crashing. The cast sits inside the lambda, on c.Id, which turns the whole Max() call into one that returns int?; the variable type changed to match.

How it was reproduced

A console project from dotnet new console -n EfLab on .NET SDK 10.0.403 with Microsoft.EntityFrameworkCore.Sqlite 10.0.14 and a SQLite file shop.db, seeded with two fictional customers in Lima. dotnet run -- max-bad took the maximum Id of customers in Oslo into an int and threw; dotnet run -- max-fix ran the int? version. Both runs started by deleting and recreating shop.db and seeding the same two rows, so the Oslo filter was empty both times and only the selector differed.

Frequently asked

Why does EF Core Max throw Nullable object must have a value?
The filter matched no rows, so there was no maximum, and the result was read into a non-nullable int. In the reproduction Max(c => c.Id) over customers in Oslo threw it.
How do I get Max on an empty table without an exception in EF Core?
Cast the selector to a nullable type, for example Max(c => (int?)c.Id), and check the result for null. In the reproduction that returned null and printed a message instead of throwing.
Where does Nullable object must have a value come from in the stack trace?
In the reproduction the top frame was a compiled EF Core lambda that takes a DbDataReader, and the trace ended in System.Linq.Queryable.Max called from Program.cs.

More decoded errors in the Fixes category. A related empty-result error from First() is covered in Sequence contains no elements.