Одна транзакция с несколькими dbcontexts

Я использую транзакции в своих модульных тестах для отката изменений. unit test использует dbcontext, а служба, которую я тестирую, использует свою. Оба они завернуты в одну транзакцию, а один dbcontext находится в блоке другого. Дело в том, что когда внутренний dbcontext сохраняет свои изменения, он не виден внешнему dbcontext (и я не думаю, потому что в другом dbcontext уже может быть загружен объект). Вот пример:

[TestMethod]
public void EditDepartmentTest()
{
    using (TransactionScope transaction = new TransactionScope())
    {
        using (MyDbContext db = new MyDbContext())
        {
            //Arrange
            int departmentId = (from d in db.Departments
                                   where d.Name == "Dep1"
                                   select d.Id).Single();
            string newName = "newName",
                   newCode = "newCode";

            //Act
            IDepartmentService service = new DepartmentService();
            service.EditDepartment(departmentId, newName, newCode);

            //Assert
            Department department = db.Departments.Find(departmentId);
            Assert.AreEqual(newName, department.Name,"Unexpected department name!");
            //Exception is thrown because department.Name is "Dep1" instead of "newName"
            Assert.AreEqual(newCode, department.Code, "Unexpected department code!");
        }
    }
}

Услуга:

public class DepartmentService : IDepartmentService
{
    public void EditDepartment(int DepartmentId, string Name, string Code)
    {
        using (MyDbContext db = new MyDbContext ())
        {
            Department department = db.Departments.Find(DepartmentId);

            department.Name = Name;
            department.Code = Code;

            db.SaveChanges();

        }
    }
}

Однако, если я закрываю внешний dbcontext перед вызовом службы и открываю новый dbcontext для assert, все работает нормально:

[TestMethod]
public void EditDepartmentTest()
{
    using (TransactionScope transaction = new TransactionScope())
    {
        int departmentId=0;
        string newName = "newName",
               newCode = "newCode";

        using (MyDbContext db = new MyDbContext())
        {
            //Arrange
            departmentId = (from d in db.Departments
                                   where d.Name == "Dep1"
                                   select d.Id).Single();
        }

        //Act
        IDepartmentService service = new DepartmentService();
        service.EditDepartment(departmentId, newName, newCode);

        using (MyDbContext db = new MyDbContext())
        {
            //Assert
            Department department = db.Departments.Find(departmentId);
            Assert.AreEqual(newName, department.Name,"Unexpected department name!");
            Assert.AreEqual(newCode, department.Code, "Unexpected department code!");
        }
    }
}

Итак, в основном у меня есть решение этой проблемы (подумал об этом во время написания этого вопроса), но я все еще удивляюсь, почему невозможно получить доступ к незафиксированным данным в транзакции, когда dbcontexts вложены. Может быть, использование (dbcontext) похоже на транзакцию? Если это так, я все еще не понимаю проблему, так как я вызываю .SaveChanges() во внутреннем dbcontext.

Ответ 1

В первом сценарии вы вложен DbContexts. Для каждой из них открывается соединение с базой данных. Когда вы вызываете свой метод обслуживания в блоке using, новое соединение открывается в TransactionScope, а еще один уже открыт. Это приведет к тому, что ваша транзакция будет повышена до распределенная транзакция и частично зафиксированные данные (результат DbContext.SaveChanges вызов в службе), не доступный из вашего внешнего соединения. Также обратите внимание, что распределенные транзакции намного медленнее и, следовательно, это имеет побочный эффект снижения производительности.

Во втором сценарии, когда вы открываете и закрываете три подключения, в одной транзакции одновременно открываются только одно соединение. Поскольку эти соединения используют ту же строку соединения, транзакция не будет автоматически продвигаться к распределенному соединению, и поэтому каждое последующее соединение внутри транзакции имеет доступ к изменениям, выполненным предыдущим соединением. p >

Вы можете попробовать добавить параметр Enlist=false в строку подключения. Это приведет к отключению автоматического включения в распределенную транзакцию, в результате чего исключение будет поднято в вашем первом сценарии. Второй сценарий будет работать безупречно, если вы используете SQL Server 2008 и более поздние версии, поскольку транзакция не будет повышена. (Предыдущие версии SQL Server все равно будут способствовать транзакции в этом сценарии.)

Вы также можете найти полезный этот отличный ответ на довольно похожий вопрос.

