回滚事务抛出"已经有一个与此命令关联的开放DataReader ..."错误

时间:2017-08-31 01:37:14

标签: entity-framework .net-core entity-framework-core

我在.NET Core 2.0上使用Entity Framework Core 2.0。为了重新创建这个问题,我制作了一个简单的控制台应用程序。

// Program.cs
static void Main(string[] args)
{
  using (var dbContext = new MyDbContext())
  {
    using (var transaction = dbContext.Database.BeginTransaction())
    {
      try
      {
        var blogs = dbContext.Blogs
          .ToList(); // throws error because of schema mismatch in my Blog class

        // other stuff that may or may not make db changes

        dbContext.SaveChanges();

        transaction.Commit();
      }

      catch (Exception ex)
      {
        transaction.Rollback(); // throws second error that hides the initial error: "There is already an open DataReader..."
      }
    }
  }
}

我故意将我的Blog类映射到数据库模式时出错,因此当我执行.ToList() dbContext.Blogs实体框架时会抛出无效的操作错误,例如{{1} }因为An exception occurred while reading a database value for Blog.Name在数据库中是Name,而在nvarchar(max)类中是int

所以现在我的Blog语句尝试在事务期间发生任何错误时回滚事务,但该回滚会导致另一个错误,即最终被记录的错误,隐藏初始错误。

catch

现在这是一个非常简单的例子,我意识到这里没有什么可以提交的。实际上,我正在运行一个ASP.NET核心应用程序,我有一个全局事务包装器,所以有些请求可能只是读取,但其他请求可能是读取和更新。

我在这里做错了吗?从我所读到的内容来看,这是相当标准的,但我已经在谷歌上搜索了两天而没有发现任何人遇到同样的问题。

当查询数据库中的博客时,EF似乎打开了DataReader,然后在执行查询的过程中发生异常,因此DataReader保持打开状态,所以在此之后对连接做任何事情时,我得到了开放的DataReader错误。如果是这样的话,我该如何处理查询期间发生的错误?我需要确保回滚可能发生的任何更新,并且我需要处理事务和连接。

1 个答案:

答案 0 :(得分:4)

这是(IMO)System.Data.SqlClient.SqlTransaction中的一个错误,它也在.NET Framework上进行了重新编译。它也是 EF Core中的一个错误,因为它不会在EF6上重现。我不确定这是否是一个已知的bug,并且可能会对EF Core和SqlTransaction进行不同的分类。最终,您的使用块将回滚事务。

这是一个最小的ADO.NET repro:

using System;
using System.Data.SqlClient;

namespace ConsoleApp8
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var con = new SqlConnection("Server=localhost;database=tempdb;Integrated Security=true;MultipleActiveResultsets=false"))
            {
                con.Open();
                using (var tran = con.BeginTransaction())
                {
                    var cmd = new SqlCommand("select * from sys.objects", con, tran);
                    var rdr = cmd.ExecuteReader();
                    rdr.Read();

                    tran.Rollback();
                }
            }
        }
    }
}

最小的EF Core repro:

using Microsoft.EntityFrameworkCore;
using System;
using System.Data.SqlClient;
using System.Linq;

namespace ConsoleApp8
{

    public class Foo
    {
        public int Id { get; set; }
    }
    public class Db : DbContext
    {
        public DbSet<Foo> Foos { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer("Server=localhost;database=EfCoreTest;Integrated Security=true;MultipleActiveResultsets=false");
            base.OnConfiguring(optionsBuilder);
        }

    }
    class Program
    {
        static void Main(string[] args)
        {
            using (var db = new Db())
            {
                db.Database.EnsureDeleted();
                db.Database.EnsureCreated();
                db.Foos.Add(new Foo());
                db.SaveChanges();
            }

            using (var db = new Db())
            { 
                var tran = db.Database.BeginTransaction();
                foreach (var foo in db.Foos)
                {
                    tran.Rollback();
                }

            }

        }
    }
}

有趣的是,这个错误不会在EF6上重现,因为在EF6中DbContext.Database.BeginTransaction使用EntityTransaction,其中EF核心使用轻量级包装器而不是SqlClient的SqlTransation,这具有这种不幸的行为。

要解决此问题,请在连接字符串中添加MultipleActiveResultsets=true(这在EF中非常有用,因为它允许您在读取结果时运行其他查询)。或者使用TSQL回滚您的交易,如下所示:

db.Database.ExecuteSqlCommand("if @@trancount > 0 rollback;");

我打开了一个GitHub问题来跟踪此问题:https://github.com/aspnet/EntityFrameworkCore/issues/9658