Как заставить только одну транзакцию в нескольких классах DbContext?

Предыстория:
Из другого вопроса здесь, в SO, у меня есть решение Winforms (Finance) со многими проектами (фиксированные проекты для решения). Теперь один из моих клиентов попросил меня «обновить» решение и добавить проекты/модули, которые будут взяты из другого решения Winforms (HR).

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

Вопрос.
У меня есть контекст (DbContext, созданный для реализации универсального шаблона репозитория) со списком внешних контекстов (загружаемых с помощью MEF — эти контексты представляют контексты из каждого подключаемого модуля, а также с помощью универсального шаблона репозитория). Шаблон репозитория).

Допустим, у меня есть это:

public class MainContext : DbContext
{
   public List<IPluginContext> ExternalContexts { get; set; }

   // other stuff here
}

а также

public class PluginContext_A : DbContext, IPluginContext
{ /* Items from this context */ }

public class PluginContext_B : DbContext, IPluginContext
{ /* Items from this context */ }

и в уже загруженном классе MainContext у меня есть оба внешних контекста (из плагинов).

Имея это в виду, допустим, у меня есть транзакция, которая повлияет как на MainContext, так и на PluginContext_B.

Как выполнить обновление/вставку/удаление в обоих контекстах в рамках одной транзакции (единство работы)?

Используя IUnityOfWork, я могу установить SaveChanges() для последнего элемента, но, насколько я знаю, у меня должен быть один контекст, чтобы он работал как одна транзакция.

Есть способ использовать MSDTC (TransactionScope), но этот подход ужасен, и я бы вообще не стал его использовать (также потому, что мне нужно включить MSDTC на клиентах и ​​сервере, и у меня все время были сбои и утечки).

Обновление:
Системы используют SQL 2008 R2. Никогда не ревите.
Если есть возможность использовать TransactionScope таким образом, чтобы он не масштабировался до MSDTC, это прекрасно, но я так и не добился этого. Все время, когда я использовал TransactionScope, он входит в MSDTC. Согласно другому сообщению на SO, в некоторых случаях TS не переходит в MSDTC: машины">проверьте здесь. Но я бы предпочел пойти другим путем вместо TransactionScope...


person Anderson Matos    schedule 06.01.2012    source источник


Ответы (4)


Если вы используете несколько контекстов, каждый из которых использует отдельное соединение, и вы хотите сохранить данные в этом контексте в одной транзакции, вы должны использовать TransactionScope с распределенной транзакцией (MSDTC).

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

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

using (var connection = new SqlConnection(connnectionString))
{
    var c1 = new Context(connection);
    var c2 = new Context(connection);

    c1.MyEntities.Add(new MyEntity() { Name = "A" });
    c2.MyEntities.Add(new MyEntity() { Name = "B" });

    connection.Open(); 

    using (var scope = new TransactionScope())
    {
        // This is necessary because DbContext doesnt't contain necessary methods
        ObjectContext obj1 = ((IObjectContextAdapter)c1).ObjectContext;
        obj1.SaveChanges(SaveOptions.DetectChangesBeforeSave);

        ObjectContext obj2 = ((IObjectContextAdapter)c2).ObjectContext;
        obj2.SaveChanges(SaveOptions.DetectChangesBeforeSave);

        scope.Complete();

        // Only after successful commit of both save operations we can accept changes
        // otherwise in rollback caused by second context the changes from the first
        // context will be already accepted = lost

        obj1.AcceptAllChanges();
        obj2.AcceptAllChanges();
    }
}

Конструктор контекста определяется как:

public Context(DbConnection connection) : base(connection,false) { }

Сам образец работал у меня, но у него есть несколько проблем:

  • Первое использование контекстов должно выполняться при закрытом соединении. Вот почему я добавляю объекты до открытия соединения.
  • Я скорее открываю соединение вручную вне транзакции, но, возможно, это не нужно.
  • Оба сохранения изменений успешно выполняются, а Transaction.Current имеет пустой идентификатор распределенной транзакции, поэтому он должен быть локальным.
  • Сохранение намного сложнее, и вы должны использовать ObjectContext, потому что DbContext не имеет всех необходимых методов.
  • Это не должно работать в каждом сценарии. Даже MSDN утверждает это:

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

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

