Editable Root Objects Sharing Transaction

Editable Root Objects Sharing Transaction

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


xxxJasonxxx posted on Tuesday, September 09, 2008

A while back I wrote three unrelated "editable root" objects that were used separately.  Now for a new project, I'm reusing those three objects together.  And I just realized that this around I need them to save using the same transaction object.  So if one editable root object's save fails, all three objects will roll back their data changes.  What is the best way to accomplish this?

Thanks,

Jason

Pawz replied on Tuesday, September 09, 2008

Are they related somehow? Child/Parent etc? Or just 3 totally seperate ones.

The problem with wrapping 3 normally standalone business objects in a transaction is that if they try to initiate their own transaction within a transaction, you get the whole Microsoft distributed transaction error (if I'm remembering what it's called correctly. The design 'feature' that makes it ridiculously hard to run multiple nested transactions ;)).

I think the best way to do it is to start the transaction at the top of the stack of functions, open a connection to the database, and pass that connection along down the stack to whatever object needs to run any updates.

rsbaker0 replied on Tuesday, September 09, 2008

This isn't too hard if you manage your transactions manually. Your DataPortal_Update() method initiates a new transaction if none is in progress, otherwise it obtains the existing transaction.

On the "server side" of  the data portal, you can adopt a convention where all your objects implement a Save(TransactionContext transaction) type method for doing the actual work, and you can pass your transaction around as needed. We have done almost arbitrary compounding of object saving using this technique.

alef replied on Wednesday, September 10, 2008

Have a look at the following : How to handle transactions across root objects.

http://forums.lhotka.net/forums/thread/16605.aspx

 

ajj3085 replied on Wednesday, September 10, 2008

Try using the ContextManager or ConnectionManager, for Linq or straight Ado.Net.

JoeFallon1 replied on Wednesday, September 10, 2008

Here is some real code you can use:

http://forums.lhotka.net/forums/thread/9066.aspx

Joe

 

xxxJasonxxx replied on Wednesday, September 10, 2008

Would this a goofy or bad way to do it - or would it be better to do it a different way?

Add a second "Save" method like this...

--------------------------------------------------------------------

Private mobjSharedTransaction As System.Data.SqlClient.SqlTransaction

Public Function SaveWithSharedTransaction(ByVal transaction As System.Data.SqlClient.SqlTransaction) As Order
      mobjSharedTransaction = transaction
      Return MyBase.Save()
End Function

--------------------------------------------------------------------

And then inside of DataPortal_Insert and DataPortal_Update do something like this...

--------------------------------------------------------------------

Dim blnUsingSharedTransaction as Boolean

'...other code...

If mobjSharedTransaction Is Nothing Then
      objTransaction = objConnection.BeginTransaction
      blnUsingSharedTransaction = False
Else
      objTransaction = mobjSharedTransaction
      blnUsingSharedTransaction = True
      mobjTransaction = Nothing
End If

'...other code...

If blnUsingSharedTransaction = False Then
      objTransaction.Commit()
End If
 
Catch ex as Exception
      If blnUsingSharedTransaction = False Then
            objTransaction.Rollback()
      End If
      Throw
End Try

xxxJasonxxx replied on Wednesday, September 10, 2008

Whoops...on second thought my code might not work.  I'm thinking about the remote dataportal jump from client to server.  I might lose my reference to the transaction object, which I'm thinking will get stored on the client.....and the DataPortal_Insert/Update will run on the server.  Am I right?

JoeFallon1 replied on Wednesday, September 10, 2008

Try reading the link I posted for some ideas.

 

ajj3085 replied on Wednesday, September 10, 2008

Check out the ConnectionManager or ContextManager.  This provides an easy way to share connections without having to worry about where the connection is stored.

It helps you in this case because your command object which is saving the root objects will be marked as transactional, and you can use ConnectionManager or ContextManager and do your BO saves within the using block for this.

Your root BOs will need an internal Save method, so that it doesn't try calling the dataportal at all.  The internal save method should just do the data access part and not worry about transactions at all.  Also, since they'll be using the Connection / ContextManager, they'll automatically use the connection your command object opens and hence the transaction you start on that connection.

rsbaker0 replied on Wednesday, September 10, 2008

ajj3085:
...
Your root BOs will need an internal Save method, so that it doesn't try calling the dataportal at all.  The internal save method should just do the data access part and not worry about transactions at all.  Also, since they'll be using the Connection / ContextManager, they'll automatically use the connection your command object opens and hence the transaction you start on that connection.

Yes, this is very important. Also remember that if you are doing any *reading* of data during your transactions, you need to use the transaction object to do the reading, otherwise you can deadlock on yourself.

(This may come for "free" with some of the transaction types supported by CSLA, but if doing manual transactions you definitely have to be aware of this).

xxxJasonxxx replied on Wednesday, September 10, 2008

Please forgive my ignorance....I've apparently discovered some gaps in my knowledge of CSLA.

