Shaper discards null checks
メンテナーはふだん 2 日以内に返信
@AndriySvyryd がすでに取り組んでいます。
2026年8月7日 から。
評価
この issue はまだ評価されていません。
説明
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
環境構築
このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
dotnet/efcore のほかの issue
-
customer-reported
難易度 4/5 3〜5日 初心者へのやさしさ 54/100
メンテナーはふだん 2 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 48/100
メンテナーはふだん 2 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 48/100
メンテナーはふだん 2 日以内に返信
-
Regression in 11.0: replacing an owned instance or re-adding an entity with the same key sets unchanged OnAdd properties to null対応中かも @seragAhmed が今日担当しました。 オープン
難易度 4/5 3〜5日 初心者へのやさしさ 48/100
dotnet/efcore#39617 · コメント 1 件 ·
メンテナーはふだん 2 日以内に返信
-
customer-reported
難易度 4/5 3〜5日 初心者へのやさしさ 48/100
メンテナーはふだん 2 日以内に返信
似ている issue
-
bug emitter:client:csharp
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
microsoft/typespec#12572 · コメント 1 件 · リアクション 1 件 ·
メンテナーはふだん 1 日以内に返信
-
needs-area-label
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 2 日以内に返信
-
AWS Lambda: KeyNotFoundException drops HTTP-triggered transactions when allowAllHeaders is disabledオープンbug
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
newrelic/newrelic-dotnet-agent#3949 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
area-networking
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
dotnet/aspnetcore#70545 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 87/100
メンテナーはふだん 1 日以内に返信