person Ladislav Mrnka    schedule 06.01.2012
comment
Есть ли способ реализовать двухэтапную фиксацию, например с областью транзакций, которая не масштабируется в MSDTC? Например, есть ли какой-либо интерфейс или API, который я мог бы изучить, чтобы продолжать работать с транзакциями так же, как TransactionScope, но с которым я мог бы справиться сам? - person Anderson Matos; 09.01.2012
comment
Я так не думаю, но это только предположение. MSDTC является центральным компонентом реализации двухфазной фиксации на платформе MS. - person Ladislav Mrnka; 09.01.2012
comment
Я нашел этот другой пост на SO: stackoverflow.com/questions/815586/ Это не так просто, но что, если я создам контроллер с несколькими транзакциями (по одной для каждого контекста) и внутри транзакций я сделаю SaveChanges(false). Если все пойдет хорошо, я могу выполнить TransactionScope.Complete() и AcceptAllChanges. Звучит интересно, но я не знаю, будет ли это вообще работать. - person Anderson Matos; 10.01.2012
comment
И что вы будете делать, если что-то пойдет не так? Некоторые транзакции могут быть уже совершены, и вам придется написать пользовательскую компенсацию, чтобы отменить изменения. - person Ladislav Mrnka; 10.01.2012

@Ladislav Mrnka Вы были правы с самого начала: я должен использовать MSDTC.

Я пробовал здесь несколько вещей, включая предоставленный мной пример кода. Я много раз проверял это с измененным зайцем и там, но это не сработает. Ошибка углубляется в то, как работают EF и DbContext, и для того, чтобы это изменить, я, наконец, нашел свой собственный инструмент ORM. Это не так.

Я также поговорил с другом (MVP), который тоже много знает об EF. Мы протестировали некоторые другие вещи здесь, но это не будет работать так, как я хочу. Я закончу с несколькими изолированными транзакциями (я пытался собрать их вместе с моим примером кода), и с этим подходом у меня нет никакого способа принудительно выполнить полный откат автоматически, и мне придется создать много общих /custom код для ручного отката изменений и здесь возникает еще один вопрос: что если такой откат не удался (это не откат, а просто обновление)?

Таким образом, единственный способ, который мы нашли здесь, — это использовать MSDTC и создать некоторые инструменты, помогающие отладить/проверить, включен ли DTC, исправны ли брандмауэры клиент/сервер и все такое прочее.

Спасибо, в любом случае. знак равно

person Anderson Matos    schedule 17.01.2012

Итак, есть шанс, что это изменится к 19 октября? На всех интертьюбах люди предлагают следующий код, и он не работает:

    (_contextA as IObjectContextAdapter).ObjectContext.Connection.Open();
    (_contextB as IObjectContextAdapter).ObjectContext.Connection.Open();

    using (var transaction = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions{IsolationLevel = IsolationLevel.ReadUncommitted, Timeout = TimeSpan.MaxValue}))
{
    _contextA.SaveChanges();
    _contextB.SaveChanges();

    // Commit the transaction
    transaction.Complete();
}

    // Close open connections
    (_contextA as IObjectContextAdapter).ObjectContext.Connection.Close();
    (_contextB as IObjectContextAdapter).ObjectContext.Connection.Close();

Это серьезное препятствие для реализации одного класса Unit of Work в репозиториях. Любой новый способ обойти это?

person LandonC    schedule 19.10.2012

Чтобы избежать использования MSDTC (распределенная транзакция):

Это должно заставить вас использовать одно соединение внутри транзакции, а также только одну транзакцию. В противном случае он должен генерировать исключение.

Примечание. Требуется как минимум EF6.

class TransactionsExample 
 { 
    static void UsingExternalTransaction() 
    { 
        using (var conn = new SqlConnection("...")) 
        { 
           conn.Open(); 

           using (var sqlTxn = conn.BeginTransaction(System.Data.IsolationLevel.Snapshot)) 
           { 
               try 
               { 
                   var sqlCommand = new SqlCommand(); 
                   sqlCommand.Connection = conn; 
                   sqlCommand.Transaction = sqlTxn; 
                   sqlCommand.CommandText = 
                       @"UPDATE Blogs SET Rating = 5" + 
                        " WHERE Name LIKE '%Entity Framework%'"; 
                   sqlCommand.ExecuteNonQuery(); 

                   using (var context =  
                     new BloggingContext(conn, contextOwnsConnection: false)) 
                    { 
                        context.Database.UseTransaction(sqlTxn); 

                        var query =  context.Posts.Where(p => p.Blog.Rating >= 5); 
                        foreach (var post in query) 
                        { 
                            post.Title += "[Cool Blog]"; 
                        } 
                       context.SaveChanges(); 
                    } 

                    sqlTxn.Commit(); 
                } 
                catch (Exception) 
                { 
                    sqlTxn.Rollback(); 
                } 
            } 
        } 
    } 
} 

Источник: http://msdn.microsoft.com/en-us/data/dn456843.aspx#existing

person thenninger    schedule 23.06.2014
comment
Спасибо, но заметили ли вы, что этот вопрос был создан 2 года назад и касался EF4? ;) - person Anderson Matos; 01.07.2014
comment
Я знаю, поэтому я добавил примечание: Примечание. Требуется как минимум EF6. Я опубликовал это, потому что быстро нашел эту статью при поиске и подумал, что она укажет другим правильное направление вместо использования старого ответа. ;) - person thenninger; 18.07.2014