GOTCHA #74 ServicedComponents implemented inconsistently on XP and 2003Enterprise 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 ServerBut 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 XPThe 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.
IN A NUTSHELLServicedComponents don't behave the same on all Windows platforms. Make sure you test early and often on all supported versions. SEE ALSOGotcha #75, "AutoComplete comes with undesirable side effects." |