Ответ 2

Использование свежих контекстов слишком часто - это анти-шаблон. Создайте один контекст и передайте его. Очень легко сделать прохождение вокруг, используя инфраструктуру инъекции зависимостей.

Однако, если я закрываю внешний dbcontext перед вызовом службы и открываю новый dbcontext для assert, все работает отлично

Нет, это совпадение, потому что второй контекст повторно использовал соединение 1-го из пула соединений. Это не гарантируется и будет ломаться под нагрузкой.

Единственный способ избежать распределенных транзакций - использовать одно соединение, которое было открыто.

Однако вы можете иметь несколько контекстов, совместно использующих одно и то же соединение. Создайте соединение с созданным вручную соединением.

Ответ 3

Это работает:

открытый класс Test1   {       public int Id {get; задавать; }       public string Имя {get; задавать; }   }

public class Test2
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class DC1 : DbContext
{
    public DbSet<Test1> Test1 { get; set; }

    public DC1(SqlConnection conn)
        : base(conn, contextOwnsConnection: false)
    {
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.HasDefaultSchema("dc1");

        modelBuilder.Entity<Test1>().ToTable("Test1");
    }
}

public class DC2 : DbContext
{
    public DbSet<Test2> Test2 { get; set; }

    public DC2(SqlConnection conn)
        : base(conn, contextOwnsConnection: false)
    {
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.HasDefaultSchema("dc2");

        modelBuilder.Entity<Test2>().ToTable("Test2");
    }
}

...

using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["EntityConnectionString"].ConnectionString))
{
    conn.Open();

    using (var tr = conn.BeginTransaction())
    {
        try
        {
            using (var dc1 = new DC1(conn))
            {
                dc1.Database.UseTransaction(tr);
                var t = dc1.Test1.ToList();
                dc1.Test1.Add(new Test1
                {
                    Name = "77777",
                });
                dc1.SaveChanges();
            }
            //throw new Exception();
            using (var dc2 = new DC2(conn))
            {
                dc2.Database.UseTransaction(tr);
                var t = dc2.Test2.ToList();
                dc2.Test2.Add(new Test2
                {
                    Name = "777777",
                });
                dc2.SaveChanges();
            }
            tr.Commit();
        }
        catch
        {
            tr.Rollback();
            //throw;
        }
        App.Current.Shutdown();
    }
}

Я предполагаю, что лучше вывести за пределы транзакции, так что никакой блокировки не происходит, но я не уверен - нужно исследовать это сам.

Обновление: Вышеприведенный код работает с кодовым подходом Код ниже для первой базы данных

public MetadataWorkspace GetWorkspace(Assembly assembly)
{
    MetadataWorkspace result = null;
    //if (!mCache.TryGetValue(assembly, out result) || result == null)
    {
        result = new MetadataWorkspace(
            new string[] { "res://*/" },
            new Assembly[] { assembly });
        //mCache.TryAdd(assembly, result);
    }
    return result;
}

...

using(var conn = new SqlConnection("..."))
{
  conn.Open();
  using(var tr = conn.BeginTransaction())
  {
        using(var entityConnection1 = new EntityConnection(
            GetWorkspace(typeof(DbContext1).Assembly), conn))
      {
        using(var context1 = new ObjectContext(entityConnection1))
        {
          using(var dbc1 = new DbContext1(context1, false))
          {
            using(var entityConnection2 = new EntityConnection(
                GetWorkspace(typeof(DbContext2).Assembly), conn))
            {
                using(var context2 = new ObjectContext(entityConnection2))
                {
                  using(var dbc2 = new DbContext2(context2, false))
                  {
                    try
                    {
                        dbc1.UseTransaction(tr);
                        // fetch and modify data
                        dbc1.SaveChanges();

                        dbc2.UseTransaction(tr);
                        // fetch and modify data
                        dbc2.SaveChanges();

                        tr.Commit();
                    }
                    catch
                    {
                        tr.Rollback();
                    }
                  }
                }
              }
          }
        }
      }
  }
}

Это полезно при использовании большого количества DbContexts в вашем приложении. Например, если у вас тысячи таблиц - я просто создал так называемые "модули" со ста таблицей на модуль. И каждый "модуль" имеет один контекст иногда, хотя мне нужно делать кросс-модульные изменения данных в одной транзакции

Ответ 4

Внешний контекст кэширует объект, полученный во время вашего размещения.