CSLA 4.x TransactionalDataPortal has a breaking change compared to CSLA 3.8

CSLA 4.x TransactionalDataPortal has a breaking change compared to CSLA 3.8

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


Jaans posted on Monday, June 02, 2014

Houston we have a problem. I've been using both CSLA 4.x and 3.8 for some time now, and haven't run across this issue until now, where we are upgrading a CSLA 3.8 based project to CSLA 4.5.

This specific project executes along with other external application components from COM+ (Enterprise Services), making it sensitive to 2PC and Transaction Isolation levels.

Context: I'm referring to using the [Transactional( TransactionalTypes.TransactionScope )] attribute on the DataPortal_XYZ methods.

For most CSLA projects, the transaction starts from the DataPortal_XYZ methods and seldom goes further to include other transactions. We have combinations of both, and specifically, we let the DataPortal_XYZ methods "take-on" the transaction isolation level of the caller. This may vary in that one caller has one isolation level, and another caller may use a different one (don't you just love integration work Ick! ).

What cannot be done is to have a caller use one isolation level, and then have the subsequent DataPortal_XYZ method execute under a different isolation level, when it is enrolled in the same transaction scope of the caller (TransactionScope = Required).

The implementation of the CSLA 3.8 Transactional DataPortal code had created a new System.Transactions.TransactionScope instance using the default parameterless constructor. This default constructor assumes a Scope = Required but specficially does not specify an isolation level. There is no default here, it's unspecified.

Here is the code for the CSLA 3.8 version: http://www.lhotka.net/cslacvs/viewvc.cgi/core/branches/V3-8-x/cslacs/Csla/Server/TransactionalDataPortal.cs?revision=4288&view=markup

The implementation of the CSLA 4.5 Transactional DataPortal looks to have some refactorings and actually creates a new System.Transactions.TransactionScope instance using a constructor overload that takes both the Scope and the Isolation level as parameters. The Scope is defaulted to Required (when not expressly specified in the DataPortal_XYZ attribute), which matches the original behaviour. But, the isolation level is also specified in this overload, using a default value of Serializable and therein lies the rub, it's not unspecified/omitted.

Here is the code for the latest CSLA 4.5.x version: https://github.com/MarimerLLC/csla/blob/master/Source/Csla/Server/TransactionalDataPortal.cs

This default of Serializable is the safest (though most expensive) isolation level and makes a good default. Unfortunately, it is not the same behaviour as with CSLA 3.8, and does not allow the "flow from caller" scenario.

Please note I'm pointing out a subtle difference here.
Of course you can specify any isolation level that you want in the DataPortal_XYZ attribute (e.g. [Transactional( TransactionalTypes.TransactionScope, TransactionIsolationLevel.RepeatableRead )] but that is not the issue. The issue is that by specifying it in the first place, results that it will always be that isolation level, regardless of the isolation level flowed from caller's scope (if one is present).

With CSLA 3.8's implementation it would also have IMPLICITLY defaulted to serializable, but only if the isolation level from a caller's scope was not different, in which case it would've taken the isolation level of the caller.

With CSLA 4.5's implementation it is EXPLICITLY set to serializable by CSLA's default behavior, resulting in a transaction exception faulting the mismatch of isolation levels (the one from the callers scope not matching the one set on the DataPortal_XYZ.

It turns out the System.Transactions.IsolationLevel enumeration has an Unspecified option.
Refer: http://msdn.microsoft.com/en-au/library/system.transactions.isolationlevel.aspx

I've done some tests and if I manually create a transaction scope using the same overload as CSLA 4 but then use this "Unspecified" option is works as expected and behaves as per CSLA 3.8 allowing me to "flow" the isolation level from the caller.

Unfortunately, the ApplicationContext.DefaultTransactionIsolationLevel does not have an Unspecified enumeration either).

@Jonny / @Rocky - Would you accept this as an issue for rectification?

I really hope so, as this is a show stopper for our project development at the moment.

In mind, the fix would be to either:

 

I'd be happy to try and put a pull-request together if that would help further.
Thanks a million,

Jaans

 

 

ajj3085 replied on Friday, June 06, 2014

I think the normal response to requests along these lines is to just not use the attribute, and instead use Manual and create the TS yourself inside the DP methods.  I've done this since we need to use the ReadCommitted isolation level; its not that big a deal, just two lines of code (and some braces).

RockfordLhotka replied on Friday, June 06, 2014

I'm open to improving the existing implementation, and from what I can see this is a reasonable suggestion for improvement - why wouldn't we support the unspecified option?

Jaans replied on Friday, June 06, 2014

Thanks Rocky.

To my mind this is somewhat of an edge case, given that CSLA 4.x has been around a while and this hasn't surfaced sooner (including from us as we have many other projects already on CSLA 4.x.

That said, making the default behaviour the same as previous version of CSLA would be more consistent. Also, when the Unspecified option is the default, it still results in the same behaviour currently where it ends up in the safest isolation mode of Serialized.

To me that suggests this change to the defaults doesn't break behaviour currently expected and used by CSLA 4.5 users, and adds support for "flowing" the isolation level from the caller when needed.

In addition, there is always the fall-back option for specifying the appSetting "CslaDefaultTransactionIsolationLevel" in the configuration file.

If you are still happy to make the change, do you want me to create and issue and create a PR or can I leave that for you?

Thanks again,
Jaans 

PS: Is there any information/updates on CSLA.js initiative yet? Would be keen to see where it's headed.

Jaans replied on Monday, June 16, 2014

Hi Rocky / Jonny

I've discovered another problem (in addition to the above discussion to add the "Unspecified" Isolation level and to then change the default transaction isolation level to "Unspecified").

Specifically I have found that if you are making use of Async/Await calls inside of a DataPortal_XYZ method that has been decorated with the [Transactional( TransactionalTypes.TransactionScope )] attribute (as you would typically do if you are using a transactional resource / service) then the TransactionScope transaction does not flow across the Task thread(s) for them.

Took a while to figure out what is going on, because there is no error - it's quite subversive in that the roll-back just fails to happen when you need it - really nasty stuff tends to ensue then.

Turns out that MS has added some additional TransactionScope constructors to take in a new enum parameter called "TransactionScopeAsyncFlowOption". Refer http://msdn.microsoft.com/en-us/library/dn261473.aspx

This has a value of "TransactionScopeAsyncFlowOption.Enabled" that resolves the above gotcha, and allows scenarios like this:

// transaction scope
using (var scope = new TransactionScope(... , TransactionScopeAsyncFlowOption.Enabled))
{
  // connection
  using (var connection = new SqlConnection(_connectionString))
  {
    // open connection asynchronously
    await connection.OpenAsync();

    using (var command = connection.CreateCommand())
    {
      command.CommandText = ...;

      // run command asynchronously
      using (var dataReader = await command.ExecuteReaderAsync())
      {
        while (dataReader.Read())
        {
          ...
        }
      }
    }
  }
  scope.Complete(); 

} 

Jaans replied on Thursday, July 03, 2014

@Johnny / @Rocky

I would just like to follow up and ensure this doesn't "fall off the bus". As stated before, I'd be happy to create Issues for these in Github if preferred - and if need be I'd try to create a pull-request if you want me to.

These two TransactionScope issues are becoming ever frustrating to work around - and the dev team keeps assuming the normal way with CSLA 4 will work like always.

To summarise:

  1. We need an additional enumeration for the CSLA TransactionIsolationLevel enumeration, called "Unspecified" to match those from System.Transactions
     
  2. This additional enumeration should be the default TransactionIsolationLevel for the CSLA Transactional DataPortal when none is specifically stated to match the behaviour from pre CSLA 4 times.

  3. Support for flowing Transactions when using Async / Await pattern. Requires using a different constructor overload for the TransactionScope object, that specifies the parameter "TransactionScopeAsyncFlowOption.Enabled". This for the CSLA Transactional DataPortal, 

Thanks again, and apologies if my repeated follow-ups seem "strong" - it's just an important issue for us and took some work to find in the first place.

Regards,
Jaans 

RockfordLhotka replied on Thursday, July 03, 2014

Please do add an issue to GitHub, and if you have a solution please feel free to submit a pull request with the fix and I'll review it.

Jaans replied on Thursday, July 03, 2014

Excellent. Will do as soon as I'm able. 

Thank you

Jaans replied on Friday, July 04, 2014

Hi Rocky

I've created the two issues on GitHub:

I've created a pull request #289 that addresses issue #287.

Unfortunately, the PR for issue #288 will have to wait until CSLA targets 4.5.1 (as opposed to just 4.5) since the support for flowing async TransactionScope requires .NET 4.5.1 or later. I've updated the GitHub issue with the relevant comment.

Thanks,
Jaans 

 

Copyright (c) Marimer LLC