Shaper discards null checks

オープン
#38,762 コメント 1 件 リアクション 0 件 担当者 1 名 GitHub で見る

メンテナーはふだん 2 日以内に返信

@AndriySvyryd がすでに取り組んでいます。

2026年8月7日 から。

評価

この issue はまだ評価されていません。

説明

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)  
主要言語
C#
スター
14.8k
フォーク
3.4k
平均マージ
3日 18時間
マージ済み PR(30日)
159

環境構築

Codespaces で開く

このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

dotnet/efcore のほかの issue

dotnet/efcore の issue をすべて見る

似ている issue

C# の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。