Consider the situation in which a nontransactional client creates a few transactional objects, all configured to require transactions. The client would like to scope all its interactions with the objects it creates under one transaction—in essence, to function like the root of that transaction. The problem is that the client is not configured to require transactions (maybe it is a legacy component or maybe it is not even a component, such as a form or a script client), so it cannot have a transaction to include the objects it creates. On the other hand, the objects require transaction support to operate properly, so for every object the client creates, COM+ creates a transaction. As a result, even if the client intended to combine the work of multiple COM+ objects into a single transaction, the net result would be multiple transactions (see Figure 4-11). The real problem now is that each transaction can commit or abort independently. The operations the client performs on the system (using the objects) are no longer atomic, so the client jeopardizes system consistency. Furthermore, even if all objects were under one transaction, how would the client vote to commit or abort that transaction?
There is an elegant and simple solution to this predicament. The solution is to introduce a middleman—a transactional component that creates the objects on behalf of the client. The middleman creates the objects and returns interface pointers back to the client. The middleman objects also should provide the client with ability to commit or abort the transaction.
These middleman requirements are generic. Therefore, COM+ provides a
readymade middleman called the transaction
context
component. As part of the COM+
Utilities application, COM+ provides two components (one for VB 6.0
and one for C++), each supporting a slightly different interface. A
VB 6.0 client should use the ITransactionContext
interface, creatable via the prog-ID
TxCtx.TransactionContext
(or the class name
TransactionContext
). A C++ client should use the
ITransactionContextEx
interface, which is creatable via the
class ID CLSID_TransactionContextEx
.
The two interfaces are defined as:
interface ITransactionContext : IDispatch { HRESULT CreateInstance([in]BSTR pszProgId,[out, retval]VARIANT* pObject); HRESULT Commit( ); HRESULT Abort( ); }; interface ITransactionContextEx : IUnknown { HRESULT CreateInstance([in]GUID* rclsid,[in]IID* riid, [out,retval]void** pObject); HRESULT Commit( ); HRESULT Abort( ); };
These interfaces allow the client to create new component instances and commit or abort the transaction. The two transaction context components are configured to require a new transaction, so they are always the root of that transaction. This configuration also prevents you from misusing the transaction context objects by enlisting them in an existing transaction. All objects created by the transaction context object share the same transaction (see Figure 4-12).
All the client has to do is create the transaction context object,
and then use it to create the other objects via the
CreateInstance( )
method. If the client wants to commit the transaction, it must
explicitly call the Commit( )
method. Once the client calls the Commit( )
method, the transaction ends on return from the Commit( )
method. If one of the internal objects votes to abort the
transaction before the client calls Commit( )
, the
client’s call to Commit( )
returns with the
error code of
CONTEXT_E_ABORT
, indicating that the transaction was
already aborted. The client can chose to start a new transaction or
handle the error in some other manner.
If the client does not call Commit( )
, the
transaction is aborted, even if all the participating objects voted
to commit. This abortion is intentional, to force the client to voice
its opinion on the work done by the objects it created. Only the
client knows whether their combined work was consistent and
legitimate. Apparently, when the client creates the transaction
context object, the transaction context object sets the consistency
bit to FALSE
and never deactivates itself. As a
result, the transaction is doomed unless the client calls
Commit( )
, which causes the transaction context
object to change the bit back to TRUE
and
deactivate itself, thus ending the transaction.
The client can abort the combined transaction by calling the
Abort( )
method. The transaction ends on return
from the Abort( )
method. The client is also
responsible for releasing the references it has on the internal
objects created by the transaction context object. It is a good
practice to do so even though these objects are released when the
transaction ends.
Example 4-6 shows how to use the transaction context object. In the example, the client creates the transaction context object and then uses it to create three transactional objects (as in Figure 4-12). The client votes to commit or abort the transaction, based on the combined success of the method invocations on the three objects.
Example 4-6. Using the transaction context object to create three transactional objects
HRESULT hres1 = S_OK; HRESULT hres2 = S_OK; HRESULT hres3 = S_OK; IMyInterface* pObj1= NULL; IMyInterface* pObj2= NULL; IMyInterface* pObj3= NULL; ITransactionContextEx* pTransContext = NULL; ::CoCreateInstance(CLSID_TransactionContextEx,NULL,CLSCTX_ALL, IID_ITransactionContextEx,(void**)&pTransContext); pTransContext->CreateInstance(CLSID_MyComponent,IID_IMyInterface,(void**)&pObj1); pTransContext->CreateInstance(CLSID_MyComponent,IID_IMyInterface,(void**)&pObj2); pTransContext->CreateInstance(CLSID_MyComponent,IID_IMyInterface,(void**)&pObj3); hres1 = pObj1->MyMethod( ); hres2 = pObj2->MyMethod( ); hres3 = pObj3->MyMethod( ); if(S_OK == hres1 && S_OK == hres2 && S_OK == hres3) pTransContext->Commit( ); else pTransContext->Abort( ); pObj1->Release( ); pObj2->Release( ); pObj3->Release( ); pTransContext->Release( );
Get COM & .NET Component Services now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.