Shaper discards null checks

Abierto
#38,762 1 comentario 0 reacciones 1 asignado Ver en GitHub

Los mantenedores suelen responder en 2 días

@AndriySvyryd ya está trabajando en esto.

Desde el 7/8/2026.

Evaluación

Este issue todavía no se ha evaluado.

Descripción

area-query customer-reported
Bug description

When projecting a query through the primary constructor of a (record) class or struct (e.g. for passing out of a method returning an IQueryable for further projection), downstream projections appear to be performed client-side, and downstream projections on the IQueryable that access properties of navigation properties have their null checks discarded, resulting in a NullReferenceException inside the shaper lambda method when projecting the results.

This occurs with both Microsoft.EntityFrameworkCore.Sqlite and with Microting.EntityFrameworkCore.Mysql

Your code
#!/usr/bin/env dotnet

#:package [email protected]
#:package [email protected]
#:property PublishAot=false

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Diagnostics;

if (args.Contains("--wait-for-debugger"))
{
    Console.WriteLine("Waiting for debugger to attach");
    while (!Debugger.IsAttached)
    {
        Thread.Sleep(100);
    }
    Console.WriteLine("Debugger attached");
    Debugger.Break();
}

var builder = Host.CreateApplicationBuilder();
builder.Services.AddDbContextFactory<TestContext>(opts => opts.UseSqlite("Filename=test.sqlite"));

using var host = builder.Build();
var ctxfactory = host.Services.GetRequiredService<IDbContextFactory<TestContext>>();

using (var ctx = ctxfactory.CreateDbContext())
{
    ctx.Database.EnsureDeleted();
    ctx.Database.EnsureCreated();

    for (int i = 1; i <= 100; i++)
    {
        ctx.Add(new TestTable1
        {
            Table1Id = i,
            Field1 = Random.Shared.GetHexString(32),
            Field2 = Random.Shared.GetHexString(32)
        });
    }

    for (int i = 1; i <= 100; i++)
    {
        ctx.Add(new TestTable2
        {
            Table2Id = i,
            Table1Id = Random.Shared.Next(2) == 0 ? null : Random.Shared.Next(1, 100),
            Field1 = Random.Shared.GetHexString(32),
            Field2 = Random.Shared.GetHexString(32)
        });
    }

    for (int i = 1; i <= 100; i++)
    {
        ctx.Add(new TestTable3
        {
            Table3Id = i,
            Table2Id = Random.Shared.Next(2) == 0 ? null : Random.Shared.Next(1, 100),
            Field1 = Random.Shared.GetHexString(32),
            Field2 = Random.Shared.GetHexString(32)
        });
    }

    ctx.SaveChanges();
}

using (var ctx = ctxfactory.CreateDbContext())
{
    IQueryable<TestRecord> query =
        ctx.Set<TestTable3>()
           .LeftJoin(
                ctx.Set<TestTable2>()
                   .Include(e => e.Table1Entry),
                o => o.Table2Id,
                i => i.Table2Id,
                (o, i) => new { Tbl3 = o, Tbl2 = i }
           )
           .Select(e => new TestRecord(e.Tbl3, e.Tbl2));

    var entries =
        query
           .Select(e => new TestOutput
           {
               T3Field1 = e.Table3Entry.Field1,
               T3Field2 = e.Table3Entry.Field2,
               T2Field1 = e.Table2Entry == null ? null : e.Table2Entry.Field1,
               T2Field2 = e.Table2Entry == null ? null : e.Table2Entry.Field2,
               T1Field1 = e.Table2Entry == null || e.Table2Entry.Table1Entry == null ? null : e.Table2Entry.Table1Entry.Field1,
               T1Field2 = e.Table2Entry == null || e.Table2Entry.Table1Entry == null ? null : e.Table2Entry.Table1Entry.Field2,
           })
           .ToList();
}

Debugger.Break();

