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

Blazor Server中EF Core异步查询报错:读取器关闭时调用CheckDataIsReady无效

Blazor Server中EF Core异步查询异常:Invalid attempt to call CheckDataIsReady when reader is closed

异常信息

fail: Microsoft.EntityFrameworkCore.Query[10100]
      An exception occurred while iterating over the results of a query for context type 'DBR.Web.Context.DBRContext'.
      System.InvalidOperationException: Invalid attempt to call CheckDataIsReady when reader is closed.
         at Microsoft.Data.SqlClient.SqlDataReader.CheckDataIsReady(Int32 columnIndex, Boolean allowPartiallyReadColumn, Boolean permitAsync, String methodName)
         at Microsoft.Data.SqlClient.SqlDataReader.TryReadColumn(Int32 i, Boolean setTimeout, Boolean allowPartiallyReadColumn)
         at Microsoft.Data.SqlClient.SqlDataReader.ReadColumn(Int32 i, Boolean setTimeout, Boolean allowPartiallyReadColumn)
         at Microsoft.Data.SqlClient.SqlDataReader.GetGuid(Int32 i)
         at lambda_method340(Closure , QueryContext , DbDataReader , ResultContext , SplitQueryResultCoordinator )
         at Microsoft.EntityFrameworkCore.Query.Internal.SplitQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
      System.InvalidOperationException: Invalid attempt to call CheckDataIsReady when reader is closed.
         at Microsoft.Data.SqlClient.SqlDataReader.CheckDataIsReady(Int32 columnIndex, Boolean allowPartiallyReadColumn, Boolean permitAsync, String methodName)
         at Microsoft.Data.SqlClient.SqlDataReader.TryReadColumn(Int32 i, Boolean setTimeout, Boolean allowPartiallyReadColumn)
         at Microsoft.Data.SqlClient.SqlDataReader.ReadColumn(Int32 i, Boolean setTimeout, Boolean allowPartiallyReadColumn)
         at Microsoft.Data.SqlClient.SqlDataReader.GetGuid(Int32 i)
         at lambda_method340(Closure , QueryContext , DbDataReader , ResultContext , SplitQueryResultCoordinator )
         at Microsoft.EntityFrameworkCore.Query.Internal.SplitQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()

问题描述

在ASP.NET Core(Blazor Server)应用中调用GetByIdAsync方法获取对象时触发上述异常。将EF仓库中的.ToListAsync()改为.ToList()后程序可正常运行,但需要保留异步实现,求异常原因及解决方案。


相关代码实现

UI代码

protected override async Task OnInitializedAsync()
{
    if (CreateOrEdit is not "rediger" && CreateOrEdit is not "opret")
    {
        errorMessage = "Der er ingenting at se her. :(",
        isInvalidPath = true;

        return;
    }

    if (!string.IsNullOrWhiteSpace(WorkshopId))
    {
        Func<IQueryable<Workshop>, IIncludableQueryable<Workshop, object>> workshopIncludes = workshop => workshop.Include(x => x.Address!);

        ResponseDTO<WorkshopDTO> loadedWorkshop = await WorkshopService.GetByIdAsync(Guid.Parse(WorkshopId), isTrackingDisabled: false, includeProperties: workshopIncludes);

        if (loadedWorkshop.Success)
        {
            if (CreateOrEdit is "rediger")
            {
                workshopToEdit = loadedWorkshop.Content!;
            }
            else
            {
                workshopInputModel.Name = loadedWorkshop.Content!.Name;
                workshopInputModel.PhoneNumber = loadedWorkshop.Content!.PhoneNumber;
                workshopInputModel.AddressInputModel!.Street = loadedWorkshop.Content!.Address!.Street;
                workshopInputModel.AddressInputModel!.City = loadedWorkshop.Content!.Address.City;
                workshopInputModel.AddressInputModel!.PostCode = loadedWorkshop.Content!.Address.PostCode;
            }
        }
    }
}

GetByIdAsync服务代码

public async Task<ResponseDTO<TOUT>> GetByIdAsync(Guid entityId, bool isTrackingDisabled = true, Func<IQueryable<T>, IIncludableQueryable<T, object>>? includeProperties = null, Expression<Func<T, T>>? selector = null, CancellationToken cancellationToken = default)
{
    ResponseDTO<TOUT> responseDTO = await repository.GetByIdAsync(entityId, isTrackingDisabled, includeProperties, selector, cancellationToken);

    return responseDTO;
}

GetByIdAsync EF仓库代码(原问题代码)

public async Task<ResponseDTO<TOUT>> GetByIdAsync(Guid id, bool isTrackingDisabled = true, Func<IQueryable<T>, IIncludableQueryable<T, object>>? includeProperties = null, Expression<Func<T, T>>? selector = null, CancellationToken cancellationToken = default)
{
    try
    {
        T requestedEntity = (await table.QueryHelper(selector: selector, includeProperties: includeProperties, isTrackingDisabled: isTrackingDisabled).ToListAsync(cancellationToken)).Find(entity => (Guid)entity.GetType().GetProperty("Id")!.GetValue(entity)! == id) ?? throw new ArgumentNullException(null);

        TOUT requestedEntityDTO = mapper.Map<TOUT>(requestedEntity);

        return new ResponseDTO<TOUT> { Success = true, Content = requestedEntityDTO };
    }
    catch (ArgumentNullException ex)
    {
        return new ResponseDTO<TOUT> { Success = false, ErrorMessage = $"Din anmodning blev ikke fundet - prøv igen.", ExceptionMessage = ex.Message, InnerExceptionMessage = ex.InnerException?.Message, ErrorType = ErrorType.EntityNotFound };
    }
    catch (OperationCanceledException ex)
    {
        return new ResponseDTO<TOUT> { Success = false, ErrorMessage = "Din anmodning blev annulleret.", ExceptionMessage = ex.Message, InnerExceptionMessage = ex.InnerException?.Message, ErrorType = ErrorType.CancellationTokenRequested };
    }
    catch (Exception ex)
    {
        return new ResponseDTO<TOUT> { Success = false, ErrorMessage = "Ubehandlet serverfejl. Kontakt server administrator.", ExceptionMessage = ex.Message, InnerExceptionMessage = ex.InnerException?.Message, ErrorType = ErrorType.Unhandled };
    }
}

