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

Linq-to-SQL迁移EF Core时DataReader已打开异常解决咨询

EF Core 流式处理多查询时的DataReader冲突问题

更新说明:此代码用于服务器端数据导出流程,无需分页/分块操作,仅需流式处理多达数十万条数据。

我正在将旧版Linq-to-SQL代码/查询迁移至.NET 7 / EF Core,部分在L2S中正常运行的代码现在抛出如下异常:

已有一个与该连接关联的DataReader处于打开状态,必须先将其关闭。

我查阅资料后看到启用MARS的建议(但需谨慎考虑),也尝试过若干.Include语句,但均无效果。

数据库表结构

存在Profile(父表)-> HistoryData(子表)的关联关系,其中HistoryData.hispKey是Profile.pKey的外键,表结构如下:

CREATE TABLE [dbo].[Group](
    [gKey] [int] IDENTITY(1,1) NOT NULL,
    [gName] [nvarchar](255) NOT NULL,
    ....
)
CREATE TABLE [dbo].[Profile](
    [pKey] [int] IDENTITY(1,1) NOT NULL,
    [pgKey] [int] NOT NULL,
    [pAuthID] [nvarchar](255) NOT NULL,
    [pDateUpdated] [datetime2](7) NOT NULL,
    [pDateCreated] [datetime2](7) NOT NULL,
    [pUpdatedBy] [varchar](255) NOT NULL,
    [pCreatedBy] [varchar](255) NOT NULL
    [pProfile] [ntext] NOT NULL,
    [pProfileXml] [xml] NOT NULL
)
CREATE TABLE [dbo].[HistoryData](
    [hisKey] [int] IDENTITY(1,1) NOT NULL,
    [hisgKey] [int] NOT NULL,
    [hispKey] [int] NOT NULL,
    [hisType] [varchar](50) NOT NULL,
    [hisIndex] [varchar](255) NOT NULL,
    [hisDateUpdated] [datetime2](7) NOT NULL,
    [hisDateCreated] [datetime2](7) NOT NULL,
    [hisUpdatedBy] [varchar](255) NOT NULL,
    [hisCreatedBy] [varchar](255) NOT NULL,
    [hisData] [ntext] NOT NULL,
    [hisDataXml] [xml] NOT NULL
)

EF Core 查询代码

投影语句与Linq-to-SQL中的写法一致(仅将DataContext替换为DbContext):

var profiles = dataContexts.xDS.Profiles
        .Where(p => p.GroupKey == gKey)
        .Where(p => options.AuthIdsToExport.Length == 0 || options.AuthIdsToExport.Contains(p.AuthID))
        .OrderBy(p => p.AuthID)
        .Select(p => new XmlProfileRow { 
             AuthId = p.AuthID, 
             Data = p.Data, 
             DataXml = p.ProfileXml, 
             DateCreated = p.DateCreated, 
             DateUpdated = p.DateUpdated 
         });

var historyDatas = dataContexts.xDS.HistoryDatas
        // .Include( h => h.Profile ) // 此操作无效
        .Where(h => h.GroupKey == gKey )
        .Where(h => options.AuthIdsToExport.Length == 0 || options.AuthIdsToExport.Contains(h.Profile!.AuthID))
        .OrderBy(h => h.Profile!.AuthID)
        .ThenBy(h => h.Type)
        .ThenBy(h => h.Index)
        .Select(h => new XmlHistoryRow { 
             AuthId = h.Profile!.AuthID, 
             DateCreated = h.DateCreated, 
             DateUpdated = h.DateUpdated, 
             Type = h.Type, 
             Index = h.Index, 
             Data = h.Data, 
             DataXml = h.DataXml 
         });

原本通过IEnumerator对象使用这两个查询,我绝对不能将所有数据加载到内存中,需要流式遍历Profiles的结果,并针对每一行流式遍历对应的History Data行,目的是避免为每个Profile的HistoryData行发起新查询。

问题复现代码

var profileEnumerator = profiles.GetEnumerator();
var historyEnumerator = historyDatas.GetEnumerator();
var currentHistory = historyEnumerator.MoveNext();
var currentProfile= profileEnumerator.MoveNext(); // 此行抛出异常

异常调用栈

at Microsoft.Data.SqlClient.SqlInternalConnectionTds.ValidateConnectionForExecute(SqlCommand command)  
at Microsoft.Data.SqlClient.SqlCommand.ValidateCommand(Boolean isAsync, String method)  
at Microsoft.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, TaskCompletionSource'1 completion, Int32 timeout, Task& task, Boolean& usedCache, Boolean asyncWrite, Boolean inRetry, String method)  
at Microsoft.Data.SqlClient.SqlCommand.ExecuteReader(CommandBehavior behavior)  
at Microsoft.Data.SqlClient.SqlCommand.ExecuteDbDataReader(CommandBehavior behavior)  
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReader(RelationalCommandParameterObject parameterObject)  
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable'1.Enumerator.InitializeReader(Enumerator enumerator)  
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable'1.Enumerator.<>c.<MoveNext>b__21_0(DbContext _, Enumerator enumerator)  
at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func'3 operation, Func'3 verifySucceeded)  
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable'1.Enumerator.MoveNext()  
at UserQuery.ExportDataAsync(String groupName, DataOptions options, DataContexts dataContexts), line 154  