public class TestContext(DbContextOptions<TestContext> options) : DbContext(options)
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<TestTable1>(m =>
        {
            m.HasKey(e => e.Table1Id);
            m.ToTable("TestTable1");
        });

        modelBuilder.Entity<TestTable2>(m =>
        {
            m.HasKey(e => e.Table2Id);
            m.ToTable("TestTable2");
            m.HasOne(e => e.Table1Entry).WithMany().HasForeignKey(e => e.Table1Id).HasPrincipalKey(e => e.Table1Id);
        });

        modelBuilder.Entity<TestTable3>(m =>
        {
            m.HasKey(e => e.Table3Id);
            m.ToTable("TestTable3");
        });
    }
}

public class TestOutput
{
    public string? T3Field1 { get; init; }
    public string? T3Field2 { get; init; }
    public string? T2Field1 { get; init; }
    public string? T2Field2 { get; init; }
    public string? T1Field1 { get; init; }
    public string? T1Field2 { get; init; }
}

public record struct TestRecord(TestTable3 Table3Entry, TestTable2? Table2Entry);

public class TestTable1
{
    public int Table1Id { get; set; }
    public string? Field1 { get; set; }
    public string? Field2 { get; set; }
}

public class TestTable2
{
    public int Table2Id { get; set; }
    public int? Table1Id { get; set; }
    public string? Field1 { get; set; }
    public string? Field2 { get; set; }
    public TestTable1? Table1Entry { get; set; }
}

