2

Suppose you set up a TransactionScope object as illustrated per the Microsoft example here. Now suppose that you need to update a lot of database tables, and you want them all in the scope of the TransactionScope object. Continually nesting SqlConnection and SqlCommand objects 10 deep will create a source code mess. If instead you call other functions which create connections (in your data access layer, for example), will they be within scope of the TransactionScope object?

Example:

' Assume variable "x" is a business object declared and populated with data.
Using scope As New TransactionScope()

    Dal.Foo.SaveProducts(x.Products)
    Dal.Foo.SaveCustomer(x.Customer)
    Dal.Foo.SaveDetails(x.Details) 
    ' more DAL calls ...
    Dal.Foo.SaveSomethingElse(x.SomethingElse)

    scope.Complete()

End Using

Assume that each DAL function contains its own using statements for connections. Example:

Public Shared Sub SaveProducts(x As Object)

    Using conn As New SqlConnection("connection string")

        Using cmd As New SqlCommand("stored procedure name", conn)

            With cmd

                ' etc.                        

            End With

        End Using

    End Using

End Sub
HardCode
  • 6,497
  • 4
  • 31
  • 54

2 Answers2

4

Yes, they will be inside the TransactionScope. What the TransactionScope basically does is to create a Transaction object and set Transaction.Current to that.

In other words, this:

Using scope As New TransactionScope()
    ... blah blah blah ...
End Using

is basically the same as this:

try
{
    // Transaction.Current is a thread-static field
    Transaction.Current = new CommittableTransaction();
    ... blah blah blah ...
}
finally
{
    Transaction.Current.Commit(); // or Rollback(), depending on whether the scope was completed
    Transaction.Current = null;
}

When a SqlConnection is opened, it checks if Transaction.Current (on this thread) is null or not, and if it is not null then it enlists (unless enlist=false in the connection string). So this means that SqlConnection.Open() doesn't know or care if the TransactionScope was opened in this method or a method that called this one.

(Note that if you wanted the SqlConnection in the child methods to NOT be in a transaction, you can make an inner TransactionScope with TransactionScopeOption.Suppress)

Jared Moore
  • 3,765
  • 26
  • 31
2

When you create a TransactionScope, all connections you open while the TransactionScope exists join the transaction automatically (they're 'auto enlisted'). So you don't need to pass connection strings around.

You may still want to, when SQL Server sees different transactions (even if they are all contained by one DTC transaction), it doesn't share locks between them. If you open too many connections and do a lot of reading and writing, you're headed for a deadlock.

Why not put the active connection in some global place and use it?

Some more info after some research. Read this: TransactionScope automatically escalating to MSDTC on some machines? .

If you're using SQL Server 2008 (and probably 2012, but not any other database), some magic is done behind the scenes, and if you open two SQL Connections one after the other, they are going to be united into a single SQL transaction, and you're not going to have any locking problem.

However, if you're using a different database, or you may open two connections concurrently, you will get a DTC transaction, which means SQL Server will not manage the locks properly, and you may encounter very unpleasant and unexpected deadlocks.

While it's easy to make sure you're only running on SQL Server 2008, making sure you don't open two connections at the same time is a bit harder. It's very easy to forget it and do something like this:

class MyPersistentObject
{
    public void Persist()
    {
         using(SQLConnection conn=...)
         {
             conn.Open()
             WriteOurStuff()
             foreach(var child in this.PersistedChildren)
                 child.Persist()
             WriteLogMessage()
         }
    }
}

If the child's Persist method opens another connection, your transaction is escalated into a DTC transaction and you're facing potential locking issues.

So I still suggest maintaining the connection in one place and using it through your DAL. It doesn't have to be a simple global static variable, you can create a simple ConnectionManager class with a ConnectionManager.Current property which will hold the current connection. Make ConnectionManager.Current as [ThreadStatic] and you solved most of your potential problems. That's exactly how the TransactionScope works behind the scenes.

Community
  • 1
  • 1
zmbq
  • 38,013
  • 14
  • 101
  • 171
  • Putting the active connection in a global place is generally a BAD idea. Open and close connections as you need them. As long as they all use the exact same connection string, the connection will be pooled and reused in a manner that is thread-safe and reliable. – Jared Moore Nov 14 '11 at 22:12
  • Also the DTC transaction only creates one SQL transaction per SQL Server that is participating. So if you create many connections to the same SQL Server within a DTC transaction, that will only create one SQL transaction. – Jared Moore Nov 14 '11 at 22:24
  • There's nothing wrong in putting the connection in a property in the object that has all those methods, if it's one object. You can also put one thread-specific connection in one global place (exactly the same way Transaction.Current is handled). I'm not sure which version of SQL Server unites different transactions as one, but older versions certainly did, and there's no telling what non-SQL Server databases will do. – zmbq Nov 15 '11 at 05:53
  • Also, take a look at this: http://stackoverflow.com/questions/1690892/transactionscope-automatically-escalating-to-msdtc-on-some-machines . If you have two open connections at the same time, a DTC transaction will be opened, and SQL Server will not share locks between the two transactions. So I agree that if you are sure you're only going to run on SQL Server 2008, and make certain your code never opens a connection, calls another method that opens another connection and then closes the first connection, you don't have to manage connections yourself. That 'making certain' may be problematic. – zmbq Nov 15 '11 at 05:59
  • General practice is to open and close connections as you need them and not cache a connection yourself, e.g. http://stackoverflow.com/questions/2877692/passing-around-a-sqlconnection/2878449 . It can be safe but it can lead to bugs. Your mileage may vary. – Jared Moore Nov 15 '11 at 20:30
  • You are right that 2 connections open at the same time will always escalate to DTC, but if you Open-Close-Open-Close, i.e. 2 connections with the same connection string but NOT open at the same time, and connecting to SQL Server 2008 or later (sorry I forgot to mention this) then the same internal connection will be reused and the transaction will not be promoted to DTC. If you are using SQL Server version lower than 2008, then caching the connection yourself as you suggest could be a good workaround. Sorry if my initial response sounded a bit uncompromising :) – Jared Moore Nov 15 '11 at 20:33
  • I see that you've updated your answer with some useful additional information, +1'd :) – Jared Moore Nov 15 '11 at 20:36