I found and am re-reading about "ApplicationContext.LocalContext" in the main book and the 2.1 e-book.

However I don't see this "ConnectionManager" or "ContextManager" you guys mentioned -- is this a CSLA framework thing or a .NET Framework thing?

 

sergeyb replied on Wednesday, September 10, 2008

These classes are part of Csla framework itself.  They are in Csla.Data namespace.  They were added as part of 3.0, if I remember correctly.

 

 

Sergey Barskiy

Principal Consultant

office: 678.405.0687 | mobile: 404.388.1899

cid:_2_0648EA840648E85C001BBCB886257279
Microsoft Worldwide Partner of the Year | Custom Development Solutions, Technical Innovation

 

From: xxxJasonxxx [mailto:cslanet@lhotka.net]
Sent: Wednesday, September 10, 2008 2:47 PM
To: Sergey Barskiy
Subject: Re: [CSLA .NET] Editable Root Objects Sharing Transaction

 

Please forgive my ignorance....I've apparently discovered some gaps in my knowledge of CSLA.

I found and am re-reading about "ApplicationContext.LocalContext" in the main book and the 2.1 e-book.

However I don't see this "ConnectionManager" or "ContextManager" you guys mentioned -- is this a CSLA framework thing or a .NET Framework thing?

 



ajj3085 replied on Wednesday, September 10, 2008

Yes, and they should be pretty easy to back port to version 2 of Csla.

xxxJasonxxx replied on Wednesday, September 10, 2008

Actually it looks like the "ContextManager" and "ConnectionManager" were not added until version 3.5.  So it's not covered in the original book or two e-books (2.1 & 3.0) that I've been looking at. 

ajj3085 replied on Thursday, September 11, 2008

Possibly.  But download the 3.5 source then, and check out the code.  I don't think there's anything that prevents you from backporting the code.  I doubt you'd have to do anything at all except copy the file into your Csla assembly.  May not even have to do that.. it might be able to live in one of your assemblies.

Using the manager is easy.

ContextManager<MyDataContext> mgr;

