GOTCHA 74 ServicedComponents implemented inconsistently on XP and 2003


GOTCHA #74 ServicedComponents implemented inconsistently on XP and 2003

Enterprise Services in .NET provide ServicedComponents, which give you COM+ features like object pooling, just-in-time activation, and transactions. I learned a few lessons when I ran into this gotcha. Enterprise Services don't behave the same on different versions of Windows. If you are using transactions and expect your application to run on Windows 2000, XP, and 2003, there are things that you need to be aware of.

Say you want to perform an operation on some objects, and when it completes you may decide to commit or abort the transaction. You would expect this to be pretty straightforward. Consider Examples 8-15 and 8-16.

Example 8-15. Using Transactions in Enterprise Services (C#)

C# (ES)

 // Factory.cs part of ESLib.dll using System; using System.EnterpriseServices; namespace ESLib {     [Transaction(TransactionOption.Required), JustInTimeActivation]     public class Factory : ServicedComponent     {         public Comp CreateComp(int key)         {             ContextUtil.MyTransactionVote                 = TransactionVote.Abort;             Comp theComp = new Comp();             theComp.init(key);             ContextUtil.MyTransactionVote                 = TransactionVote.Commit;             return theComp;         }         protected override void Dispose(bool disposing)         {             if (disposing)             {                 ContextUtil.DeactivateOnReturn = true;             }             base.Dispose (disposing);         }     } } // Comp.cs part of ESLib.dll using System; using System.EnterpriseServices; namespace ESLib {     [Transaction(TransactionOption.Required), JustInTimeActivation]     public class Comp : ServicedComponent     {         private int theKey;         private int theVal;         internal void init(int key)         {             ContextUtil.MyTransactionVote                 = TransactionVote.Abort;             theKey = key;             theVal = key * 10;             ContextUtil.MyTransactionVote                 = TransactionVote.Commit;         }         public int GetValue()         {             return theVal;         }         public void SetValue(int val)         {             ContextUtil.MyTransactionVote                 = TransactionVote.Abort;             theVal = val;             if (val < 0)             {                 ContextUtil.DeactivateOnReturn = true;                 throw new ApplicationException(                         "Invalid value");             }             ContextUtil.MyTransactionVote                 = TransactionVote.Commit;         }     } } //Test.cs part of ESUser.exe using System; using ESLib; namespace ESUser {     class Test     {         public static void Work()         {             using(Factory theFactory = new Factory())             {                 try                 {                     Comp component1                         = theFactory.CreateComp(1);                     Comp component2                         = theFactory.CreateComp(2);                     Console.WriteLine(component1.GetValue());                     Console.WriteLine(component2.GetValue());                  component1.SetValue(1);                     component2.SetValue(-1);                     Console.WriteLine(component1.GetValue());                     Console.WriteLine(component2.GetValue());                 }                 catch(Exception ex)                 {                     Console.WriteLine("Oops: " + ex.Message);                 }             } // theFactory is Disposed here.         }         public static void Main()         {             try             {                 Work();             }             catch(Exception ex)             {                 Console.WriteLine("Error:" + ex.Message);             }         }     } } 

Example 8-16. Using Transactions in Enterprise Services (VB.NET)

VB.NET (ES)

 ' Factory.vb part of ESLib.dll Imports System.EnterpriseServices <Transaction(TransactionOption.Required), JustInTimeActivation()> _ Public Class Factory     Inherits ServicedComponent     Public Function CreateComp(ByVal key As Integer) As Comp         ContextUtil.MyTransactionVote = TransactionVote.Abort         Dim theComp As New Comp         theComp.init(key)         ContextUtil.MyTransactionVote = TransactionVote.Commit         Return theCOmp     End Function     Protected Overloads Overrides Sub Dispose( _            ByVal disposing As Boolean)         If disposing Then             ContextUtil.DeactivateOnReturn = True         End If         MyBase.Dispose(disposing)     End Sub End Class ' Comp.vb part of ESLib.dll Imports System.EnterpriseServices <Transaction(TransactionOption.Required), JustInTimeActivation()> _ Public Class Comp     Inherits ServicedComponent     Private theKey As Integer     Private theVal As Integer     Friend Sub init(ByVal key As Integer)         ContextUtil.MyTransactionVote = TransactionVote.Abort         theKey = key         theVal = key * 10         ContextUtil.MyTransactionVote = TransactionVote.Commit     End Sub     Public Function GetValue() As Integer         Return theVal     End Function     Public Sub SetValue(ByVal val As Integer)         ContextUtil.MyTransactionVote = TransactionVote.Abort         theVal = val         If val < 0 Then             ContextUtil.DeactivateOnReturn = True             Throw New ApplicationException("Invalid value")         End If         ContextUtil.MyTransactionVote = TransactionVote.Commit     End Sub End Class 'Test.vb part of ESUser.exe Imports ESLib Module Test     Public Sub Work()         Dim theFactory As New Factory         Try             Dim component1 As Comp = theFactory.CreateComp(1)             Dim component2 As Comp = theFactory.CreateComp(2)             Console.WriteLine(component1.GetValue())             Console.WriteLine(component2.GetValue())             component1.SetValue(1)             component2.SetValue(-1)             Console.WriteLine(component1.GetValue())             Console.WriteLine(component2.GetValue())         Catch ex As Exception             Console.WriteLine("Oops: " + ex.Message)         Finally             theFactory.Dispose()         End Try     End Sub     Public Sub Main()         Try             Work()         Catch ex As Exception             Console.WriteLine("Error:" + ex.Message)         End Try     End Sub End Module 

Comp is a ServicedComponent with one method, Method1(). This method throws an exception if the parameter val is less than 0. Note that Comp has its TRansaction attribute set as transactionOption.Required. Factory is a ServicedComponent that is used to create objects of Comp. In the Main() method of Test, you create two instances of Comp by calling the CreateComp() method of the Factory. You then invoke Method1() on the two instances. The exception that is thrown by the second call to Method1() is displayed in the catch handler block.

When you execute the code on Windows Server 2003, it runs as you would expect and produces the result shown in Figure 8-20.

Figure 8-20. Output from Example 8-15 on Windows 2003 Server


But if you run the same code on Windows XP, you get the error shown in Figure 8-21.

Figure 8-21. Output from Example 8-15 on Windows XP


The reason for this problem is that while one of the Comp components has voted to abort the transaction, the root object (the instance of the Factory) that created this object wants to commit it. Windows Server 2003 has no problem with that. But Windows XP does.

The solution is for the component Comp to tell the Factory that it is setting the transaction vote to Abort. The root object must then set its vote to Abort as well. This is not ideal because now the component needs to have a back pointer to the root object that created it. The code that does this is shown in Example 8-17 and Example 8-18. It uses an interface to break the cyclic dependency between the component and its factory.

Example 8-17. Communicating with the root object (C#)

C# (ES)

 // Factory.cs part of ESLib.dll using System; using System.EnterpriseServices; namespace ESLib {     public interface ITransactionCoordinator     {         void SetVoteToAbort();     }     [Transaction(TransactionOption.Required), JustInTimeActivation]     public class Factory : ServicedComponent, ITransactionCoordinator     {         public Comp CreateComp(int key)         {             ContextUtil.MyTransactionVote                 = TransactionVote.Abort;             Comp theComp = new Comp();             theComp.TheTransactionCoordinator = this;             theComp.init(key);             ContextUtil.MyTransactionVote                 = TransactionVote.Commit;             return theComp;         }         protected override void Dispose(bool disposing)         {             if (disposing)             {                 ContextUtil.DeactivateOnReturn = true;             }             base.Dispose (disposing);         }         #region ITransactionCoordinator Members         public void SetVoteToAbort()         {             ContextUtil.MyTransactionVote = TransactionVote.Abort;         }         #endregion     } } // Comp.cs part of ESLib.dll using System; using System.EnterpriseServices; namespace ESLib {     [Transaction(TransactionOption.Required), JustInTimeActivation]     public class Comp : ServicedComponent     {         private int theKey;         private int theVal;         private ITransactionCoordinator theTXNCoordinator;         internal ITransactionCoordinator TheTransactionCoordinator         {             get { return theTXNCoordinator; }             set { theTXNCoordinator = value; }         }         internal void init(int key)         {             ContextUtil.MyTransactionVote                 = TransactionVote.Abort;             theKey = key;             theVal = key * 10;             ContextUtil.MyTransactionVote                 = TransactionVote.Commit;         }         public int GetValue()         {             return theVal;         }         public void SetValue(int val)         {             ContextUtil.MyTransactionVote                 = TransactionVote.Abort;             theVal = val;             if (val < 0)             {                 ContextUtil.DeactivateOnReturn = true;                 if (theTXNCoordinator != null)                     theTXNCoordinator.SetVoteToAbort();                 throw new ApplicationException(                     "Invalid value");             }             ContextUtil.MyTransactionVote                 = TransactionVote.Commit;         }     } } 

Example 8-18. Communicating with the root object (VB.NET)

VB.NET (ES)

 ' Factory.vb part of ESLib.dll Imports System.EnterpriseServices Public Interface ITransactionCoordinator     Sub SetVoteToAbort() End Interface <Transaction(TransactionOption.Required), JustInTimeActivation()> _ Public Class Factory     Inherits ServicedComponent     Implements ITransactionCoordinator     Public Function CreateComp(ByVal key As Integer) As Comp         ContextUtil.MyTransactionVote = TransactionVote.Abort         Dim theComp As New Comp         theComp.TheTransactionCoordinator = Me         theCOmp.init(key)         ContextUtil.MyTransactionVote = TransactionVote.Commit         Return theCOmp     End Function     Protected Overloads Overrides Sub Dispose( _         ByVal disposing As Boolean)         If disposing Then             ContextUtil.DeactivateOnReturn = True         End If         MyBase.Dispose(disposing)     End Sub     Public Sub SetVoteToAbort() _         Implements ITransactionCoordinator.SetVoteToAbort         ContextUtil.MyTransactionVote() = TransactionVote.Abort     End Sub End Class ' Comp.vb part of ESLib.dll Imports System.EnterpriseServices <Transaction(TransactionOption.Required), JustInTimeActivation()> _ Public Class Comp     Inherits ServicedComponent     Private theKey As Integer     Private theVal As Integer     Private theTXNCoordinator As ITransactionCoordinator     Friend Property TheTransactionCoordinator() _         As ITransactionCoordinator         Get             Return theTXNCoordinator         End Get         Set(ByVal Value As ITransactionCoordinator)             theTXNCoordinator = Value         End Set     End Property     Friend Sub init(ByVal key As Integer)         ContextUtil.MyTransactionVote = TransactionVote.Abort         theKey = key         theVal = key * 10         ContextUtil.MyTransactionVote = TransactionVote.Commit     End Sub     Public Function GetValue() As Integer         Return theVal     End Function     Public Sub SetValue(ByVal val As Integer)         ContextUtil.MyTransactionVote = TransactionVote.Abort         theVal = val         If val < 0 Then             ContextUtil.DeactivateOnReturn = True             If Not theTXNCoordinator Is Nothing Then                 theTXNCoordinator.SetVoteToAbort()             End If             Throw New ApplicationException("Invalid value")         End If         ContextUtil.MyTransactionVote = TransactionVote.Commit     End Sub End Class 

Note that Method1() informs the root object that it wants to abort the transaction. The root object then sets its transaction vote to Abort. After this modification, the program produces the same result on XP as on Windows Server 2003.

One moral from this story is to run unit tests not just on your machine but also on every supported platform. How practical is this? Well, it's not just practical, it's highly feasible with project automation and continuous integration tools like NAnt, NUnit and Cruise Control .NET.

If you have hardware/cost limitations, you can use a product such as VirtualPC or VMWare to run these tests on different platforms on the same hardware.


IN A NUTSHELL

ServicedComponents don't behave the same on all Windows platforms. Make sure you test early and often on all supported versions.

SEE ALSO

Gotcha #75, "AutoComplete comes with undesirable side effects."



    .NET Gotachas
    .NET Gotachas
    ISBN: N/A
    EAN: N/A
    Year: 2005
    Pages: 126

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net