Entity Framework "There is already an open DataReader associated with this Command which must be closed first"

Entity Framework "There is already an open DataReader associated with this Command which must be closed first"

我希望我能把这个问题总结得足够好。 让我们从设置开始吧。

设置

我们有一个 ASP.NET 核心应用程序并使用 Entity Framework(没有核心)。 我们对所有控制器使用通用的 AuthorizeFilter:

services.AddMvc(options =>
{
    var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
        options.Filters.Add(new AuthorizeFilter(policy));
})

并且我们对某些控制器应用了额外的授权策略。例如:

[ApiController]
[Authorize("UserHasActivatedFeature")]
[Authorize("IsPremiumUser")]
public class PremiumFeatureController : ControllerBase

这些策略都通过单个 IAuthorizationHandler 实现来处理。看起来像这样:

public class UserAuthorizationHandler : IAuthorizationHandler
{
    private readonly IUserRepository _userRepository;
    public UserAuthorizationHandler(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }
    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        var pendingRequirements = context.PendingRequirements.ToList();

        foreach (var requirement in pendingRequirements)
        {
            switch (requirement)
            {
                case PremiumRequirement:
                    if (_userRepository.IsPremium(userId))
                    {
                        context.Succeed(requirement);
                    }

                    break;
                case FeatureActivatedRequirement:
                    if (_userRepository.IsFeatureActivated(userId))
                    {
                        context.Succeed(requirement);
                    }

                    break;
            }
        }

        return Task.CompletedTask;
    }
}

UserRepository 如下所示:

public class UserRepository : IRepository<User>, IUserRepository
{
    private DbSet<User> DbSet { get; private set; }
    public UserRepository(UnitOfWork unitOfWork)
    {
        DbSet = unitOfWork.DbContext.Set<User>();
    }
    public bool IsPremium(int userId)
    {
        return DbSet.AsNoTracking().Any(user => user.Id == userId && user.IsPremium);
    }
    public bool IsFeatureActivated(int userId)
    {
        return DbSet.AsNoTracking().Any(user => user.Id == userId && user.IsFeatureActivated);
    }
}

问题

在我们的测试环境中,大约有 20 个人正在测试我们的应用程序。有时 UserAuthorizationHandler 抛出异常说:“已经有一个打开的 DataReader 与此命令关联,必须先关闭”。抛出此异常时,它会从 IsPremium 抛出两次,一次从 IsFeatureActivated 抛出。

Stacktrace(2 个中只有 1 个)

System.Data.Entity.Core.EntityCommandExecutionException: An error occurred while executing the command definition. See the inner exception for details. ---> System.InvalidOperationException: There is already an open DataReader associated with this Command which must be closed first.
at System.Data.SqlClient.SqlInternalConnectionTds.ValidateConnectionForExecute(SqlCommand command)
at System.Data.SqlClient.SqlCommand.ValidateCommand(String method, Boolean async)
at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, TaskCompletionSource`1 completion, Int32 timeout, Task& task, Boolean& usedCache, Boolean asyncWrite, Boolean inRetry)
at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method)
at System.Data.SqlClient.SqlCommand.ExecuteReader(CommandBehavior behavior, String method)
at System.Data.Entity.Infrastructure.Interception.InternalDispatcher`1.Dispatch[TTarget,TInterceptionContext,TResult](TTarget target, Func`3 operation, TInterceptionContext interceptionContext, Action`3 executing, Action`3 executed)
at System.Data.Entity.Infrastructure.Interception.DbCommandDispatcher.Reader(DbCommand command, DbCommandInterceptionContext interceptionContext)
at System.Data.Entity.Core.EntityClient.Internal.EntityCommandDefinition.ExecuteStoreCommands(EntityCommand entityCommand, CommandBehavior behavior)
--- End of inner exception stack trace ---
at System.Data.Entity.Core.EntityClient.Internal.EntityCommandDefinition.ExecuteStoreCommands(EntityCommand entityCommand, CommandBehavior behavior)
at System.Data.Entity.Core.Objects.Internal.ObjectQueryExecutionPlan.Execute[TResultType](ObjectContext context, ObjectParameterCollection parameterValues)
at System.Data.Entity.Core.Objects.ObjectContext.ExecuteInTransaction[T](Func`1 func, IDbExecutionStrategy executionStrategy, Boolean startLocalTransaction, Boolean releaseConnectionOnSuccess)
at System.Data.Entity.Core.Objects.ObjectQuery`1.<>c__DisplayClass41_0.<GetResults>b__0()
at System.Data.Entity.SqlServer.DefaultSqlExecutionStrategy.Execute[TResult](Func`1 operation)
at System.Data.Entity.Core.Objects.ObjectQuery`1.GetResults(Nullable`1 forMergeOption)
at System.Data.Entity.Core.Objects.ObjectQuery`1.<System.Collections.Generic.IEnumerable<T>.GetEnumerator>b__31_0()
at System.Data.Entity.Internal.LazyEnumerator`1.MoveNext()
at System.Linq.Enumerable.Single[TSource](IEnumerable`1 source)
at awesomeCompany.Repositories.UserRepository.IsPremium(Int32 userId)
at awesomeCompany.API.Handlers.UserAuthorizationHandler.HandleAsync(AuthorizationHandlerContext context)
Inner Exception:
System.InvalidOperationException: There is already an open DataReader associated with this Command which must be closed first.
at System.Data.SqlClient.SqlInternalConnectionTds.ValidateConnectionForExecute(SqlCommand command)
at System.Data.SqlClient.SqlCommand.ValidateCommand(String method, Boolean async)
at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, TaskCompletionSource`1 completion, Int32 timeout, Task& task, Boolean& usedCache, Boolean asyncWrite, Boolean inRetry)
at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method)
at System.Data.SqlClient.SqlCommand.ExecuteReader(CommandBehavior behavior, String method)
at System.Data.Entity.Infrastructure.Interception.InternalDispatcher`1.Dispatch[TTarget,TInterceptionContext,TResult](TTarget target, Func`3 operation, TInterceptionContext interceptionContext, Action`3 executing, Action`3 executed)
at System.Data.Entity.Infrastructure.Interception.DbCommandDispatcher.Reader(DbCommand command, DbCommandInterceptionContext interceptionContext)
at System.Data.Entity.Core.EntityClient.Internal.EntityCommandDefinition.ExecuteStoreCommands(EntityCommand entityCommand, CommandBehavior behavior)