using( mgr = ContextManager<MyDataContext>.GetManager() {
    // do data access
}

Use that everywhere you need to use a DataContxext (Or IDBConnection) and the manager handles opening or re-using the existing connection for you.

xxxJasonxxx replied on Thursday, September 11, 2008

Ok, I've downloaded 3.5 and I've been looking at the code inside the "ConnectionManager" class.  I agree this looks like a really slick way to share a connection or transaction.

However, I have one major concern...
As the ConnectionManager shares the connection/transaction among multiple objects, how can each object determine whether it is the originator or owner of the shared connection/transaction?  I think they will need to know that for deciding whether to call "Commit" or "Rollback".  Am I thinking correctly?

- - - - -

Related to that, if you look at the link Joe Fallon posted above...
         http://forums.lhotka.net/forums/thread/9066.aspx
... it looks like his code posted towards the bottom of the conversation is trying to address this problem of how to determine which object originated the connection/transaction when he does this.....
         If LocalContext.Item("StartedTrans") Is BO Then
...but I'm honestly not following what is happening here (what is BO or Core.IBusinessObject and what is this actually comparing). 

Can anyone offer any insight?

JoeFallon1 replied on Thursday, September 11, 2008

BO is the specific business object class that you are using.

e.g. If you are doing Orders then you would an Order BO with a child collection of OrderLines each with an OrderLine.

So the code above becomes:

If LocalContext.Item("StartedTrans") Is Order Then

===========================================

Say you have 3 root BOs to save and each has the code I show in them.

Then the first one to call BeginTransaction is the one that actually starts the transaction. The next two make the same call but since an active tr exists, they simply enlist in it. When the next two call EndTransaction, nothing happens because they are not the one that started it (their Types are different than the test If LocalContext.Item("StartedTrans") Is Order Then). Only when the first BO calls EndTransaction does it meet the test above and then the entire tr is committed.

Joe

 

xxxJasonxxx replied on Thursday, September 11, 2008

Joe:

Please bear with me a tiny bit longer.  You've been a huge help and I've almost got it.....

I think I understand the concept or intent, but I may be getting mixed up on the syntax.

CONCEPT / INTENT.....
I think you're saying that if my Order object was the originator of the connection/transaction, that IF statement in your code is asking every object that uses it......are you the Order object?  Do I understand your concept/intent correctly?

SYNTAX.....

I'm assuming you're NOT saying I would explicitly change the code from this.....
         If LocalContext.Item("StartedTrans") Is BO Then
.....to this....
         If LocalContext.Item("StartedTrans") Is Order Then
.....because that seems too specific rather than generic/resuable (I don't think that's your intent).

Are you saying that the "BO" variable would evaluate as an "Order" type or "Order" object?

This could be part of my confusion too -- I noticed you are passing "BO" in like this...
         Public Shared Function BeginTransaction(ByVal BO As Core.IBusinessObject) As IDbTransaction
.....which makes me wonder if every business object would evaluate as True for being a Core.IBusinessObject (I think that's an interface all business base objects inherit from).

And possibly a major point of confusion for me with "BO" could be.......is this storing and comparing a memory reference to a specific object or is it storing and comparing an object type?

I'm not sure I'm expressing my confusion clearly or asking the right questions......but hopefully you get the idea and can help.

Thanks,

Jason

xxxJasonxxx replied on Thursday, September 11, 2008

Sorry...I think my confusion is simply not understanding how the "IS" keyword works in your IF statement.

      If LocalContext.Item("StartedTrans") Is BO Then

I am going back to do research on this.....it's one of those things I haven't used much lately except for when evaluating whether a variable "Is Nothing".  So I've just forgotten and need to refresh.

Thank you for the help -- really appreciate it.

xxxJasonxxx replied on Thursday, September 11, 2008

In case anyone else's memory is as bad as mine...

The Is operator determines if two object references refer to the same object. However, it does not perform value comparisons. If object1 and object2 both refer to the exact same object instance, result is True; if they do not, result is False.

Dim myObject As New Object
Dim otherObject As New Object
Dim yourObject, thisObject, thatObject As Object
Dim myCheck As Boolean
yourObject = myObject
thisObject = myObject
thatObject = otherObject
' The following statement sets myCheck to True.
myCheck = yourObject Is thisObject
' The following statement sets myCheck to False.
myCheck = thatObject Is thisObject
' The following statement sets myCheck to False.
myCheck = myObject Is thatObject
thatObject = myObject
' The following statement sets myCheck to True.
myCheck = thisObject Is thatObject

rsbaker0 replied on Thursday, September 11, 2008

xxxJasonxxx:

Sorry...I think my confusion is simply not understanding how the "IS" keyword works in your IF statement.

      If LocalContext.Item("StartedTrans") Is BO Then

I am going back to do research on this.....it's one of those things I haven't used much lately except for when evaluating whether a variable "Is Nothing".  So I've just forgotten and need to refresh.

Thank you for the help -- really appreciate it.

 

The "is" operator in C# just tests whether the left hand side is the of the Type on the right side. So, the above code is just testing if the object is of type "BO".

JoeFallon1 replied on Friday, September 12, 2008

xxxJasonxxx:

CONCEPT / INTENT.....
I think you're saying that if my Order object was the originator of the connection/transaction, that IF statement in your code is asking every object that uses it......are you the Order object?  Do I understand your concept/intent correctly?

SYNTAX.....

I'm assuming you're NOT saying I would explicitly change the code from this.....
         If LocalContext.Item("StartedTrans") Is BO Then
.....to this....
         If LocalContext.Item("StartedTrans") Is Order Then
.....because that seems too specific rather than generic/resuable (I don't think that's your intent).

Are you saying that the "BO" variable would evaluate as an "Order" type or "Order" object?

This could be part of my confusion too -- I noticed you are passing "BO" in like this...
         Public Shared Function BeginTransaction(ByVal BO As Core.IBusinessObject) As IDbTransaction
.....which makes me wonder if every business object would evaluate as True for being a Core.IBusinessObject (I think that's an interface all business base objects inherit from).

And possibly a major point of confusion for me with "BO" could be.......is this storing and comparing a memory reference to a specific object or is it storing and comparing an object type?

Concept intent: Yes. That is what is going on.

So the call to EndTransaction has this code:

If LocalContext.Item("StartedTrans") Is BO Then

Which says: get the item from the local context which I stored when I *started* the transaction and compare it to each BO which called End Transaction. So for the two later objects, they fail the test and nothing happens. But the last call is the Order object and it is the one that started the tr so the code evaluates to true and commits everything.

Syntax: right again - no need to change the code at all since BO is a parameter which is passed in (as Me) to each method. Only the BO that starts the tr is stored in context with the string key: "StartedTrans"

The BO itself is stored in context when the tr is started and it is compared with each BO that calls EndTransaction to be sure that only the one that started it can end it.

Joe

 

ajj3085 replied on Thursday, September 11, 2008

They don't really need to know.  Your code would something like this:

public class MyCommand : CommandBase {
    [Transactional( Transaction.TransactionScope )]
    private void override DataPortal_Execute() {
         ConnectionManager mg;

        using( mg = ConnectionManager.GetManager() ) {
             myRoot1.SaveSelf();
             myRoot2.SaveSelf();
        }
    }
}

public class MyRoot : BusinessBase {
    [Transactional( Transaction.TransactionScope )]
    private void override DataPortal_Insert() {
        InsertSelf();
    }

    internal void SaveSelf() {
        if ( IsNew ) {
            InsertSelf();
        }
        else {
           UpdateSelf();
       }
    }

    private void InsertSelf() {
         ConnectionManager mg;

         using( mg = ConnectionManager.GetManager() ) {
             // do update
         }
    }
}

Copyright (c) Marimer LLC