Shaper discards null checks

Aperta
#38,762 1 commento 0 reazioni 1 assegnatario Vedi su GitHub

I maintainer di solito rispondono entro 2 giorni

@AndriySvyryd ci sta già lavorando.

Dal 7/8/2026.

Valutazione

Questa issue non è ancora stata valutata.

Descrizione

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)  
Lingua principale
C#
Stelle
14.8k
Fork
3.4k
Merge medio
3g 18h
PR unite (30g)
159

Preparare l'ambiente

Apri in Codespaces

Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di dotnet/efcore

Tutte le issue di dotnet/efcore

Issue simili

Altre issue su C#

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.