Efcore: Query: LastOrDefault/LastOrDefaultAsync doesn't return default value when projecting for InMemory provider

Created on 25 Apr 2018  路  3Comments  路  Source: dotnet/efcore

When the query returns 0 rows calling LastOrDefault() throws a null reference exception instead of returning the default value when you project a property.

System.NullReferenceException
  HResult=0x80004003
  Message=Object reference not set to an instance of an object.
  Source=Microsoft.EntityFrameworkCore
  StackTrace:
   at Microsoft.EntityFrameworkCore.Metadata.Internal.EntityMaterializerSource.TryReadValue[TValue](ValueBuffer valueBuffer, Int32 index, IPropertyBase property)
   at System.Linq.Enumerable.SelectArrayIterator`2.MoveNext()
   at Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider.ExceptionInterceptor`1.EnumeratorExceptionInterceptor.MoveNext()
   at System.Linq.Enumerable.TryGetFirst[TSource](IEnumerable`1 source, Boolean& found)
   at System.Linq.Enumerable.First[TSource](IEnumerable`1 source)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass17_1`1.<CompileQueryCore>b__0(QueryContext qc)
   at EFCore2.Poc.Program.Main(String[] args) in c:\users\jose\source\repos\EFCore2.Poc\EFCore2.Poc\Program.cs:line 20

Steps to reproduce

```c#
class Program
{
static void Main(string[] args)
{
using (var ctx = new MyContext())
{
var s1 = new SomeEntity { Order = 1, Name = "Juan" };
var s2 = new SomeEntity { Order = 2, Name = "Pedro" };
var s3 = new SomeEntity { Order = 3, Name = "Fasola" };

            ctx.SomeEntities.AddRange(s1, s2, s3);
            ctx.SaveChanges();

            var last = ctx.SomeEntities
                .Where(o => o.Order > 3)
                .OrderBy(o => o.Order)
                .Select(o => o.Id)
                .LastOrDefault();
        }
    }
}

public class MyContext : DbContext
{
    public DbSet<SomeEntity> SomeEntities { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseInMemoryDatabase(Guid.NewGuid().ToString());
    }
}

public class SomeEntity
{
    public int Id { get; set; }
    public int Order { get; set; }
    public string Name { get; set; }
}

```

Further technical details

EF Core version: 2.0.2
Database Provider: Microsoft.EntityFrameworkCore.InMemory
Operating system: Windows 10 Pro x64
IDE: Visual Studio 2017 15.6.6

area-in-memory area-query closed-fixed punted-for-3.0 type-bug

Most helpful comment

Works on 3.1.0

All 3 comments

Verified this is still a problem for InMemory in the current bits.

For the query:

ctx.Orders.Where(c => c.CustomerID == "foo").OrderBy(c => c.CustomerID).Select(c => c.OrderID).LastOrDefault()

We generate the following query plan:

(QueryContext queryContext) => IEnumerable<int> _InterceptExceptions(
    source: IEnumerable<int> _Select(
        source: IEnumerable<ValueBuffer> _ToSequence(() => ValueBuffer LastOrDefault(IOrderedEnumerable<ValueBuffer> _OrderBy(
                    source: IEnumerable<ValueBuffer> _Where(
                        source: IEnumerable<ValueBuffer> ProjectionQuery(
                            queryContext: queryContext, 
                            entityType: EntityType: Order), 
                        predicate: (ValueBuffer c) => string TryReadValue(c, 1, Order.CustomerID) == "foo"), 
                    expression: (ValueBuffer c) => string TryReadValue(c, 1, Order.CustomerID), 
                    orderingDirection: Asc))), 
        selector: (ValueBuffer c) => int TryReadValue(c, 0, Order.OrderID)), 
    contextType: TestModels.Northwind.NorthwindContext, 
    logger: DiagnosticsLogger<Query>, 
    queryContext: queryContext)

FirstOrDefault works fine, with the following query plan:

(QueryContext queryContext) => IEnumerable<int> _InterceptExceptions(
    source: IEnumerable<int> _ToSequence(() => int FirstOrDefault(IEnumerable<int> _Select(
                source: IOrderedEnumerable<ValueBuffer> _OrderBy(
                    source: IEnumerable<ValueBuffer> _Where(
                        source: IEnumerable<ValueBuffer> ProjectionQuery(
                            queryContext: queryContext, 
                            entityType: EntityType: Order), 
                        predicate: (ValueBuffer c) => string TryReadValue(c, 1, Order.CustomerID) == "foo"), 
                    expression: (ValueBuffer c) => string TryReadValue(c, 1, Order.CustomerID), 
                    orderingDirection: Asc), 
                selector: (ValueBuffer c) => int TryReadValue(c, 0, Order.OrderID)))), 
    contextType: TestModels.Northwind.NorthwindContext, 
    logger: DiagnosticsLogger<Query>, 
    queryContext: queryContext)

For LastOrDefault we pushed down the operator below the select. Which caused the type change for LastOrDefault. Instead of select over int enumerable, we are selecting over ValueBuffer now. So the selector needs to account for empty valuebuffer.

Works on 3.1.0

Was this page helpful?
0 / 5 - 0 ratings