Когда я нашел этот вопрос в первый раз в конце 2018 года, я не думал, что может быть ошибка в тогдашнем ответе с наибольшим количеством голосов, но это так. Сначала я подумал о том, чтобы просто прокомментировать ответ, но потом снова захотел подкрепить свою заявку своими ссылками. И тесты, которые я сделал (на основе .Net Framework 4.6.1 и .Net Core 2.1.)
Учитывая ограничение OP, транзакция должна быть объявлена в соединении, что оставляет нас к двум различным реализациям, уже упомянутым в других ответах:
Использование TransactionScope
using (SqlConnection conn = new SqlConnection(conn2))
{
try
{
conn.Open();
using (TransactionScope ts = new TransactionScope())
{
conn.EnlistTransaction(Transaction.Current);
using (SqlCommand command = new SqlCommand(query, conn))
{
command.ExecuteNonQuery();
//TESTING: throw new System.InvalidOperationException("Something bad happened.");
}
ts.Complete();
}
}
catch (Exception)
{
throw;
}
}
Использование SqlTransaction
using (SqlConnection conn = new SqlConnection(conn3))
{
try
{
conn.Open();
using (SqlTransaction ts = conn.BeginTransaction())
{
using (SqlCommand command = new SqlCommand(query, conn, ts))
{
command.ExecuteNonQuery();
//TESTING: throw new System.InvalidOperationException("Something bad happened.");
}
ts.Commit();
}
}
catch (Exception)
{
throw;
}
}
Вы должны знать, что при объявлении TransactionScope в SqlConnection этот объект соединения не автоматически зачисляется в транзакцию, вместо этого вы должны явным образом подключить его к conn.EnlistTransaction(Transaction.Current);
Проверьте и докажите
Я подготовил простую таблицу в базе данных SQL Server:
SELECT * FROM [staging].[TestTable]
Column1
-----------
1
Запрос на обновление в .NET выглядит следующим образом:
string query = @"UPDATE staging.TestTable
SET Column1 = 2";
И сразу после command.ExecuteNonQuery () выдается исключение:
command.ExecuteNonQuery();
throw new System.InvalidOperationException("Something bad happened.");
Вот полный пример для справки:
string query = @"UPDATE staging.TestTable
SET Column1 = 2";
using (SqlConnection conn = new SqlConnection(conn2))
{
try
{
conn.Open();
using (TransactionScope ts = new TransactionScope())
{
conn.EnlistTransaction(Transaction.Current);
using (SqlCommand command = new SqlCommand(query, conn))
{
command.ExecuteNonQuery();
throw new System.InvalidOperationException("Something bad happened.");
}
ts.Complete();
}
}
catch (Exception)
{
throw;
}
}
Если тест выполняется, он генерирует исключение до завершения TransactionScope, и обновление не применяется к таблице (откат транзакции), а значение остается неизменным. Это ожидаемое поведение, как все и ожидали.
Column1
-----------
1
Что происходит сейчас, если мы забыли включить соединение в транзакцию с conn.EnlistTransaction(Transaction.Current);
?
Повторный запуск примера снова вызывает исключение, и поток выполнения сразу переходит к блоку catch. Хотя ts.Complete();
никогда не вызывается, табличное значение изменилось:
Column1
-----------
2
Так как область транзакции объявляется после SqlConnection, соединение не знает о области и не косвенно подключается к так называемой внешней транзакции .
Более глубокий анализ для ботаников базы данных
Чтобы копнуть еще глубже, если выполнение приостанавливается после command.ExecuteNonQuery();
и до того, как выдается исключение, мы можем запросить транзакцию в базе данных (SQL Server) следующим образом:
SELECT tst.session_id, tat.transaction_id, is_local, open_transaction_count, transaction_begin_time, dtc_state, dtc_status
FROM sys.dm_tran_session_transactions tst
LEFT JOIN sys.dm_tran_active_transactions tat
ON tst.transaction_id = tat.transaction_id
WHERE tst.session_id IN (SELECT session_id FROM sys.dm_exec_sessions WHERE program_name = 'TransactionScopeTest')
Обратите внимание, что можно установить имя_сессии сеанса через свойство Имя приложения в строке подключения: Application Name=TransactionScopeTest;
Текущая действующая транзакция разворачивается ниже:
session_id transaction_id is_local open_transaction_count transaction_begin_time dtc_state dtc_status
----------- -------------------- -------- ---------------------- ----------------------- ----------- -----------
113 6321722 1 1 2018-11-30 09:09:06.013 0 0
Без conn.EnlistTransaction(Transaction.Current);
никакая транзакция не связана с активным соединением, и поэтому изменения не происходят в транзакционном контексте:
session_id transaction_id is_local open_transaction_count transaction_begin_time dtc_state dtc_status
----------- -------------------- -------- ---------------------- ----------------------- ----------- -----------
Замечания .NET Framework и .NET Core
Во время моих испытаний с .NET Core я обнаружил следующее исключение:
System.NotSupportedException: 'Enlisting in Ambient transactions is not supported.'
Кажется, .NET Core (2.1.0) в настоящее время не поддерживает подход TransactionScope, независимо от того, инициализируется ли Scope до или после SqlConnection.