我尝试了什么?

正如我上面所说的,这个异常只是偶尔出现(比如一天 2 或 3 次)所以我试图在本地隔离问题并创建单元测试来同时使用多个并行调用来测试存储库,但是我无法重现该问题。同时调用方法 100 次接缝是完全没问题的。

我在 Whosebug 上阅读了很多类似的问题。在另一个查询中或在 IEnumerable 循环中使用查询时,许多接缝都会遇到这个问题,但是因为我只在正常的 foreach 循环中对数据库实体使用 Any() 方法,遍历与列表完全无关的列表userRepository 我不确定是否有任何解决方案适用于此。

同时将 Setting MultipleActiveResultSets=true 应用于连接字符串接缝并不是一个好的解决方案,我只会将其视为最后的选择。

问题首先是使用单例。 DbContext 既不是线程安全的,也不是长期存在的。它是一个工作单元,可以缓存任何未决的更改,并在调用 SaveChanges 时以原子方式全部提交,或者在处置时丢弃它们。这意味着一旦工作单元完成,就应该始终释放 DbContext。

使用单例 DbContext 也没有任何好处。这不是连接,因此长时间保持连接不会节省时间。当 DbContext 必须读取数据或保留更改时,它会自行打开和关闭连接。它不会让他们活着。

通过制作 IAuthorizationHandler,您还可以将 DbContext 实例保持为单例,并最终从多个请求中同时使用它。在两个请求尝试同时从 DbContext 实例读取之前,您可能不会收到任何异常。

解决方案

设置 UserAuthorizationHandler 范围

要解决此问题,最简单的解决方案似乎是使 UserAuthorizationHandler 成为一个作用域实例。此处发布的代码似乎没有使用任何其他单例服务,因此它本身不需要是单例。

明确使用范围

如果必须保持单例,则必须根据需要在范围内创建 DbContext 和扩展的 UserRepository。在这种情况下,在 HandleAsync 内。为此,您需要注入 IServiceProvider 而不是 IUserRepository。这在 Consuming a scoped service in a background task 中有解释:

public class UserAuthorizationHandler : IAuthorizationHandler
{
    private readonly IServiceProvider _services;
    public UserAuthorizationHandler(IServiceProvider services)
    {
        _services = services;
    }
    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        using (var scope = Services.CreateScope())
        {
            using(var userRepository=scope.GetRequiredService<IUserRepository>())
            {
                var pendingRequirements = context.PendingRequirements.ToList();
                ...
            }
        }
    }

顺便说一句 不要 在异步方法中使用阻塞 IO/database 调用。这会阻止一个线程,该线程可用于服务其他请求 由于自旋等待增加 CPU 使用率。 很多 ASP.NET 核心的性能提升来自避免不必要的块

这意味着 IsPremiumIsFeatureActivated 应该变成异步的,或者,如果确实需要同步方法,则应该添加异步版本。在这种特殊情况下,两种方法都被使用,所以一个好主意是使用 单个 查询来 return 两个值:

public async Task<(bool isPremium,isFeatureActivated)> FeaturesForAsync(int userId)
{
    var results=await _context.Users
                        .Where(u=>u.Id==userId)
                        .Select(u=>new {u.IsPremium,u.IsFeatureActivated})
                        .SingleOrDefaultAsync();

    if (results==null) return default;
    return (results.IsPremium,results.IsFeatureActivated);
}

或者,使用记录而不是元组:

record UserFeatures(bool IsPremium,bool IsFeatureActivated);

public async Task<(bool isPremium,isFeatureActivated)> FeaturesForAsync(int userId)
{
    var results=await _context.Users
                        .Where(u=>u.Id==userId)
                        .Select(u=>new UserFeatures( 
                                         u.IsPremium,
                                         u.IsFeatureActivated))
                        .SingleOrDefaultAsync();

    return results ?? new UserFeatures(false,false);
}