Use Command object to manage transaction across root objects

Use Command object to manage transaction across root objects

Old forum URL: forums.lhotka.net/forums/t/980.aspx


albator posted on Tuesday, August 22, 2006

Hi,

We are using CSLA Command object to save multiple root objects in a same ADO.Net transaction.

We can't use System transaction because we are dialing with Oracle...

We made our transaction traveling by using LocalContext variable to store the current connection/transaction.

This work fine, the only difficulty is to pass PK to child objects.

We are planning to develop a CodeSmith template that will help us generate this kind of command object.

This is a sample code of a command object managing transaction:

[Serializable]

public class TransactionCommand: CommandBase,ICloneable

{

#region Authorization Methods

public static bool CanExecuteCommand()

{

// TODO: customize to check user role

//return ApplicationContext.User.IsInRole("");

return true;

}

#endregion

#region Client-side Code

private PublicationCollection _pubs;

private Order _currentOrder;

public PublicationCollection Pubs //PublicationCollection Pubs

{

get

{

return _pubs;

}

set

{

_pubs = value;

}

}

public Order CurrentOrder

{

get

{

return _currentOrder;

}

set

{

_currentOrder = value;

}

}

#endregion

#region Factory Methods

public static TransactionCommand NewTransactionCommand()

{

return new TransactionCommand();

}

public TransactionCommand Execute()

{

//if (Pubs != null)

// Pubs[0].Shown += new EventHandler<EventArgs>(TransactionCommand_Shown);

TransactionCommand lClonedCommand = ((ICloneable)this).Clone() as TransactionCommand;

return DataPortal.Execute<TransactionCommand>(lClonedCommand);

}

void TransactionCommand_Shown(object sender, EventArgs e)

{

throw new Exception("The method or operation is not implemented.");

}

private TransactionCommand()

{ /* require use of factory methods */ }

#endregion

#region Server-side Code

protected override void DataPortal_Execute()

{

Database lDb = DatabaseFactory.CreateDatabase();

DbConnection lConnection = lDb.CreateConnection();

lConnection.Open();

using (DBConnectionScope ldbScope = new DBConnectionScope("default", lConnection))

{

ldbScope.BeginTransaction("default");

try

{

if(_currentOrder != null)

_currentOrder = _currentOrder.Save();

if (_pubs != null)

_pubs = _pubs.Save();

}

catch

{

ldbScope.RollBack();

throw;

}

ldbScope.Commit();

}

}

#endregion

#region ICloneable

object ICloneable.Clone()

{

using (MemoryStream buffer = new MemoryStream())

{

BinaryFormatter formatter = new BinaryFormatter();

formatter.Serialize(buffer, this);

buffer.Position = 0;

object temp = formatter.Deserialize(buffer);

return temp;

}

}

#endregion

}

Do you have suggestion of how make this more easier for complexe related business objects?

Is there somebody that has already worked on that king of object?

Somebody has already written a codesmith template?

Is there something in CSLA 2.1 that can make this easier?

Thanks

Alain Martineau

 

Brian Criswell replied on Tuesday, August 22, 2006

Unfortunately, I do not have any ideas for what you are attempting, but I have tried to solve this issue before CSLA.NET 2.0 came out.  I had an static class that would allow my objects to share a connection and transaction.  I have developed this idea further, but have not had a chance to code it.  My idea was to create a own wrapper class for a connection similarly to how SafeDataReader wraps and delegates operations to a DataReader.  The class would have a static connection and a static reference count.  Whenever, you created a new instance of the class it would check to see if the internal static connection was null or disposed (checked by seeing if the connection string was empty) and create a new static connection if necessary and increase the reference count by one.  Disposing of a SafeConnection would decrease the reference count by one.  When the reference count reached zero again, it would dispose the internal connection.  Calling SafeConnection.BeginTransaction() would begin a transaction on the internal connection and optionally attach the transaction to all created command objects.  Anyway, you probably get the idea.  The upside is that you can use it the same way you use a regular connection object, but you get automatic sharing of the connection and transactions without having to jump through hoops.

JZuerlein replied on Tuesday, August 22, 2006

I found an interesting Transaction Manager class in a book by James Newkirk.  "Test-Driven Development in Microsoft.Net"  It associates the transaction with the current thread, which might be a problem for you, but it's very straight forward.  It was great for wrapping a bunch of unit tests around a transaction.

The source code is at...

http://workspaces.gotdotnet.com/tdd

RockfordLhotka replied on Tuesday, August 22, 2006

Along this same line, in version 2.1 I am adding ApplicationContext.LocalContext - which is a collection of arbitrary values that is not moved between client and app server. It is also thread-specific, like the rest of ApplicationContext - and the goal behind it is to allow you (on the app server) to put your Transaction or Connection object (or other context data) into LocalContext so it is available to all other objects as they run on the app server.

You could, therefore, use your command object idea. The first thing DataPortal_Execute() would do on the server is set things into LocalContext - like your open Connection, perhaps your parent object or just the PK value - whatever makes sense. Those values automatically then become global to all subsequent code on the app server.

If you do this right, you can use a using statement for your connection:

protected override void DataPortal_Execute()
{
  // create/open connection and transaction
  ApplicationContext.LocalContext["cn"] = cn;
  ApplicationContext.LocalContext["cn"] = tr;
  using (cn)
  {
    try
    {
      // call your other objects here
      tr.Commit();
    }
    catch (Execption ex)
    {
      tr.Rollback();
      throw;
    }
  }
}

Or something like that anyway. Hopefully you get the idea.

Even if you are using 2.0 you can do this - just use ClientContext. It only flows from client to server, and so isn't brought back from the server. In fact, the data portal clears it when your call to the server is complete, so it is auto-cleaning. In essense it does what LocalContext will do for you already.

Keep in mind that this auto-cleaning only occurs with a remote data portal. If you are using a local data portal you MUST make sure to remove your connection/transaction references from LocalContext or ClientContext in a finally block!!

Copyright (c) Marimer LLC