Download the source code for this article
Over the last year or two,quite a few articles and books have been written on COM+, transaction managementand programming stateful & stateless components, explaining to you in detailwhat COM+ does and how it does it. WhenI was tasked with creating a model for developing lightweight, reusablecomponents, I found a lot of What-not-How type of articles on COM+, butnot a lot of practical How-To type of articles. I started working through a lot of information on COM+, combined thisinformation with how I wanted a reusable component to behave, and came up with areal easy model for creating reusable COM+ business objects.
Architecture and NamingConventions
Lets start off by examining thearchitecture and naming conventions my components had to fit into. The following naming conventions have been used for numerous projects inthe company and proved to be very useful.
A component consists of theBusiness Object (BO) layer on the COM+ machine, and a Client Object (CO) layerthat runs on the client machine. TheCO functions as an intermediary between the application and the BO layer. A component on the CO layer does the following:
- It gathers configuration information in Plug & Play style (more about this in another article)
- It functions as a wrapper around business logic in the BO component
- If provides a stateful COM component to the application, shielding the application from any interaction with BO components
Iuse the following naming conventions to easily distinguish between classes ondifferent levels.
- On CO level
- Project name gets the suffix CO
- On BO level
- Project name gets suffix BO
- Class name gets suffix SVR (Server)
- Classes get the prefix cls
- Collection classes gets the prefix col
Figure 1 shows this architecture and naming conventions applied for our sampleproject, Client.
Figure 1:Architecture and naming conventions applied to Client project
As with most component-reusescenarios, the aim is to develop lightweight reusable objects, with some or nobusiness logic. The next step is to create more complex reusable objects bycombining the lightweight objects together with real-world business logic.
At SDT we have a reuse level gradingsystem that we use to indicate the level of reuse you can expect from a specificcomponent. These levels range frommere concept reuse (level 1) to binary reuse on level 5, with the ultimate beinglevel 6, which is basically level 5 reuse plus a reusable user interface. Take note that their is aninevitable tradeoff between ultimate reusability and performance. In thisarticle the focus is on reuse and maintainability rather than on outrightperformance. In real life, performance is always important, but if your projectstarts running to 3 to 4 million lines of code, maintainability also becomes amajor factor.
The requirements set by SDT for reusablecomponents COM+ are:
- The component must be on reuse level 5, that is it must be reusable in binary form (a DLL)
- The component must adhere to above mentioned naming conventions
- The component must be available for reuse on both the BO and CO level
- The component must gather any configuration information in Plug & Play style
- The application must be totally isolated from any internal configuration information about the component it is using.
- Object state must be maintained on the client machine.
- Object state must be communicated to and from the BO object for every method to be executed from CO level
- Changed object state should only be returned to the CO level if the method executed successfully. If the method should fail, the original object state has to be returned to the CO level.
I have two projects I will beusing as examples. In order to makethe code listings small and easy to read, I kept the properties of the objectsto a minimum and removed all error handling code and comments. It adds up to asubstantial amount of code, so throughout the article I only show bits andpieces of the code, however all the code for the article is available fordownload.
Project 1: Client
This component maintains basicclient information. The component consists of the following Visual Basicprojects and classes:
- Project: ClientCO
- clsSystem: The system class is tasked with obtaining configuration information in Plug and Play style for the Client project, such as database connection information (I prefer to keep connection information on CO level and pass it to the BO object with every method call, as this allows multiple CO objects to use one COM+ machine to work on more than one database)
- Project: ClientBO
- clsClientSVR (MTSTransactionMode = 2: Requires Transaction)
Theclient table has the following fields:
Figure 2: Client Table
Project 2: Contract
This component maintains basicinformation regarding insurance contracts. It also maintains the link between a contract and all the clientsassociated with that contract. Itwill reuse our client component for this task. The component consists of the following Visual Basic projects andclasses:
- Project: ContractCO
- clsSystem: The system class is tasked with obtaining configuration information for the Contract project, such as database connection information
- clsContractSVR (MTSTransactionMode = 2: Requires Transaction)
- clsContractClientSVR (MTSTransactionMode = 2: Requires Transaction)
- colContractClientsSVR (MTSTransactionMode = 3: Uses Transaction)
Figure 3: Contract Tables
Note that on the BO projects, you need to set the following properties: Unattended Execution must be on, Retained in Memory must be on and Binary Compatibility must be set.
Overview: How COM+ managestransactions & state
I will attempt to give a quickoverview of how COM+ goes about its business. Quite a few articles and books areavailable on the topic by acclaimed authors. I can recommend the book Programming DistributedApplications with COM+ and Microsoft Visual Basic 6 by Ted Pattison, or anarticle he wrote for Microsoft Systems Journal in October 1999, titledWriting COM+-style Transactions with Visual Basic and SQL Server. The following overview provides a very brief summary of thekey functionality of COM+. Understandingthe impact of each of these functions plays a crucial role in understanding howto use them in your reusable objects.
COM+ provides a programmingmodel based on declarative transactions. Settingthe MTSTransactionMode property of the class sets an object’stransaction mode at design time. Whenthe COM+ runtime creates a new object, it examines the component’s MTSTransactionModesetting to determine whether the new object should be created inside atransaction. Once an object hasbeen created, you cannot add it to a transaction, neither can you disassociateit from the transaction in which it has been created. Once an object is created inside a transaction, it spends its entirelifetime inside this transaction. Whenthe transaction is committed or aborted, the COM+ runtime destroys all theobjects inside it.
When the COM+ runtime receivesa request to create a new object, it inspects the transaction context of thecreator to determine whether the creator is running inside a transaction. It also inspects the transaction support attribute of the new object. Using these two pieces of information it decides whether to create theobject 1) inside a new transaction, 2) inside the same transaction as itscreator or 3) without a transaction.
Figure 4 shows a diagram of a client application connecting to an object (theroot object) through a COM+ context. The root object in turn instantiated a secondary object in the same COM+transaction. The root objectconnects to the secondary object through another COM+ context. Figure4 also shows three important Boolean values that COM+ uses to manage thescope of the transaction and the object lifetime. In Figure4 these flags are set to their default values. For a detailed discussion on how COM+ uses this three flags, refer tochapter 10 of the book by Ted Pattison mentioned above.
Figure 4: Rootobject and Secondary object in COM+ transaction
The root COM+ object plays avery important role in a COM+ transaction. Every transaction as a wholemaintains a Boolean value (MustAbort) that indicates whether thetransaction should be aborted. Thisvalue is initially set to False when the transaction is created. COM+ inspects the value of MustAbort when:
- The root object returns control to its caller. If MustAbort is False nothing happens. If it is True, COM+ aborts the transaction and deactivates all the secondary objects inside the transaction.
- The root object is deactivated.
Depending on the value of MustAbort, COM+ will then either commit or abort the transaction. Whatever the outcome of the transaction, when the transaction completes, COM+ will recycle the object(s) that took part in the transaction. This means that the lifetime of the transaction plays an enormous part indesigning the interaction between objects partaking in either the same ordifferent transactions.
There are two method pairsavailable to the developer with which to control the transaction scope andoutcome. These are DisableCommit/Enablecommitand SetComplete/SetAbort.
The DisableCommit and EnableCommitmethods allows you to set the value of the IsHappyflag. When an object running insidea transaction is deactivated, and IsHappy is False, the transaction’s MustAbortflag is set to true, which will cause the transaction to be aborted. Note that this is method only sets the value of IsHappy, it doesnot perform any other logic. Thismeans that the value of this flag can be changed multiple times, but the valueis only taken into account when the object is deactivated.
The SetComplete and SetAbortmethods sets the value of IsHappy to either True orFalse, plus it sets the value of IsDone to True. The value of the IsDone flag is examined every time an objectreturns control to its caller. Ifthe IsDone value is True, COM+ deactivates this object, andthe normal process to determine whether to commit or abort the transaction isstarted.
From the above it is clearthat, if you are developing an object that may be reused by other objects on theBO level, care has to be taken when calling SetComplete and SetAbort. If your object participates in a transaction as a secondary object, and SetCompleteor SetAbort is called, this object will be recycled (and the instancestate will be lost) when it returns control to the calling object. This may not be what the calling object was expecting. An example of this is: In Object B, the Save method does a SetCompleteat successful completion. ObjectA uses an instance of Object B it loads an instance of ObjectB, does some work and saves, it does some more work and want to save itagain. Problem is that after thefirst time Save is executed, Object B is recycled when it returnscontrol to Object A, and the instance state is lost. Object A is not aware of this, as it actually connects to thecontext and not directly to Object B. The next time Object A tries to access any method or property of ObjectB, a new instance of Object B is created within the existing objectcontext. Object A is totallyunaware that it is now working with another instance of Object B. If Object A now performs logic that is dependant on the state of ObjectB, it will produce erroneous results.
Issues such as the above meansthat you cannot call SetComplete unless you are absolutely sure that noother object expects you to hold your state. As your component will be reused on a binary level, it has no guaranteewho uses it or exactly how it is being used. This means that great care has to be taken when calling SetCompletein your component, and that you have to inform any developer of another BO-levelcalling component of this fact, at design time. You can alert the user of your component to the fact that you are using SetCompletein a method by documentation or a naming convention.
Any BO level component shouldalways only be accessed by its CO counterpart component from the CO level.Because a component and its CO level partner will always be designed andimplemented together, the CO object can take the usage of SetCompleteinto account in the design stage.
When designing interactionbetween COM+ objects, I make use the following rules (which I like to refer toas the COM+ State Rules) :
- Non Transactional COM+ objects can maintain instance state
- Transactional COM+ objects maintains instance state for the duration of the transaction
- Transactional COM+ objects cannot maintain instance state across transaction boundaries
- Transaction scope can be managed by object scope
- Transaction scope can be managed by calling SetComplete/SetAbort
- Acquire resources late, release them early
The core concept behind themethod I follow is to realize other BO objects as the primary users of yourreusable BO object, with the BO object’s own CO counterpart seen as asecondary user of the object. Thisis quite a significant paradigm shift from seeing the CO object as the primaryuser of its BO counterpart. Whenthe CO object is seen as the primary user, all methods on the BO object isnormally geared towards a) getting state information from the CO, b) performingan action on this state and c) returning the changed state to the CO. When you view other BO objects as the primary users of your BO levelobject, the interfaces to the functionality provided by the object changes fromsimple Get-In, Do-your-stuff and Return-Results type of approach, to afull OOP interface. The BO object needs to expose its methods and properties viaan OOP interface to other BO objects. Allthe functionality available to other BO level objects must however also beavailable to the object’s own CO counterpart. There must exist only one set of business logic on the BOlayer, accessible to the BO and the CO layer in the following ways:
- A BO object (Object A) can instantiate an instance of another BO object (Object B). Object B exposes an OOP interface to Object A through which Object A is able to call methods and set and retrieve properties of Object B. Object A can control the scope of the instance of Object B and by doing so, the scope of the transaction
- A CO object is able to call the business logic in its corresponding BO object via a wrapper function. It calls a function on the BO object and passes its instance state into this function in one or more state variables. The function executes the same method exposed to other BO level objects, and when it is done, the CO can retrieve the changed instance state from the state variables. The CO object does not expect the Bo object to maintain state between method calls, and sends and retrieves the instance state to and from the BO object with every function call.
The above requirements are metby following a very simple recipe on the BO object. The functionality on BO level is provided in plain objectoriented fashion. The BO object hasproperty procedures with private variables to maintain the instance state, andfunctionality is provided as methods that act on the properties of the object(and any secondary objects). Any BO objectis trusted to never call SetComplete when accessed by other BO objects (Wewill examine the use of SetAbort in more detail when we look at error handling). If you have a look at the COM+ State Rules, you will see that aroot object then only has to worry about the scope of its secondary objects, asobject instance state and transaction management is done using the functionalityprovided by COM+.
CO access to the methods on theBO is provided by a wrapper function on the BO object. Note that I refer to it as a wrapper function and not amethod. This is purely anaming convention I use to distinguish between the methods exposed for use byother BO objects (called methods), and the methods exposed for use by CO objects(I call these functions). Thereason I refer to this specific type of method as a function is because itreceives all the information it needs via the parameter list, as opposed to anormal method that acts on the existing state of an object. So, this wrapper function is implemented as a normal method of theobject, but it takes as input a variable or variables carrying the totalinstance state of the object. Thiswrapper function is very simple, as it performs a maximum of three tasks:
The instance state as sent from the CO object is copied into the local state variables of the BO object
The corresponding method is executed
The local instance state is copied from the local variables into the By Reference variables from where the CO object will have access to them
Dependingon the logic in the method, step 1 or 3 may be left out. If the method is used to retrieve instance state from the database, step1 may be left out, while step 3 may be left out if the method does not make anychanges to the instance state of the object.
On theCO object, the methods that call the functions on the BO object follow a similarpattern. The methods generally consist of the following steps
- The local instance state is packed into a variable
- An instance of the BO object is created
- The BO function is called and the local instance state is passed to this function
- The BO object is destroyed
- The instance state is unpacked into the local variables
Depending on the logic of themethod executed, it may not be necessary to send state to the BO method (whenloading the object from the database for example), or to retrieve state backfrom the method (when deleting the object from the database)
So, for every method on the BOobject, there is a function call to expose this method to its CO object. In order to distinguish between the function and the method, I use thefollowing naming convention: The method and the function gets the same name,with the prefix f_ added to the start of the name of the function,indicating that this is the wrapper function and only to be called from the COlevel.
How the CO object uses the BO object
If youlook at the code for the f_Load methodof clsClientSVR in our sample project (shortened code shown below), you will see that on BO levelwe have f_Load and Load. Loaddoes the actual work, while f_Load just serves as a wrapper providing clsClientwith access to this logic.
When an external applicationuses ClientCO (code shown below) to load an object from the database, thefollowing logic is executed:
- clsClient.Load instantiates an instance of the BO object and calls the f_Load function of this object
- On the BO, f_Load calls the method Load
- Load loads the information from the database into its private state variables
- f_Load packs the object instance state into an XML string
- On the BO, f_Load calls the method Load
- The instance state is unpacked from the XML string into the CO object’s local state variables
How another BO object uses this BO object
Using the object on BO layer ispretty simple if you keep the COM+ Rules in mind. The code snippet below shows how another COM+ object (let’scall it Object A) would use the functionality offered by clsClientSVR.
In the above code, the root object passes the connection string to the secondary object. This means that my sample will only work if the tables for these two objects are located on the same database. In the real world you may find that your objects may be residing on different databases and even different machines, so your calling application will have to allow for more than one set of configuration data to be carried.
The programmer of Object Aneeds to take into account the transaction settings of both Object A and ClientBO.clsClientSVR(lets call it Object B) when implementing such functionality. The programmer’s options are not limited by the transactional settingsof Object B, even if he receives it in binary format, as the MTSTransactionModesetting can be overridden by setting the property on the COM+ administrationconsole (there are some limitations though, for example: You cannot trust anobject originally marked Not an COM+ Object object to participate in atransaction if was not designed for that purpose). The programmer will have to decide whether Object A and Object B shouldshare the same transaction, participate in separate transactions, or if one oreither should run without transactions.
It is important to note thatObject B will join Object A in its transaction only if Object A is alreadyrunning within a transaction (MTSTransactionMode = Requires Transaction or Requires NewTransaction) and Object B has MTSTransactionMode setting of UsesTransaction or RequiresTransaction
As can be seen from above code,using Client on a BO level is pure object oriented programming. Transaction and state management is done by COM+. The root object trusts the secondary object not to call SetCompletebetween method calls. The developerof the root object can easily obtain the MTSTransactionMoce setting ofthe secondary object by using the COM+ administration console. The developer can then use the COM+ State Rules to decide whetherthe two objects can use the same transaction and what the scope of the secondobject should be. If the root object runs inside a different transaction thanthe secondary object, the root object can control the scope of the transactionof the secondary object by either calling the SetComplete method of thesecondary object by means of a wrapper method, or by simply deactivating theobject as soon as possible. Iprefer the second option, as it fits neatly into the rule Acquire resourceslate and release them early. Forinterest’s sake, the SetComplete wrapper method can be implemented asshown below. If Object A calls this method on Object B, when the methodcompletes it passes through the object context when returning control to ObjectA, at which time COM+ will inspect the IsDone flag and recycle Object B.
In the samples I always use theCreateInstance method to create a new object instance. In real life however, I prefer to wrap this inside a function. This isvery useful when switching between MTS and COM+, asyou should use CreateInstance for MTS while CreateObject is the preferredmethod for COM+. The function is quite simple and does the following(sample shown is configured for use with MTS – in COM+ I always use CreateObject)
Starting to benefit fromreuse
Let us now look at a slightlymore complex sample of reuse. In our Insurance project the contract can havemultiple clients associated with it. Wewant to reuse our Client component to provide a collection of Client objects,and these objects must be accessible on both the BO and CO layers of ourInsurance project. The CO level ofthe insurance object will reuse our clsClient object, while the BO levelof the insurance object will reuse clsClientSVR. Thus the ContractBO project has to reference the ClientBOproject, and the ContractCO project has to reference the ClientCOproject. Notethat the only CO project to reference ClientBO is ClientCO.
The relationship betweenContract and Client is many-to-many. Iimplement this link using a table called ContractClients. I do not use a compound key for this table, as updating changes to atable with compound keys can become a total nightmare if any one of the keyfields changes. I implement clsContractClientto map to a row in this table, and the collection class colContractClientsthat will Load all the ContractClient records for a specified contract. Depending on your business logic, you may decide to Loadthe corresponding Client object either on first reference, or every time clsContractClientis loaded. I chose to Loadthe corresponding Client object in clsContractClient.Load,
The collection class colContractClientstakes as parameter the primary key of the contract, and then fetch the primarykeys of all the clients associated with this contract, loads these clsContractClientobjects and then adds them to the collection. The advantage of this approach is total encapsulation of my Clientobject. There is only one method toload my client object and one method to save it, meaning fixing an error oradding functionality to my Client object has to be done only in clsClientSVR. The disadvantage of this approach is that, in order to load nclients I will always have n+1 queries to SQL. In most cases it is possible for the collection class to select all thenecessary information in one query and to push this information into the clientobjects, but that means you need to know exactly how clsClientSVR expectsits data, which is not possible if you are aiming for binary reuse. It boils down to a tradeoff between speed andmaintainability/reusability, and Ichose the easier maintainable option. Thecode for f_LoadAll and LoadAll of colContractClientsSVR isshown below. As is the case with all other code, f_LoadAll is foruse by the CO level object while LoadAll performs the actual work and canbe used by other BO level objects.
The collection class colContractClients provides you with two functions to retrieve objects from the collection, either by primary key (ItemByKey) or by position (ItemByPosition) It also provides an enumerator function. In order to be able to enumerate the collection, you have to set the Procedure ID of the NewEnum function to 4. To set this property, load the project in VB, go to the object browser (F2) and select the project ClientBO. It will then list all your classes in the left pane. Select colContractClientsSVR, and all properties and methods will appear in the right pane. Select NewEnum, right-click and select “Properties” from the pop-up menu. Click on the “Advanced” button, and set “Procedure ID” to 4. As a neat touch, I also like to hide the NewEnum function, so click on “Hide this member” if you share my sentiments. Repeat above steps for colContractClients on the CO layer.
Loading our Contract now becomes a slightly more complex affair, and it is done in 4 steps.
- clsContractSVR.Load selects the contract data and loads it into the private variables of clsContractSVR
- The LoadAll method of colContractClientsSVR is called. This method selects the primary key field of every clsContractClientSVR object in the collection, and then calls the Load method for each of these.
- clsContractClientSVR.Load loads its data into the private variables and then calls the Load method on its private instance of clsClientSVR
- clsClientSVR.Load loads the client data into the object
Although the above forms a long chain, it is pretty straightforward OOP programming. We now have the ability to load a rather complex object hierarchy in memory on our COM+ machine. The next step is to transport this hierarchy to the CO level.