You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 16:34:53