QueryHelper扩展方法

public static IQueryable<T> QueryHelper<T>(this DbSet<T> table, bool isTrackingDisabled = true, Func<IQueryable<T>, IIncludableQueryable<T, object>>? includeProperties = null, Expression<Func<T, bool>>? filter = null, Expression<Func<T, T>>? selector = null, Func<IQueryable<T>, IOrderedQueryable<T>>? orderBy = null) where T : class
{
    IQueryable<T> query = table.AsSplitQuery();

    if (isTrackingDisabled)
    {
        query = query.AsNoTracking();
    }

    if (filter is not null)
    {
        query = query.Where(filter);
    }

    if (includeProperties is not null)
    {
        query = includeProperties(query);
    }

    if (selector is not null)
    {
        query = query.Select(selector);
    }

    if (orderBy is not null)
    {
        query = orderBy(query);
    }

    return query;
}

Workshop领域模型

public class Workshop : BaseEntity
{
    [StringLength(50)]
    public string Name { get; set; } = null!;

    public Guid AddressId { get; set; }

    [ForeignKey(nameof(AddressId))]
    public Address? Address { get; set; }

    [StringLength(15)]
    public string PhoneNumber { get; set; } = null!;

    public ICollection<Member>? Members { get; set; }

    public ICollection<Case>? Cases { get; set; }

    public ICollection<Specialization>? Specializations { get; set; }
}

Address领域模型

public class Address : BaseEntity
{
    [StringLength(50)]
    public string Street { get; set; } = null!;

    [StringLength(25)]
    public string City { get; set; } = null!;

    [Required]
    public short? PostCode { get; set; }
}

Program.cs中的DBRContext配置

builder.Services.AddDbContext<DBRContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DBRContext") ?? throw new InvalidOperationException("Connectionstring 'DBRContext' was not found."), options => options.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)));

异常原因

  1. 全表加载+内存筛选的错误逻辑
    仓库的GetByIdAsync先通过ToListAsync()加载整张表到内存,再用Find()在内存中筛选ID。这种做法不仅性能低下(全表扫描),更关键的是结合Split Query时,异步场景下会导致SQLDataReader提前关闭。
  2. Split Query异步读取的资源冲突
    全局配置了QuerySplittingBehavior.SplitQuery,且QueryHelper默认启用AsSplitQuery()。Split Query会为主实体和关联实体分别执行SQL查询,异步读取时,若上下文在读取完成前被释放(或异步操作的资源管理时序问题),会触发“reader is closed”异常。同步ToList()是阻塞读取,资源能被正确释放,所以无报错。

解决方案

核心修复:数据库层面过滤ID

将“全表加载+内存筛选”改为直接在数据库层面用Where过滤ID,再用FirstOrDefaultAsync()获取单条数据,从根源解决性能和异步资源问题。

修改后的EF仓库GetByIdAsync代码:

public async Task<ResponseDTO<TOUT>> GetByIdAsync(Guid id, bool isTrackingDisabled = true, Func<IQueryable<T>, IIncludableQueryable<T, object>>? includeProperties = null, Expression<Func<T, T>>? selector = null, CancellationToken cancellationToken = default)
{
    try
    {
        // 直接在数据库层面过滤ID,避免全表加载
        var query = table.QueryHelper(selector: selector, includeProperties: includeProperties, isTrackingDisabled: isTrackingDisabled)
                         .Where(entity => EF.Property<Guid>(entity, "Id") == id);

        T requestedEntity = await query.FirstOrDefaultAsync(cancellationToken) ?? throw new ArgumentNullException(null);

        TOUT requestedEntityDTO = mapper.Map<TOUT>(requestedEntity);

        return new ResponseDTO<TOUT> { Success = true, Content = requestedEntityDTO };
    }
    catch (ArgumentNullException ex)
    {
        return new ResponseDTO<TOUT> { Success = false, ErrorMessage = $"Din anmodning blev ikke fundet - prøv igen.", ExceptionMessage = ex.Message, InnerExceptionMessage = ex.InnerException?.Message, ErrorType = ErrorType.EntityNotFound };
    }
    catch (OperationCanceledException ex)
    {
        return new ResponseDTO<TOUT> { Success = false, ErrorMessage = "Din anmodning blev annulleret.", ExceptionMessage = ex.Message, InnerExceptionMessage = ex.InnerException?.Message, ErrorType = ErrorType.CancellationTokenRequested };
    }
    catch (Exception ex)
    {
        return new ResponseDTO<TOUT> { Success = false, ErrorMessage = "Ubehandlet serverfejl. Kontakt server administrator.", ExceptionMessage = ex.Message, InnerExceptionMessage = ex.InnerException?.Message, ErrorType = ErrorType.Unhandled };
    }
}

其他优化建议

  • 增强QueryHelper通用性:可以给QueryHelper添加Guid? id参数,直接内置ID过滤逻辑,避免重复代码。
  • 验证上下文生命周期:确保DBRContext在Blazor Server中配置为Scoped(默认配置已满足),避免上下文提前回收。

内容的提问来源于stack exchange,提问作者DM-98

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 05:50:28