A long time ago, ADO.NET 2.0 was released, and along with it was the System.Transactions assembly, which contains the TransactionScope class - a guide to the world of easy and unconstrained use of transactions. In today's article I will discuss some of the nuances that arise when using this leaky, but such a pretty abstraction.
So, starting with ADO.NET 2.0, in order to enclose your code in a transaction, the developer only needs to locate it inside the TransactionScope block:
')
using (var transactionScope = new TransactionScope(TransactionScopeOption.Suppress, new TransactionOptions() { IsolationLevel = IsolationLevel.Serializable }) {
I used the most important parameters in the constructor - let's consider them (in reverse order).
IsolationLevel
Good old
IsolationLevel . Enum IsolationLevel includes as many as 7 levels of isolation, but do not flatter yourself - these values are interpreted only as recommendations of the ADO.NET provider, and you can use only those levels that are supported by your DBMS.
By default, the highest isolation level is used - Serializable, and I even came across criticism on this score: they say, not
in a patsan way, according to the standard, it is recommended to use Read Committed as the default. I prefer the opposite solution: the default is the most reliable mode, and if you need to improve performance or overcome deadlock, you can always switch to a softer mode.
By the way, you cannot change the Isolation Level during a transaction.
TransactionScopeOption
Enum TransactionScopeOption contains three values: Requires, RequiresNew, Suppress, which define the behavior when entering the TransactionScope block. The behavior in all possible cases of TransactionScopeOption is perfectly described in
msdn , and I will only summarize:
- Requires (default) requires a transaction. When entering the block, either the transaction of the parent TransactionScope will be used (if any), or a new transaction will be created.
- RequiresNew always requires creating a new transaction
- Suppress executes block code outside transaction
Note that in the RequiresNew and Suppress modes, any TransactionScope is rudimentary, whereas in the Requires mode, you can use nested TransactionScope. Nested TransactionScope is visually very similar to classic nested transactions (known to MySQL fans as Savepoints). But this is a false analogy, and the following example explains why:
public void Method1() { using (var transactionScope1 = new TransactionScope(TransactionScopeOption.Requires)) { Method2(); transactionScope1.Complete(); } } public void Method2() { using (var transactionScope2 = new TransactionScope(TransactionScopeOption.Requires)) {
Notice that in Method2 we did not call transactionScope2.Complete, which means transactionScope2 will be rolled back. In the case of classic nested transactions, we can roll back the internal transaction without rolling back the transaction. Here, both TransactionScope work within the same transaction, which means if at least one of the internal transactionScope does not call Complete, the transaction will be marked for rollback, and when you exit the root TransactionScope, a rollback will occur (commit / rollback transactions always happen when you exit the root TransactionScope ). Moreover, if you try to call Complete (as in Method1) in a transactional TransactionScope, and the transaction has already been marked for rollback, a TransactionAbortedException will be thrown out.
Regarding the nested TransactionScope there is one unpleasant feature: at the time of writing the code, we do not know whether the Complete of the current TransactionScope will mean a commit transaction. Suppose we have the following method:
public void TransactionMethod(TransactionScopeOption.Requires) { using (var transactionScope = new TransactionScope(TransactionScopeOptions.Requires)) { ... transactionScope.Complete(); }
and the method that calls it:
public void CallingMethod1() {
And everything would be fine, but over time, a higher-level service appears that calls TransactionMethod from its internal TransactionScope:
public void CallingMethod1() {
And here the transactionScope.Complete () call inside the TransactionMethod no longer leads to a transaction commit, which means that the underlying logic associated with the fact that the commit commit has already occurred will fail.
Although, in fairness, it is worth noting that the situation described is quite specific, and, as a rule, the developer doesn’t care whether the commit will occur when exiting the current transactionScope or one of the overlying ones.
Now it's time to pay attention to the two other values of TransactionScopeOption: RequiresNew and Suppress. I rarely had to use these modes. Moreover, if I am not mistaken, I did it only once, and just when solving the problem described in the previous
article .
The question of the use or non-use of RequiresNew and Suppress is certainly determined by the requirements of the algorithm, but I have some biases about this. The fact is that TransactionScope in the RequiresNew and Suppress modes in the presence of operations modifying the database state makes it impossible to use the old trick when the integration test code is in a transaction that rolls back after the test, thereby restoring the database state:
[Test] public void void IntegrationTest() { using (new TransactionScope()) {
If TransactionScope in the Requires mode is created in the test code, they will be hooked to the test TransactionScope, which means we can roll back all changes. If the code has TransactionScope in the RequiresNew or Suppress mode, then we cannot roll back the result of their work from the test TransactionScope. It is worth noting that the presence of logic, tied at the time of the commit transaction (as in the previous example), also makes it impossible to use this technique.
Finally, I note that TransactionScope is local to the stream (because its implementation is based on a ThreadStatic variable). If you need to use one transaction from several threads, use the
DependentTransaction class.
Here, perhaps, that's all. TransactionScope is beautiful, but cunning - don't forget about it :)