Ryan Whitaker

Dishes of Ryan

Transactions with TableAdapters, a lazy man's approach

If you've used TableAdapters, you'll know that their ability to deal with transactions leaves a little to be desired.  Little?  Did I say "little"?  I mean "a lot".

Sahil Malik has a good slew of advice for people wanting to use transactions with TableAdapters.  Most people will probably just wrap everything up in a TransactionScope and be done with it, which is fine if you're not running a high-traffic site.  In doing this, you'll run into the annoyance of the transaction being promoted to the DTC, which is an expensive bit of "bling" to tango with.

For the people that don't like the whole promotion to DTC thing, they'll probably extend their TableAdapter's partial class and add a BeginTransaction method similar to what Sahil proposes.

For me, who's looking at a bajillion TableAdapters, with quite a few of those needing to operate within the scope of a transaction, I instead choose to play the Hacky card and just set the transaction on the TableAdapter's commands through a little bit of reflection.  Some may throw your hands up in the air on this.  I, however, love it, as it fits my purposes like a latex glove.

The code follows for my so-called TableAdapterHelper.

using System;
using System.Data;
using System.Data.SqlClient;
using System.Reflection;

public class TableAdapterHelper
{
public static SqlTransaction BeginTransaction(object tableAdapter)
{
    return BeginTransaction(tableAdapter, IsolationLevel.ReadUncommitted);
}

public static SqlTransaction BeginTransaction(object tableAdapter, IsolationLevel isolationLevel)
{
    // get the table adapter's type
    Type type = tableAdapter.GetType();

    // get the connection on the adapter
    SqlConnection connection = GetConnection(tableAdapter);

    // make sure connection is open to start the transaction
    if (connection.State == ConnectionState.Closed)
        
connection.Open();

    // start a transaction on the connection
    SqlTransaction transaction = connection.BeginTransaction(isolationLevel);

    // set the transaction on the table adapter
    SetTransaction(tableAdapter, transaction);

    return transaction;
}

/// <summary>
/// Gets the connection from the specified table adapter.
/// </summary>
private static SqlConnection GetConnection(object tableAdapter)
{
    Type type = tableAdapter.GetType();
    PropertyInfo connectionProperty = type.GetProperty("Connection", BindingFlags.NonPublic | BindingFlags.Instance);
    SqlConnection connection = (SqlConnection)connectionProperty.GetValue(tableAdapter, null);
    return connection;
}

/// <summary>
/// Sets the connection on the specified table adapter.
/// </summary>
private static void SetConnection(object tableAdapter, SqlConnection connection)
{
    Type type = tableAdapter.GetType();
    PropertyInfo connectionProperty = type.GetProperty("Connection", BindingFlags.NonPublic | BindingFlags.Instance);
    connectionProperty.SetValue(tableAdapter, connection, null);
}

/// <summary>
/// Enlists the table adapter in a transaction.
/// </summary>
public static void SetTransaction(object tableAdapter, SqlTransaction transaction)
{
    // get the table adapter's type
    Type type = tableAdapter.GetType();

    // set the transaction on each command in the adapter
    PropertyInfo commandsProperty = type.GetProperty("CommandCollection", BindingFlags.NonPublic | BindingFlags.Instance);
    SqlCommand[] commands = (SqlCommand[])commandsProperty.GetValue(tableAdapter, null);
    foreach (SqlCommand command in commands)
         command.Transaction = transaction;

    // set the connection on the table adapter
    SetConnection(tableAdapter, transaction.Connection);
}
}

Here's some example usage of the helper methods above.  In this example, we have some work that spans two different adapters that we need to enclose in a transaction.

SqlTransaction transaction = null;

try
{
    using (FooTableAdapter fooAdapter = new FooTableAdapter())
    {
        transaction = TableAdapterHelper.BeginTransaction(fooAdapter);
        fooAdapter.DoSomething();
    }

    using (BarTableAdapter barAdapter = new BarTableAdapter())
    {
        TableAdapterHelper.SetTransaction(barAdapter, transaction);
        barAdapter.DoSomething();
    }

    transaction.Commit();
}
catch
{
    transaction.Rollback();
    throw;
}
finally
{
    transaction.Dispose();
}

So, what are the disadvantages here?  We're using reflection so there's no compile-time checking nor is there any strong-typing action going on.  Using reflection is obviously slower execution-wise than extending your TableAdapter's class.  Also, the code-gen implementation of your TableAdapter class could theoretically change if VS2005 gets patched in the future and would therefore cause your TableAdapterHelper to break.

What are the advantages?  Compile-time checking, schmompile-schmime checking.  Reflection is eons faster than a promotion to DTC, and this approach is certainly faster than extending your TableAdapter classes by hand.  And, the code-gen implementation of your TableAdapter class is unlikely to change.

Posted: Mar 30 2006, 03:14 PM by RyanW | with 31 comment(s) |
Filed under:

Comments

Mike H. said:

Pretty sneaky, I like it. It's great that you've added another option to this whole business.

Just in case anyone runs into the same thing I did, you may need to add queries to your TableAdapter. For example, I wanted to delete within a transaction. I needed to add a new delete query and couldn't just use the automatically generated .Delete method.

When I tried the .Delete way, I got an exception "ExecuteNonQuery requires the command to have a transaction when the connection assigned to the command is in a pending local transaction. The Transaction property of the command has not been initialized." Adding a delete query and using the new method fixed the problem.

Thanks Ryan!
Mike
# April 11, 2006 4:25 PM

Humpty said:

Thanks so much for this code! I'm using ASP.NET 2.0 with SQL Server 2000 and I noticed that even when I use the TransactonScope class as many examples indicate my transactions are still promoted to the DTC with only one connection! I couldn't believe this. I really like the idea behind TableAdapters as they cut down on the amount of code I have to write, but it seems even the most basic of basic operations trigger promotion to the DTC on SQL Server 2000. I wonder if this is Microsoft's attempt to push people to upgrade to 2005...
# April 18, 2006 3:58 PM

Miroslav Braikov said:

Thank you for that code Sir.

Very helpfull.

# May 28, 2007 4:33 AM

eti said:

Nice trick, but does not work with Trust Level Medium ...

# June 5, 2007 9:14 AM

beti said:

It doesn't work in my project :(

# June 22, 2007 2:53 PM

beti said:

It seems like the problem was in my project. The code works perfectly. Thanks ! :)

# June 24, 2007 7:43 AM

Roberto F. said:

Thanks for the code.

About the problem described by Mike H., it's possible to mantain auto-generated commands adding this code to the SetTransaction method:

// set the transaction on each command in the adapter

PropertyInfo adapterProperty = type.GetProperty("Adapter", BindingFlags.NonPublic | BindingFlags.Instance);

SqlDataAdapter adapter = (SqlDataAdapter)adapterProperty.GetValue(tableAdapter, null);

adapter.UpdateCommand.Transaction = transaction;

adapter.InsertCommand.Transaction = transaction;

adapter.DeleteCommand.Transaction = transaction;

# June 27, 2007 12:15 PM

Martin Worger said:

Excellent. So much in fact, here it is converted to VB (including Roberto F's suggestions)

Imports System

Imports System.Data

Imports System.Data.SqlClient

Imports System.Reflection

'Credit to Ryan Whitaker's Blog at weblogs.asp.net/.../441529.aspx

'Converted to VB by Martin Worger 5th July 2007

'Incorporates Roberto F's suggestion

Public Class clsTableAdapterHelper

   Public Function BeginTransaction(ByVal MyTableAdapter As Object) As SqlTransaction

       Return BeginTransaction(MyTableAdapter, IsolationLevel.ReadUncommitted)

   End Function

   Public Function BeginTransaction(ByVal MyTableAdapter As Object, ByVal MyIsolationLevel As IsolationLevel) As SqlTransaction

       Dim TAType As Type = MyTableAdapter.GetType

       Dim Conn As SqlConnection = GetConnection(MyTableAdapter)

       If Conn.State = ConnectionState.Closed Then

           Conn.Open()

       End If

       Dim Tran As SqlTransaction = Conn.BeginTransaction(MyIsolationLevel)

       SetTransaction(MyTableAdapter, Tran)

       Return Tran

   End Function

   Public Sub SetTransaction(ByVal MyTableAdapter As Object, ByVal MyTransaction As SqlTransaction)

       Dim MyType As Type = MyTableAdapter.GetType

       'Original Ryan method

       'Dim CommandsProperty As PropertyInfo = MyType.GetProperty("CommandCollection", BindingFlags.NonPublic Or BindingFlags.Instance)

       'Dim Commands() As SqlCommand = CType(CommandsProperty.GetValue(MyTableAdapter, Nothing), SqlCommand())

       'For Each Command As SqlCommand In Commands

       '    If Command.CommandText <> "" Then

       '        Command.Transaction = MyTransaction

       '    End If

       'Next Command

       'Roberto F's alternative suggestion...

       Dim AdapterProperty As PropertyInfo = MyType.GetProperty("Adapter", BindingFlags.NonPublic Or BindingFlags.Instance)

       Dim MyDataAdapter As SqlDataAdapter = CType(AdapterProperty.GetValue(MyTableAdapter, Nothing), SqlDataAdapter)

       With MyDataAdapter

           If Not .DeleteCommand Is Nothing Then .DeleteCommand.Transaction = MyTransaction

           If Not .InsertCommand Is Nothing Then .InsertCommand.Transaction = MyTransaction

           If Not .UpdateCommand Is Nothing Then .UpdateCommand.Transaction = MyTransaction

       End With

       SetConnection(MyTableAdapter, MyTransaction.Connection)

   End Sub

   Private Function GetConnection(ByVal MyTableAdapter As Object) As SqlConnection

       Dim MyType As Type = MyTableAdapter.GetType

       Dim ConnProperty As PropertyInfo = MyType.GetProperty("Connection", BindingFlags.NonPublic Or BindingFlags.Instance)

       Dim Conn As SqlConnection = CType(ConnProperty.GetValue(MyTableAdapter, Nothing), SqlConnection)

       Return Conn

   End Function

   Private Sub SetConnection(ByVal MyTableAdapter As Object, ByVal MyConnection As SqlConnection)

       Dim MyType As Type = MyTableAdapter.GetType

       Dim ConnProperty As PropertyInfo = MyType.GetProperty("Connection", BindingFlags.NonPublic Or BindingFlags.Instance)

       ConnProperty.SetValue(MyTableAdapter, MyConnection, Nothing)

   End Sub

End Class

# July 5, 2007 12:12 PM

Damo said:

A great article - many thanks!

Incidentally, I am not sure why but I had to modify the SetTransaction sub  to get it to work for all my custom queries, otherwise I ran into the error mentioned by Mike H above.

Anyway, if anyone else has the same problem, this fixed it:

   Public Shared Sub SetTransaction(ByVal MyTableAdapter As Object, ByVal MyTransaction As SqlTransaction)

       Dim MyType As Type = MyTableAdapter.GetType

       Dim AdapterProperty As PropertyInfo = MyType.GetProperty("Adapter", BindingFlags.NonPublic Or BindingFlags.Instance)

       Dim MyDataAdapter As SqlDataAdapter = CType(AdapterProperty.GetValue(MyTableAdapter, Nothing), SqlDataAdapter)

       Dim CommandsProperty As PropertyInfo = MyType.GetProperty("CommandCollection", BindingFlags.NonPublic Or BindingFlags.Instance)

       Dim MyCommands As SqlCommand() = CType(CommandsProperty.GetValue(MyTableAdapter, Nothing), SqlCommand())

       Dim intPtr As Integer

       'Set the transaction on the default commands

       With MyDataAdapter

           If Not .DeleteCommand Is Nothing Then

               .DeleteCommand.Transaction = MyTransaction

           End If

           If Not .InsertCommand Is Nothing Then

               .InsertCommand.Transaction = MyTransaction

           End If

           If Not .UpdateCommand Is Nothing Then

               .UpdateCommand.Transaction = MyTransaction

           End If

       End With

       'Set the transaction on the custom commands

       For intPtr = 0 To MyCommands.Length - 1

           MyCommands(intPtr).Transaction = MyTransaction

       Next

       SetConnection(MyTableAdapter, MyTransaction.Connection)

   End Sub

# July 12, 2007 9:57 AM

sy said:

Damo's VB conversion of July 12, 2007 9:57 AM worked wonderfully, but I had to remove the Shared modifier.  I had the same error with custom queries, even the wizard-generated updates.

# July 17, 2007 6:25 PM

Jaime said:

One quick point... using the transactions in this way (and I believe is a vert good way), can cause a connection leak in the TableAdapter... since the table adapter did not open the connection, it will not close it when it goes out of scope (nor when its Dispose method is called). A quick way to solve this is changing the finally section to:

finally {

  transaction.Connection.Close();

  transaction.Dispose();

}

# August 6, 2007 5:44 PM

John said:

Another way to ensure the connection is closed is to open it prior to setting the transaction:

using (SqlConnection connection = fooAdapter.Connection)

{

connection.Open();

using (SqlTransaction transaction = TableAdapterHelper.BeginTransaction(fooAdapter))

{

fooAdapter.DoSomething();

}

transaction.Commit();

}

It is also a good idea to check for the existance of the Select, Insert, Update, and Delete commands on the adapter before trying to set their transactions in TableAdapterHelper.SetTransaction:

SqlDataAdapter adapter = (SqlDataAdapter)adapterProperty.GetValue(tableAdapter, null);

if(adapter.SelectCommand != null)

adapter.SelectCommand.Transaction = transaction;

if (adapter.UpdateCommand != null)

adapter.UpdateCommand.Transaction = transaction;

if (adapter.InsertCommand != null)

adapter.InsertCommand.Transaction = transaction;

if (adapter.DeleteCommand != null)

adapter.DeleteCommand.Transaction = transaction;

# August 15, 2007 10:52 AM

David said:

Thanks for posting this code.

With some slight modification, I have been able to use it with the TransactionScope method

# January 15, 2008 11:14 AM

David said:

           Using transScope As Transactions.TransactionScope = New Transactions.TransactionScope

               Using MyConn As New SqlClient.SqlConnection(Me.CompanyMasterTableAdapter.Connection.ConnectionString)

                   ' open the connection

                   MyConn.Open()

                   Try

                       clsTableAdapterHelper.TransactionalScopeUpdate(Me.CompanyMasterTableAdapter, Me.DsCompanies.CompanyMaster, MyConn)

                       clsTableAdapterHelper.TransactionalScopeUpdate(Me.CompanyAddressTableAdapter, Me.DsCompanies.CompanyAddress, MyConn)

                   Catch ex As Exception

                       ' throw ex back out, trans rollsback

                       Throw New ApplicationException(ex.Message)

                   End Try

               End Using

               ' complete trans if we get this far, indicate true-success

               transScope.Complete()

           End Using

# January 15, 2008 11:18 AM

David said:

   Public Shared Function TransactionalScopeUpdate(ByVal MyTableAdapter As Object, ByVal MyTable As DataTable, ByVal MyTransConnection As SqlConnection) As Boolean

       Dim lResult As Boolean = False

       ' get original connection for the dataadapter..  set this back after updating database

       Dim connOriginal As SqlConnection = GetConnection(MyTableAdapter)

       ' get the SQL Data Adapter

       Dim MyDataAdapter As SqlDataAdapter = CType(GetAdapterProperty(MyTableAdapter.GetType).GetValue(MyTableAdapter, Nothing), SqlDataAdapter)

       Try

           ' set the transaction connection

           Call SetConnection(MyTableAdapter, MyTransConnection)

           ' update our table

           MyDataAdapter.Update(MyTable)

           ' return true if no exception

           lResult = True

       Catch ex As Exception

           Throw New ApplicationException(ex.Message)

       Finally

           ' set connection back to original connection

           Call SetConnection(MyTableAdapter, connOriginal)

       End Try

       Return lResult

   End Function

   Private Shared Sub SetTransactionCommands(ByVal MyTableAdapter As Object, ByVal MyTransaction As SqlTransaction)

       ' Get the SQL Data Adapter and set the default commands

       Dim MyDataAdapter As SqlDataAdapter = CType(GetAdapterProperty(MyTableAdapter.GetType).GetValue(MyTableAdapter, Nothing), SqlDataAdapter)

       With MyDataAdapter

           If .DeleteCommand IsNot Nothing Then

               .DeleteCommand.Transaction = MyTransaction

           End If

           If .InsertCommand IsNot Nothing Then

               .InsertCommand.Transaction = MyTransaction

           End If

           If .UpdateCommand IsNot Nothing Then

               .UpdateCommand.Transaction = MyTransaction

           End If

       End With

       'Set any custom commands

       Dim MyCommands As SqlCommand() = GetMyCommands(MyTableAdapter, GetCommandsProperty(MyTableAdapter.GetType))

       For IntPtr As Integer = 0 To MyCommands.Length - 1

           MyCommands(IntPtr).Transaction = MyTransaction

       Next

   End Sub

   Private Shared Function GetAdapterProperty(ByVal MyType As Type) As PropertyInfo

       Return MyType.GetProperty("Adapter", BindingFlags.NonPublic Or BindingFlags.Instance)

   End Function

   Private Shared Function GetCommandsProperty(ByVal MyType As Type) As PropertyInfo

       Return MyType.GetProperty("CommandCollection", BindingFlags.NonPublic Or BindingFlags.Instance)

   End Function

   Private Shared Function GetConnectionProperty(ByVal MyType As Type) As PropertyInfo

       Return MyType.GetProperty("Connection", BindingFlags.NonPublic Or BindingFlags.Instance)

   End Function

   Private Shared Function GetMyCommands(ByVal MyTableAdapter As Object, ByVal CommandsProperty As PropertyInfo) As SqlCommand()

       Return CType(CommandsProperty.GetValue(MyTableAdapter, Nothing), SqlCommand())

   End Function

   Private Shared Function GetConnection(ByVal MyTableAdapter As Object) As SqlConnection

       ' get the connection

       Return CType(GetConnectionProperty(MyTableAdapter.GetType).GetValue(MyTableAdapter, Nothing), SqlConnection)

   End Function

   Private Shared Sub SetConnection(ByVal MyTableAdapter As Object, ByVal MyConnection As SqlConnection)

       ' get the connection property and set the connection

       GetConnectionProperty(MyTableAdapter.GetType).SetValue(MyTableAdapter, MyConnection, Nothing)

   End Sub

# January 15, 2008 11:25 AM

Ivan Tamsir said:

Thanks A Lot for sharing this code Ryan.

Best Regards.

# January 18, 2008 12:30 PM

Benjamin Peikes said:

It's not clear why you even need to use reflection.

# February 11, 2008 5:19 PM

Mark said:

Because the commands are not accessible from the outside. You have to create a partial class of a tableAdapter to change their transaction but you end up creating partial classes for all your tableadapters

# March 14, 2008 7:47 AM

Matt said:

David, why did you go to all that effort of making it work within a TransactionScope; is that not what we are trying to get rid of?

# March 14, 2008 12:00 PM

cuongnt@bkav.com.vn said:

Thanks a lot for sharing  this code!

# May 8, 2008 10:44 AM

Colby said:

# June 3, 2008 11:21 PM

fabson said:

very nice code ..i am just about trying it out but please can the fooAdapter still dosomething [i.e fooAdapter.dosomthing] after the baradapter has done something. and do i need to call settransaction again if an already used adapter is to be used again...

Thanks

# July 7, 2008 6:57 AM

vikas wadhwa said:

Thanks for the Code. It was the simplest code I found over net. Easy To implement. Gr8 Work.

# September 26, 2008 5:33 AM

grobanderson said:

It seems it doesn't work on Oracle ODBC:

message is

"ERROR [HYC00] [Oracle][ODBC]Optional feature not implemented."

Someone help me!!

# October 29, 2008 7:13 AM

?????????????? ?????????????????????????? … » ?????????? ?????????? » .NET: ???????????????????? ???????????????????????? ?? ???????????????????????????? ???????????????????????????? DataSet. said:

Pingback from  ?????????????? ?????????????????????????? &#8230;  &raquo; ?????????? ??????????   &raquo; .NET: ???????????????????? ???????????????????????? ?? ???????????????????????????? ???????????????????????????? DataSet.

# February 27, 2009 2:17 AM

Matt C said:

Just another addition to the testimonials by other posters, above...

Thank you very much for this code, it's great - exactly what I needed.  I also agree with you when you say this is probably the best way to implement transactions with Table Adapters.

Thanks!

# March 4, 2009 12:22 PM

Matt C said:

Just a quick follow-up...

Is there any reason why, in your TableAdapterHelper class, all the parameters to the functions could not be passed by Reference instead of by Value?  In fact, unless I am missing something, this might even be a preferable approach, to save passing copies of TableAdapter and Transaction objects around all over the place.

Am I right or wrong?

# March 6, 2009 9:44 AM

TD said:

Byval and byref are retarded for VB only.  Byval means nothing with object (ref) types.  Understand the stack vs. heap

# June 10, 2009 2:54 PM

TD said:

Seems like you are going through a lot of trouble to get the connection off the table adapter passed in.  why not just make the connection modifer public and would that not bypass the need to use reflecttion at the TA connection level?  If the connection is public, all you have to do is call the begintrans to get the sql trans object.

As for the internal commands, I don't use them anyway.

Then, I would suggest creating a generic table adapter that all table adapters would inherit from.  You can see the base class property in all table adapters in the designer, just set the base there.

The generic table adapter should provide  a means for setting a transaction to all internal commands.

This would be my approach if it helps...

# June 10, 2009 3:09 PM

Pete said:

Works like a charm!

# August 7, 2009 5:38 AM

Rob Orchiston said:

Yep, this works awesome! Thanks.

# September 16, 2009 2:41 PM
Leave a Comment

(required) 

(required) 

(optional)

(required)