生成的SQL语句

抛出异常前生成的SQL如下(逻辑正确):

info: 10/21/2024 10:30:59.384 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)       
Executed DbCommand (1ms) [Parameters=[@__gKey_0='1009' (Nullable = true)], CommandType='Text', CommandTimeout='30']      
SELECT [p].[pAuthID] AS [AuthId], [h].[hisDateCreated] AS [DateCreated], [h].[hisDateUpdated] AS [DateUpdated], [h].[hisType] AS [Type], [h].[hisIndex] AS [Index], [h].[hisData] AS [Data], [h].[hisDataXml] AS [DataXml]      
FROM [HistoryData] AS [h]      
LEFT JOIN [Profile] AS [p] ON [h].[hispKey] = [p].[pKey]      
WHERE [h].[hisgKey] = @__gKey_0      
ORDER BY [p].[pAuthID], [h].[hisType], [h].[hisIndex]

fail: 10/21/2024 10:30:59.385 RelationalEventId.CommandError[20102] (Microsoft.EntityFrameworkCore.Database.Command)       
Failed executing DbCommand (0ms) [Parameters=[@__gKey_0='1009' (Nullable = true)], CommandType='Text', CommandTimeout='30']      
SELECT [p].[pAuthID] AS [AuthId], [p].[pProfile] AS [Data], [p].[pProfileXml] AS [DataXml], [p].[pDateCreated] AS [DateCreated], [p].[pDateUpdated] AS [DateUpdated]      
FROM [Profile] AS [p]      
WHERE [p].[pgKey] = @__gKey_0      
ORDER BY [p].[pAuthID]

解决方案

方法1:启用MARS(多活动结果集)

这是最直接的解决方式,问题本质是同一个数据库连接上同时打开了两个DataReader。只需在连接字符串中添加MultipleActiveResultSets=True即可。

  • MARS是SQL Server原生支持的特性,不会破坏现有业务逻辑
  • 针对流式处理大量数据的场景,MARS性能表现稳定,无需过度担忧兼容性问题

方法2:使用独立DbContext实例

为两个查询分别创建独立的DbContext实例,让每个查询使用各自的数据库连接,从根源避免DataReader冲突:

using var profileContext = new YourDbContext();
var profiles = profileContext.Profiles
    .Where(p => p.GroupKey == gKey && (options.AuthIdsToExport.Length == 0 || options.AuthIdsToExport.Contains(p.AuthID)))
    .OrderBy(p => p.AuthID)
    .Select(p => new XmlProfileRow { ... });

using var historyContext = new YourDbContext();
var historyDatas = historyContext.HistoryDatas
    .Where(h => h.GroupKey == gKey && (options.AuthIdsToExport.Length == 0 || options.AuthIdsToExport.Contains(h.Profile!.AuthID)))
    .OrderBy(h => h.Profile!.AuthID)
    .ThenBy(h => h.Type)
    .ThenBy(h => h.Index)
    .Select(h => new XmlHistoryRow { ... });

var profileEnumerator = profiles.GetEnumerator();
var historyEnumerator = historyDatas.GetEnumerator();
// 后续流式遍历逻辑

这种方式无需修改连接字符串,适合无法启用MARS的场景,但会增加数据库连接数,需注意调整连接池配置。

方法3:合并查询并流式处理

如果业务逻辑允许,可将两个查询合并为一个关联查询,一次性获取所有数据后在内存中分组处理,全程仅使用一个DataReader:

var combinedData = profileContext.Profiles
    .Where(p => p.GroupKey == gKey && (options.AuthIdsToExport.Length == 0 || options.AuthIdsToExport.Contains(p.AuthID)))
    .OrderBy(p => p.AuthID)
    .SelectMany(p => p.HistoryDatas
        .Where(h => h.GroupKey == gKey)
        .OrderBy(h => h.Type)
        .ThenBy(h => h.Index)
        .Select(h => new {
            Profile = new XmlProfileRow { 
                AuthId = p.AuthID, 
                Data = p.Data, 
                DataXml = p.ProfileXml, 
                DateCreated = p.DateCreated, 
                DateUpdated = p.DateUpdated 
            },
            History = new XmlHistoryRow { 
                AuthId = p.AuthID, 
                DateCreated = h.DateCreated, 
                DateUpdated = h.DateUpdated, 
                Type = h.Type, 
                Index = h.Index, 
                Data = h.Data, 
                DataXml = h.DataXml 
            }
        })
        .DefaultIfEmpty() // 保留无历史数据的Profile
    );

foreach (var item in combinedData)
{
    // 流式处理Profile及对应的History数据
}

这种方式彻底避免了DataReader冲突,同时保持流式处理特性,需确保EF Core能生成高效的关联SQL,避免N+1查询问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 00:30:56