Click here to Skip to main content
Click here to Skip to main content

C#: Avoiding DTC with Typed DataSets and SQL Server 2000

By , 16 Nov 2006
 

Introduction

This article describes a means of enabling transaction support with typed datasets in C# 2.0 and SQL Server 2000, without resorting to System.Transactions.TransationScope and therefore Distributed Transaction Coordination.

Background

I created an application for a customer that needed to import data from a spreadsheet into our SQL Server 2000 database. I wanted this performed in a transaction, but found that the only method appeared to be to use TransactionScope, which would require DTC to be present, and would use more resources than necessary. I needed a simple way to enhance the DataSet code generated by the Visual Studio 2005 DataSet editor, so that I could enlist the generated data adapters in a transaction.

I found that because the generated adapters inherit System.ComponentModel.Component instead of DataAdapter, I didn't have access to the UpdteCommand, InsertCommand, or DeleteCommand in order to set the transaction property of each. I had to find another way.

The Problem in Code

You simply can't do the following:

using (SqlTransaction transaction = connection.BeginTransaction())
{
    // The following won’t compile because adapter1 and adapter2 
    // inherit from System.ComponentModel.Component, not DataAdapter
    adapter1.UpdateCommand.Transaction = transaction;
    adapter2.UpdateCommand.Transaction = transaction;
    // Also set transaction on insert and delete commands here

    adapter1.Update(table1);
    adapter2.Update(table2);

    transaction.Commit();
}

I could have used System.Transactions, which makes things much simpler:

using (TransactionScope transaction = new TransactionScope())
{
    adapter1.Update(table1);
    adapter2.Update(table2);

    transaction.Complete();
}

But since we're using SQL Server 2000, this will be automatically promoted to a Distributed Transaction, requiring the Distributed Transaction Coordinator service to be running on the server, and using up more resources in the process. SQL Server 2005 has support for working out whether a transaction should be promoted to a distributed transaction or not, so when we are using 2005, System.Transactions.TransactionScope should probably be the way to go since it’s simpler, and has no additional overhead.

The Solution in Code

I couldn't find a documented method other than System.Transactions to solve this, so I got round it by extending the VS-generated dataset so that each adapter for which I wanted transaction control had an EnlistTransaction method. This is simply adding a back door to the Command objects. I basically made the generated DataSet a partial class and added the new methods in the newly created file, so that regeneration by adding columns, etc. won’t break it.

My first example now becomes:

using (SqlTransaction transaction = connection.BeginTransaction())
{
       // These methods will update all relevant command objects’ transaction property
       adapter1.EnlistTransaction(transaction);
       adapter2.EnlistTransaction(transaction);
 
       adapter1.Update(table1);
       adapter2.Update(table2);
 
       transaction.Commit();
}

Points of Interest

Here are the actual steps to modifying the generated dataset. The trick is to make it a partial class, as follows:

  1. Select your dataset in the Solution Explorer, and click View Code. This will automatically create the additional code file.
  2. Create the partial classes for the dataadapters you wish to provide transaction support to.
  3. Add the following code (replace names as necessary):
    public partial class [TableAdapterName]
    {
        public void EnlistTransaction(System.Data.SqlClient.SqlTransaction transaction)
        {
            System.Data.SqlClient.SqlTransaction _transaction;
            
            if (this._transaction != null)
            {
                throw new System.InvalidOperationException
    		("This adapter has already been enlisted in a transaction");
            }
            else
            {
                this._transaction = transaction;
                Adapter.UpdateCommand.Transaction = _transaction;
                Adapter.InsertCommand.Transaction = _transaction;
                Adapter.DeleteCommand.Transaction = _transaction;
            }
        }
    }

And now you're set!

History

  • 16th November, 2006: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Neil Barnwell
Web Developer
United Kingdom United Kingdom
Member
I started out as a Visual Basic 6 hobbyist, before moving into Java after I decided (rightly or wrongly) that it would be a better move than C++. Eventually though, I settled on .NET and am now a C# 2.0 developer for a logistics firm in the UK, working with a mixture of desktop and web environments.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralTransaction for SELECTmemberJohannesAbt5 Mar '08 - 6:32 
You should set the transaction for SELECT, too.
GeneralBugmemberdojohansen5 Mar '07 - 3:54 
Hi,
 
there are two things that immediately jump out at me:
 
1) The variable in the "enlist" method referring to the transaction should not be local, but an instance member. Move the declaration out of the method.
 
2) Your choice to use a partial class leads to one copy of this code for every typed dataset you want to enable transactions for.
 
The second point is not totally straightforward to address, but if you can accept a slight amount of web-server overhead, you can do it with reflection. You'll be able to access the protected members (private ones too, but private reflection is a bad practice since there is no "contract" (public API) and library developers are free to remove or otherwise change private members without any code compatibility constraints).
 
Reflection would allow you to put your enlisting code in a utility class, and eliminate the need to extend each adapter individually. The code for using it would then become something like
 
using (DbTransaction transaction = connection.BeginTransaction())
{
SqlUtil.EnlistTransaction(adapter1, transaction);
SqlUtil.EnlistTransaction(adapter2, transaction)
adapter1.Update();
adapter2.Update();
transaction.Commit();
}
 