public class TestTable3
{
    public int Table3Id { get; set; }
    public int? Table2Id { get; set; }
    public string? Field1 { get; set; }
    public string? Field2 { get; set; }
}
Stack traces
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at lambda_method156(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.<Main>$(String[] args) in C:\Users\klightspeed\Documents\TestEFCore\app.cs:line 83
Verbose output
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT "t"."Table3Id", "t"."Field1", "t"."Field2", "t"."Table2Id", "t0"."Table2Id", "t0"."Field1", "t0"."Field2", "t0"."Table1Id", "t1"."Table1Id", "t1"."Field1", "t1"."Field2"
      FROM "TestTable3" AS "t"
      LEFT JOIN "TestTable2" AS "t0" ON "t"."Table2Id" = "t0"."Table2Id"
      LEFT JOIN "TestTable1" AS "t1" ON "t0"."Table1Id" = "t1"."Table1Id"
fail: Microsoft.EntityFrameworkCore.Query[10100]
      An exception occurred while iterating over the results of a query for context type 'TestContext'.
      System.NullReferenceException: Object reference not set to an instance of an object.
         at lambda_method156(Closure, QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator)
         at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.MoveNext()
      System.NullReferenceException: Object reference not set to an instance of an object.
         at lambda_method156(Closure, QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator)
         at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.MoveNext()
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at lambda_method156(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.<Main>$(String[] args) in C:\Users\klightspeed\Documents\TestEFCore\app.cs:line 83
Segmentation fault
EF Core version

10.0.10

Database provider

Microsoft.EntityFrameworkCore.Sqlite

Target framework

.NET 10

Operating system

Windows 11

IDE

Visual Studio 2026 18.8.2

More Info

shaperExpression passed to Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ProcessShaper:

.New TestOutput(){
    T3Field1 = ((.New TestRecord(
        .Extension<Microsoft.EntityFrameworkCore.Query.RelationalStructuralTypeShaperExpression>,
        .Extension<Microsoft.EntityFrameworkCore.Query.IncludeExpression>)).Table3Entry).Field1,
    T3Field2 = ((.New TestRecord(
        .Extension<Microsoft.EntityFrameworkCore.Query.RelationalStructuralTypeShaperExpression>,
        .Extension<Microsoft.EntityFrameworkCore.Query.IncludeExpression>)).Table3Entry).Field2,
    T2Field1 = ((.New TestRecord(
        .Extension<Microsoft.EntityFrameworkCore.Query.RelationalStructuralTypeShaperExpression>,
        .Extension<Microsoft.EntityFrameworkCore.Query.IncludeExpression>)).Table2Entry).Field1,
    T2Field2 = ((.New TestRecord(
        .Extension<Microsoft.EntityFrameworkCore.Query.RelationalStructuralTypeShaperExpression>,
        .Extension<Microsoft.EntityFrameworkCore.Query.IncludeExpression>)).Table2Entry).Field2,
    T1Field1 = .If (
        (.New TestRecord(
            .Extension<Microsoft.EntityFrameworkCore.Query.RelationalStructuralTypeShaperExpression>,
            .Extension<Microsoft.EntityFrameworkCore.Query.IncludeExpression>)).Table2Entry == null || ((.New TestRecord(
            .Extension<Microsoft.EntityFrameworkCore.Query.RelationalStructuralTypeShaperExpression>,
            .Extension<Microsoft.EntityFrameworkCore.Query.IncludeExpression>)).Table2Entry).Table1Entry == null
    ) {
        null
    } .Else {
        (((.New TestRecord(
            .Extension<Microsoft.EntityFrameworkCore.Query.RelationalStructuralTypeShaperExpression>,
            .Extension<Microsoft.EntityFrameworkCore.Query.IncludeExpression>)).Table2Entry).Table1Entry).Field1
    },
    T1Field2 = .If (
        (.New TestRecord(
            .Extension<Microsoft.EntityFrameworkCore.Query.RelationalStructuralTypeShaperExpression>,
            .Extension<Microsoft.EntityFrameworkCore.Query.IncludeExpression>)).Table2Entry == null || ((.New TestRecord(
            .Extension<Microsoft.EntityFrameworkCore.Query.RelationalStructuralTypeShaperExpression>,
            .Extension<Microsoft.EntityFrameworkCore.Query.IncludeExpression>)).Table2Entry).Table1Entry == null
    ) {
        null
    } .Else {
        (((.New TestRecord(
            .Extension<Microsoft.EntityFrameworkCore.Query.RelationalStructuralTypeShaperExpression>,
            .Extension<Microsoft.EntityFrameworkCore.Query.IncludeExpression>)).Table2Entry).Table1Entry).Field2
    }
}

I found this while debugging an issue with a service using dotnet and EF Core. Certain queries were giving a NullReferenceException.
In this case, the query was of the form:

    public virtual IQueryable<BodyMatchLineEntry> QueryBodyMatchLines(
            long bodyId,
            DateTimeOffset? minDate,
            DateTimeOffset? maxDate,
            int? maxResults
        )
    {
        var query = Set<FileLineBody>()
            .Where(e => e.BodyId == bodyId)
            .LeftJoin(
                Set<FileInfo>(),
                o => o.FileId,
                i => i.Id,
                (o, i) => new { Body = o, File = i }
            )
            .LeftJoin(
                Set<FileLineInfo>()
                    .Include(e => e.Software)
                    .Include(e => e.SchemaEvent)
                    .Include(e => e.GameVersion),
                o => new { o.Body.FileId, o.Body.LineNo },
                i => new { i.FileId, i.LineNo },
                (o, i) => new { o.File, Info = i, o.Body }
            )
            .LeftJoin(
                Set<FileLineStation>()
                    .Include(e => e.Station),
                o => new { o.Body.FileId, o.Body.LineNo },
                i => new { i.FileId, i.LineNo },
                (o, i) => new { o.File, o.Info, o.Body, Station = i }
            );

        if (minDate?.ToUniversalTime().DateTime is DateTime minTS)
        {
            query = query.Where(e => e.Body.GatewayTimestamp >= minTS);
        }

        if (maxDate?.ToUniversalTime().DateTime is DateTime maxTS)
        {
            query = query.Where(e => e.Body.GatewayTimestamp <= maxTS);
        }

        return
            query
                .OrderByDescending(e => e.Body.GatewayTimestamp)
                .Take(maxResults ?? 1000)
                .Where(e => e.File != null && e.Info != null)
                .Select(e => new BodyMatchLineEntry(
                    e.File!,
                    e.Body,
                    e.Info!,
                    e.Station
                ));
    }

and the projection was of the form:

    private protected async Task<Dictionary<long, List<MatchEntry>>> GetBodyMatchEntriesAsync(
            ICollection<long> bodyIds,
            int? limitMatches,
            DateTimeOffset? minDate,
            DateTimeOffset? maxDate,
            CancellationToken canceltoken
        )
    {
        await using var ctx = await ContextFactory.CreateDbContextAsync(canceltoken);

        var matches = new Dictionary<long, List<MatchEntry>>();

        foreach (var bodyid in bodyIds)
        {
            matches[bodyid] = //await
                ctx.QueryBodyMatchLines(bodyid, minDate, maxDate, limitMatches)
                   .Select(e => new MatchEntry
                   {
                       FileName = e.File.FileName,
                       LineNo = e.Body.LineNo,
                       SoftwareName = e.Info.Software == null ? null : e.Info.Software.SoftwareName,
                       SoftwareVersion = e.Info.Software == null ? null : e.Info.Software.SoftwareVersion,
                       Schema = e.Info.SchemaEvent == null ? null : e.Info.SchemaEvent.Schema,
                       EventType = e.Info.SchemaEvent == null ? null : e.Info.SchemaEvent.EventType,
                       GameVersion = e.Info.GameVersion == null ? null : e.Info.GameVersion.GameVersion,
                       GameBuild = e.Info.GameVersion == null ? null : e.Info.GameVersion.GameBuild,
                       IsOdyssey = e.Info.GameVersion == null ? null : e.Info.GameVersion.IsOdyssey,
                       IsHorizons = e.Info.GameVersion == null ? null : e.Info.GameVersion.IsHorizons,
                       Timestamp = e.Info.Timestamp,
                       GatewayTimestamp = e.Body.GatewayTimestamp,
                       SystemId = e.Info.SystemId,
                       BodyId = e.Body.BodyId,
                       StationId = e.Station == null ? null : e.Station.StationId,
                       StationName = e.Station == null || e.Station.Station == null ? null : e.Station.Station.StationName,
                       MarketId = e.Station == null || e.Station.Station == null ? null : e.Station.Station.MarketId
                   })
                   .ToList();
                   //.ToListAsync(canceltoken);
        }

        return matches;
    }

The stack trace in this case was:

   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 EddnIndexLookup.Services.EddnLookupService.<GetBodyMatchEntriesAsync>d__24.MoveNext() in C:\Users\klightspeed\source\repos\EddnIndexUpdate\EddnIndexLookup\Services\EddnLookupService.cs:line 747
   at EddnIndexLookup.Services.EddnLookupService.<GetBodyMatchEntriesAsync>d__24.MoveNext() in C:\Users\klightspeed\source\repos\EddnIndexUpdate\EddnIndexLookup\Services\EddnLookupService.cs:line 773

and the disassembly around the _ipForWatsonBuckets was:

00007FFB849168C2  call        EddnIndex.Common.Models.BodyMatchLineEntry..ctor(EddnIndex.Common.Models.FileInfo, EddnIndex.Common.Models.FileLineBody, EddnIndex.Common.Models.FileLineInfo, EddnIndex.Common.Models.FileLineStation) (07FFB848E1008h)  
00007FFB849168C7  mov         rcx,rdi  
00007FFB849168CA  call        EddnIndex.Common.Models.BodyMatchLineEntry.get_Station() (07FFB848E1350h)  
00007FFB849168CF  mov         rcx,rax  
00007FFB849168D2  cmp         dword ptr [rcx],ecx  <--- NullReferenceException
00007FFB849168D4  call        EddnIndex.Common.Models.FileLineStation.get_StationId() (07FFB848E1368h)  
00007FFB849168D9  mov         edx,eax  
00007FFB849168DB  shl         rdx,20h  
00007FFB849168DF  or          rdx,1  
00007FFB849168E3  mov         rcx,rbx  
00007FFB849168E6  call        EddnIndexLookup.DTO.MatchEntry.set_StationId(System.Nullable`1<Int32>) (07FFB848E1380h)  
Lenguaje dominante
C#
Estrellas
14.8k
Forks
3.4k
Merge medio
3 d 18 h
PR fusionados (30 d)
159

Preparar el entorno

Abrir en Codespaces

Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de dotnet/efcore

Todos los issues de dotnet/efcore

Issues similares

Más issues de C#

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.