EF Core 2.1升级到.NET8后InMemory查询触发空值异常
问题分析与修复:.NET 8 EF Core InMemory查询空值异常
问题背景
将.NET 5.0(搭配EF 2.1.1)项目升级至.NET 8.0(搭配EF Core 8.0.12)后,原本正常运行的LINQ左连接查询在InMemory数据库环境下抛出System.InvalidOperationException: Nullable object must have a value异常。
实体与DbContext配置
namespace Example { public class Competitor { public string ID { get; set; } } public class Result { public string ID { get; set; } public string WinnerID { get; set; } } public class MyDbContext : DbContext { public MyDbContext() : base(new DbContextOptionsBuilder<MyDbContext>().UseInMemoryDatabase("MyDB").Options) { } public DbSet<Competitor> Competitors { get; set; } public DbSet<Result> Results { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Competitor>() .HasKey(c => c.ID); modelBuilder.Entity<Result>() .HasKey(r => r.ID); } } }
报错查询代码
namespace Example { public class UnitTest1 { [Fact] public void TestOuterJoinToGroup() { using var context = new MyDbContext(); context.Competitors.AddRange(new Competitor() { ID = "1" }, new Competitor() { ID = "2" }); context.Results.AddRange(new Result() { ID = "1", WinnerID = "1" }, new Result() { ID = "2", WinnerID = "1" }); context.SaveChanges(); var individualSummary = context.Results.GroupBy( ic => ic.WinnerID, (WinnerID, ic) => new { CompetitorID = WinnerID, Wins = ic.Count() }); var competitorResults = from c in context.Competitors join r in individualSummary on c.ID equals r.CompetitorID into groupJoin from outerJoin in groupJoin.DefaultIfEmpty() select new { Id = c.ID, Wins = outerJoin == null ? 0 : outerJoin.Wins, }; var arrCompetitorResults = competitorResults.ToList(); } } }
完整异常信息
System.InvalidOperationException: Nullable object must have a value. at lambda_method148(Closure, ValueBuffer) at System.Linq.Utilities.<>c__DisplayClass2_0`3.<CombineSelectors>b__0(TSource x) at System.Linq.Utilities.<>c__DisplayClass2_0`3.<CombineSelectors>b__0(TSource x) at System.Linq.Enumerable.SelectEnumerableIterator`2.MoveNext() at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryShapedQueryCompilingExpressionVisitor.QueryingEnumerable`1.Enumerator.MoveNextHelper() at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryShapedQueryCompilingExpressionVisitor.QueryingEnumerable`1.Enumerator.MoveNext() at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection) at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source) at Example.UnitTest1.TestOuterJoinToGroup() in D:\source\TestProject1\TestProject1\UnitTest1.cs:line 73
异常原因分析
这是EF Core 8.0 InMemory数据库提供程序的查询翻译bug导致的:
- 分组查询
individualSummary返回的匿名类中,Wins是非可空int类型(Count()方法返回int)。 - 左连接(
DefaultIfEmpty())后,无匹配结果的outerJoin会为null,但EF Core InMemory的查询编译逻辑错误地跳过了outerJoin == null ? 0 : outerJoin.Wins的空值判断,直接尝试从空的ValueBuffer中读取非可空的Wins值,最终触发异常。 - 旧版本EF 2.1.1的查询翻译逻辑对该场景处理更宽松,因此未出现此问题。
修复方案
方案1:将分组结果的Wins改为可空类型
修改分组查询,把Wins声明为可空int,让EF Core能正确识别空值场景,后续用GetValueOrDefault替代三元判断:
var individualSummary = context.Results.GroupBy( ic => ic.WinnerID, (WinnerID, ic) => new { CompetitorID = WinnerID, Wins = (int?)ic.Count() // 改为可空int类型 }); var competitorResults = from c in context.Competitors join r in individualSummary on c.ID equals r.CompetitorID into groupJoin from outerJoin in groupJoin.DefaultIfEmpty() select new { Id = c.ID, Wins = outerJoin?.Wins.GetValueOrDefault(0) ?? 0 };
方案2:将分组查询转为内存查询
先把Results数据加载到内存再分组,绕开EF Core InMemory的查询翻译问题:
// 先加载到内存再执行分组 var individualSummary = context.Results.AsEnumerable() .GroupBy( ic => ic.WinnerID, (WinnerID, ic) => new { CompetitorID = WinnerID, Wins = ic.Count() }); var competitorResults = from c in context.Competitors.AsEnumerable() join r in individualSummary on c.ID equals r.CompetitorID into groupJoin from outerJoin in groupJoin.DefaultIfEmpty() select new { Id = c.ID, Wins = outerJoin == null ? 0 : outerJoin.Wins, };
方案3:合并查询逻辑,避免子查询
直接在主查询中完成分组和计数,简化结构让EF Core能正确翻译:
var competitorResults = from c in context.Competitors join r in context.Results on c.ID equals r.WinnerID into resultGroup select new { Id = c.ID, Wins = resultGroup.Count() // 直接对分组计数,无需子查询 };
方案4:使用导航属性(推荐)
修改实体模型添加导航属性,让EF Core自动处理关联关系,避免手动编写左连接:
// 修改Result实体,添加导航属性 public class Result { public string ID { get; set; } public string WinnerID { get; set; } public Competitor Winner { get; set; } } // 在OnModelCreating中配置关联 modelBuilder.Entity<Result>() .HasKey(r => r.ID) .HasOne(r => r.Winner) .WithMany() .HasForeignKey(r => r.WinnerID); // 简化查询 var competitorResults = from c in context.Competitors select new { Id = c.ID, Wins = context.Results.Count(r => r.WinnerID == c.ID) };
内容的提问来源于stack exchange,提问作者colinsl77
相关产品推荐
相关产品推荐