Dag
GeneralRe: Bugmemberdojohansen5 Mar '07 - 3:57 
> if you can accept a slight amount of *web server* overhead
 
Ooops. Not all apps are web apps of course. I mean the CLR app, as opposed to the database - seeing as the point with this whole exercise was to avoid the rather larger overhead of involving the DTC.
GeneralReply to NormmemberShipcreak22 Jan '07 - 1:13 
A user left this comment (don't know why I can't see it, so I've posted it here):
 
"I like your solution, but whenever I create a partial class of the designer generated dataset, the command objects and adapters are not visible for the class methods that I am writing. Even though I use VB.Net, shouldn't the same object properties be available that you are accessing in C# ?
 
Norm"
 
How are you creating the partial class? Don't create it manually, as that won't set the designer class to partial. Instead, double-click the DataSet icon in the solution explorer (or select it, and click "view code").
 
This will create a new .vb file, with the partial class definition prepared and ready for you to add your functionality.
 
Hope this helps.
GeneralRe: Reply to Normmemberliscio11 May '07 - 4:56 
i try to do it, but it opens a "xml" file, not C# code.
the xml file that opens, is named "DataSet1.xsd".
what's the problem?
GeneralRe: Reply to Normmemberjcrussell8 Oct '07 - 14:59 
I tried doing exactly this (double clicking on my dataset)
 
I converted the code to vb.net

Public Sub EnlistTransaction(ByVal transaction As System.Data.SqlClient.SqlTransaction)
Dim _transaction As System.Data.SqlClient.SqlTransaction
 
If Not (_transaction Is Nothing) Then
Throw New System.InvalidOperationException("This adapter has already been enlisted in a transaction")
Else
_transaction = transaction

Adapter.UpdateCommand.Transaction = _transaction
Adapter.InsertCommand.Transaction = _transaction
Adapter.DeleteCommand.Transaction = _transaction

End If
End Sub

 

The underlined code returns an error: Name 'Adapter' is not declared
Does anyone know why the properties of the partial class is not being picked up?
 

 

 
Jason

General...or you can do thismemberStefan Knoll23 Nov '06 - 4:03 
public static OracleDataAdapter GetAdapter( Component TableAdapter )
{
PropertyInfo Info = TableAdapter.GetType().GetProperty( "Adapter",
BindingFlags.Instance | BindingFlags.NonPublic );
 
if( Info != null )
return Info.GetValue( TableAdapter, null ) as OracleDataAdapter;
else
return null;
}
 
It work for any TableAdapters, but of course if Microsoft changes the designer code....
 
Stefan

GeneralRe: ...or you can do thismemberdojohansen5 Mar '07 - 4:03 
This was precisely my idea - make a utility class and work around the fact the embedded adapter is a protected member by using reflection.
 
As for the "if MS changes the designer code", simply extending the class is precisely as suceptible to breaking due to such a change. In both cases, the dependency is there must be a member
 
protected DataAdapter Adapter { get; }
 
available.
 
Reflection incurs a run-time overhead though. "Private reflection" - using reflection to access private members - is, as a rule of thumb, a bad programming practice that one should require special justification for.
GeneralAnother way...memberpatgrape16 Nov '06 - 15:59 
If generating your typed dataset in the dataset designer you can also set the connectionmodifier property of each tableadapter (in the designer properties window) to Public. (If you don't mind them being public). Then the designer builds a public Connection member for the autogenerated code for these tableadapter classes and you can set each tableadapter's connection to some connection you instantiate and open at runtime. My understanding is that as the exact same connection is used in sequence for each table adapter while their Update methods are called, it doesn't attempt to enlist in DTC transaction. However, instantiating 2 individual connections (i.e. with the same connection string) and giving each tableadapter one of them to use (inside a single transactionscope) does not work - it's not smart enough to go and check whether these 2 connections refer to the same "place" or not. I saw this suggested a few places and it worked for me when I had this issue; updates succeeded when I subsequently stopped my dtc service.
 
Pat

GeneralRe: Another way...memberfhtino11 Oct '07 - 20:57 
I confirm that. If you set the same connection to all the tableadapters, the transaction is not elevated (no DTS is needed).
 
A piece of code (it works without DTS, it's managed as a "local transaction" and not a "distributed transaction"):
 

using (TransactionScope ts = new TransactionScope())
{
using (SqlConnection sqlConn = new SqlConnection(Properties.Settings.Default.FabryConnectionString))
{
sqlConn.Open();
PersoneTableAdapter pta = new PersoneTableAdapter();
OggettiTableAdapter ota = new OggettiTableAdapter();
pta.Connection = sqlConn;
ota.Connection = sqlConn;
MyDS.PersoneDataTable persone = pta.GetAllPersone();
MyDS.OggettiDataTable oggetti = ota.GetData();
persone[0].Cognome = "XXXXXXX_" + DateTime.Now.ToString();
oggetti[0].NomeOggetto = "NomeOggetto_" + DateTime.Now.ToString();
pta.Update(persone);
ota.Update(oggetti);
sqlConn.Close();
}
ts.Complete();
}
 
Fabrizio

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130523.1 | Last Updated 16 Nov 2006
Article Copyright 2006 by Neil Barnwell
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid