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

EF Core 8延迟加载与立即加载行为不一致问题排查

EF Core立即加载与延迟加载处理Null字段的差异

该问题可通过指定Git仓库复现

实体定义(省略部分代码)

class Folder 
{
    public virtual ICollection<File> Files { get; set; }  
    public virtual ICollection<Log> Logs { get; set; }
}

class File 
{
    public Int32 FolderId { get; set; }
    public virtual Folder Folder { get; set; }
    public virtual ICollection<Log> Logs { get; set; }
}

class Log 
{
    public Int32? FolderId { get; set; }
    public virtual Folder Folder { get; set; }

    public Int32? FileId { get; set; }
    public virtual File File { get; set; }

    public bool IsError { get; set; }
}

实体配置(已启用延迟加载)

public class FolderEFConfiguration : IEntityTypeConfiguration<Folder> {
    public void Configure(EntityTypeBuilder<Folder> b) 
    {
        b.HasMany(x => x.Files)
         .WithOne(y => y.Folder)
         .HasForeignKey(y => y.FolderId).IsRequired();
    }
}

public class LogEFConfiguration : IEntityTypeConfiguration<Log> 
{
    public void Configure(EntityTypeBuilder<Log> b) 
    {
        //...
        b.HasOne(x => x.File)
         .WithMany(fl => fl.Logs)
         .HasForeignKey(x => x.FileId);

        b.HasOne(x => x.Folder)
         .WithMany(fd => fd.Logs)
         .HasForeignKey(x => x.FolderId);
        //...
    }
}

问题表现

以下延迟加载代码在执行fd.Logs.Count()时抛出异常:

using AppContext ctx = new ActapiContext(conf.GetConnectionString("abc"));
var fd = await ctx.Set<Folder>().Where(x => x.InnerRef == "XXXXX").FirstOrDefaultAsync();
Console.WriteLine(fd?.Files.Count());
Console.WriteLine(fd?.Logs.Count());

异常信息:

System.Data.SqlTypes.SqlNullValueException: Data is Null. This method or property cannot be called on Null values

而使用立即加载的代码可正常运行:

using AppContext ctx = new ActapiContext(conf.GetConnectionString("abc"));

var fd = await ctx.Set<Folder>().Include(x => x.Logs).Where(x => x.InnerRef == "XXXXX").FirstOrDefaultAsync();

Console.WriteLine(fd?.Files.Count());
Console.WriteLine(fd?.Logs.Count());

排查发现问题根源:数据库变更脚本添加IsError字段时未设置NOT NULL,导致大量记录的IsError值为NULL:

alter table Logs add IsError bit default 0

修改为以下脚本后,两种加载方式均正常:

alter table Logs add IsError bit not null default 0

核心疑问:为何立即加载在两种脚本配置下都能正常运行?

两种加载方式对应的SQL语句

立即加载

-- EAGER LOADING
SELECT [t].[Id], [t].[Title], [l].[Id], [l].[BlogId], [l].[Date], 
    [l].[IsError], [l].[PostId], [l].[Text]
FROM (
    SELECT TOP(1) [b].[Id], [b].[Title]
    FROM [dbo].[Blogs] AS [b]
    WHERE [b].[Id] = 1
) AS [t]
LEFT JOIN [dbo].[Logs] AS [l] ON [t].[Id] = [l].[BlogId]
ORDER BY [t].[Id]

延迟加载

-- LAZY LOADING
SELECT TOP(1) [b].[Id], [b].[Title]
FROM [dbo].[Blogs] AS [b]
WHERE [b].[Id] = 1

-- 延迟加载Logs时执行的查询
exec sp_executesql N'SELECT [l].[Id], [l].[BlogId], [l].[Date], [l].[IsError], [l].[PostId], [l].[Text]
FROM [dbo].[Logs] AS [l]
WHERE [l].[BlogId] = @__p_0',N'@__p_0 int',@__p_0=1

答案

原因在于EF Core对两种加载模式下的数据库NULL值到非空CLR类型的映射逻辑不同:

  1. 立即加载(Include):
    当使用Include进行关联查询时,EF Core在将查询结果映射到实体对象时,会自动为数据库中NULL的字段填充对应CLR类型的默认值。比如bool类型的默认值是false,所以即使数据库中IsError为NULL,EF Core会自动把它转换成false,不会抛出异常。

  2. 延迟加载:
    延迟加载是通过动态代理实现的,当你第一次访问fd.Logs时,EF Core会单独执行一条查询获取Logs数据。此时EF Core的结果映射逻辑更严格:因为你的Log实体中IsError是不可空的bool类型,但数据库返回了NULL值,CLR的bool无法容纳NULL,所以直接抛出SqlNullValueException。

简单来说,立即加载在映射时做了"容错"处理,自动填充默认值;而延迟加载的查询结果处理没有这个容错步骤,直接严格校验类型匹配,导致NULL值无法转换为非空CLR类型而报错。


内容的提问来源于stack exchange,提问作者tschmit

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 01:45:08