Understanding COM and Enterprise Services

I l @ ve RuBoard

Understanding COM+ and Enterprise Services

You can think of COM+ as a foundation for your enterprise applications. COM+ was designed as a services framework with built-in features such as transaction processing, synchronization, and object pooling that developers can use instead of having to roll their own solutions. COM+ relieves the developer from having to worry about the core technologies and lets them focus on the actual problems being solved by their applications. In other words, COM+ services provide a solid foundation on which to build scalable and robust enterprise applications.

The subject of COM+ services is huge, and I cannot possibly do it justice in one chapter. So I will focus on a core set of features that most developers can leverage in their applications.

At the core of all of the COM+ technologies in Visual Basic .NET is the System.EnterpriseServices.ServicedComponent class. Deriving your objects from this class puts a whole set of COM+ services at your fingertips, including just-in-time activation, object pooling, and transactions.

You'll find that all classes derived from the ServicedComponent class are known as serviced components . Any serviced component can be hosted by a COM+ application and use services provided by COM+. Table 8-1 lists some of the COM+ services that can be supported by serviced components.

Table 8-1. Some COM+ Services That Can Be Supported by Serviced Components

COM+ Service

Description

Automatic Transaction Processing

Allows you to designate a class at design time that participates in transactions.

Just-In-Time Activation

Allows you to create an inactive object. The object remains inactive until a client invokes one of its methods . The runtime then creates and initializes the full object. When the call returns, COM+ deactivates the object but retains the context in memory. Once an object is deactivated, it releases all resources it has obtained during its lifetime.

Object Construction

Allows you to specify class instance initialization information, eliminating the need to hardcode this information.

Object Pooling

Allows you to reduce the overhead of working with serviced components. You can create a minimal number of components and then reuse them to service additional incoming requests .

Private Components

Allows you to protect specific serviced components from accessibility by other processes. By default, services components are accessible and published to all available clients . This service provides a mechanism for controlling the availability of your components.

Queued Components

Provides an easy way to invoke and execute components asynchronously by clients. Processing of the requests can occur regardless of the availability of the originator or the recipient of the requests.

SOAP Services

Allows you to take an existing serviced component and publish it as an XML Web service. The component will continue to be accessible through standard COM+ activation mechanisms, with the added bonus of being accessible through Web Services Description Language (WSDL) and SOAP.

Synchronization

A mechanism that prohibits components from being called by more than one client at a time. The access can flow from object to object.

Services such as transactions and queued components are configured through the use of attributes of the serviced component class. These features must be specified at design time. You can implement other services by calling methods of other serviced components or overriding methods of your own component. Some services can flow from one component to another. Transactions are an excellent example of this flow.

COM+ Requirements

To take advantage of COM+, a component must meet all of the following requirements:

  • It must inherit from the ServicedComponent class or from another class derived from ServicedComponent .

  • It must apply attributes that specify the COM+ services supported by that component.

  • The assembly containing the component must have a strong name .

Creating a Serviced Component

Serviced components can be contained in any application or assembly type. You must perform a few basic steps to implement a serviced component in your applications. You start with a new project in Visual Basic .NET and then do the following:

  1. Add a reference to System.EnterpriseServices.dll.

  2. Add an ApplicationName attribute to your project, specifying the desired name of your COM+ application.

  3. Create a class derived from the ServicedComponent class.

  4. Create a public/private key pair using the sn.exe tool by calling sn “k myKeyPair.snk in your project's directory.

  5. Add an AssemblyKeyFile attribute to your project that specifies the key file you created.

I've already explained why you need to use the ServicedComponent class, but the last two steps need some explanation. When you create a serviced component, it must be registered with Component Services before it can be used. Any component that is available through Component Services is globally available on your system. As a result, you must provide a strong name for your assembly. When we discussed primary Interop assemblies (PIAs) in Chapter 4, I introduced the Strong Name tool (sn.exe). Using this tool, you can generate a key file for signing your assembly (specified by the AssemblyKeyFile attribute). This provides a unique identity for your assembly that prevents any potential assembly and namespace confusion and makes COM+ and the .NET runtime happy.

Note

You can share a key file between multiple projects in your application if you want.


Let's see what this looks like in code. You can find this class in the SimpleComponent sample application. Our class definition is pretty simple:

 ImportsSystem.EnterpriseServices PublicClassMyServicedComponent InheritsServicedComponent EndClass 

When you create a new Visual Basic .NET project, the IDE creates a file called AssemblyInfo.vb. This is usually the best place to store all of your assembly-level attributes. You are by no means required to store all of your assembly attributes, but doing so helps keep things organized and makes your life easier. So our minor addition to this file would look like this:

 ImportsSystem.EnterpriseServices ImportsSystem.Reflection <Assembly:ApplicationName("COMPlusSample")> <Assembly:AssemblyKeyFile("myKeyFile.snk")> 

To test our application, I've created a form with a single button. (See Figure 8-1.) This button simply creates an object and then disposes of it:

 DimcAsNewMyServicedComponent() ServicedComponent.DisposeObject(c) 

This demonstrates two things. First, creating an instance of a COM+ object is simple and easy. Second, when a client is done with a component, it should call DisposeObject to ensure that the reference is correctly released.

Tip

Calling DisposeObject is critical if you need your application to be able to scale well. If you don't you call it, you'll make extremely poor use of your system's computing resources (both memory and CPU).


Figure 8-1. The SimpleComponent sample application.

graphics/f08pn01.jpg

What is not immediately obvious from this sample is that your COM+ components are self-registering. When you click the button, your application will try to create an instance of the MyServicedComponent class. When COM+ realizes that the application hasn't yet been registered, it will add your application to the COM+ catalog before completing the construction of your component. To see the results of this registration process, you can check out the Component Services management console, as shown in Figure 8-2.

Figure 8-2. Checking the Component Services console for our sample application.

graphics/f08pn02.jpg

You have other options for registering your COM+ applications. In some situations, self-registration is not feasible ”for example, if you have a component library that contains COM+ components. In this case, you need to register the component using the Service Registration utility (regsvc.exe).

Note

Registering your COM+ application causes your assembly to be added to the local machine's Global Assembly Cache (GAC). This makes the public components available to all applications on the system.


Understanding Application Activation

COM+ supports two forms of component activation: library activation and services activation. The activation dictates how your clients work with your components, and it can have a critical effect on performance. Put simply, library activation causes your component to be created within the process of the client application itself (in-process). Server activation, on the other hand, causes the components to be created in an out-of-process host application. This means that all client calls to your COM+ components are out-of-process.

In-process calls are, by definition, less expensive than equivalent out-of-process calls because an out-of-process call requires a lot of marshaling overhead to make it happen and in-process calls are directly accessible to your client's process. Another way to look at this is that library COM+ applications are duplicated for each process they're created in. If the process exits, so does the COM+ application. Server applications stand alone from all other processes on the system, regardless of what the client applications do. In addition, server applications are shared between all clients on the machine instead of being re-created for each client application. This can have certain advantages, especially when you're implementing something like object pooling ”your object pools can be global, which allows you to retain greater control over the performance of your application. (See Figure 8-3.)

Figure 8-3. How server and library applications work.

graphics/f08pn03.jpg

You can control the way a COM+ application is activated by using the ApplicationActivation assembly-level attribute. The two possible settings are ActivationOption.Library and ActivationOption.Server . In our previous example, we didn't define this attribute at all, and you might wonder how I got away with that. It turns out that the default application activation for a COM+ application in Visual Basic .NET is library. That's why I didn't need to define it explicitly. However, to be pedantic I could add a line to the AssemblyInfo.vb file that looks like this:

 <Assembly:ApplicationActivation(ActivationOption.Library)> 

If you look at the SimpleComponent sample application, you'll find that I did define this attribute, if only for completeness.

Understanding COM+ Contexts

Every COM+ component has a context . The context contains all of the information regarding the current state of an instance of a component. This context information is stored by COM+ in a context object. It is usually created when a component is activated and destroyed when it is deactivated. The context also provides information about the environment the component is running in and represents everything the component needs to know about its world. Once the context has been created, it is immutable until the context is invalidated (either through object deactivation or destruction).

When creating a context for a component, the COM+ runtime evaluates your component's attributes to determine what services are supported. These services, and their requirements, become a part of the context and represent a contract between the client and the component. This contract includes information about transactional requirements and security settings, all of which must be satisfied to allow the component to be used.

To access your component's COM+ context, you must use the ContextUtil class from System.EnterpriseServices . This class provides a host of shared properties and methods that provide information about your current context and allow you to manipulate the context in specific ways. Table 8-2 lists the shared properties supported by the ContextUtil class. All of the members of the class are shared for one important reason: your component can have access only to its own context. COM+ does not allow one component to access another component's context, so you can never have more than one instance of a context object. The only possible exception is when contexts are permitted to flow from one component to another. In that case, however, the shared context is still the only context accessible by those components. This renders the distinction moot: a component can have access to only one context at a time.

Table 8-2. Shared Properties of ContextUtil

Property

Description

ActivityId

A GUID identifying the activity containing the component. You can think of an activity, or a task, as a single logical thread of execution. A COM+ activity can span processes and machines.

ApplicationId

Gets a GUID for the current application. Each COM+ application has an associated GUID.

ApplicationInstanceId

Gets a GUID for the current application instance.

ContextId

Gets a GUID representing the current context.

DeactivateOnReturn

Gets or sets the done bit in the COM+ context.

IsInTransaction

Gets a value indicating whether the current context is transactional.

IsSecurityEnabled

Gets a value indicating whether role-based security is active in the current context.

MyTransactionVote

Gets or sets the consistent bit in the COM+ context. The consistent bit signifies the status of a transaction ”either success or failure. If this value is Commit , the transaction has been successful and the consistent bit is set to true . If the value is Abort , the COM+ context will vote to abort the current transaction and set the consistent bit to false .

PartitionId

Retrieves the GUID identifying the current partition.

Transaction

Retrieves an object describing the current COM+ Distributed Transaction Coordinator (DTC) transaction.

TransactionId

Gets the GUID of the current COM+ DTC transaction.

Most of the properties are read-only because they merely indicate the current state of your component's context. You manipulate the context by using one of the shared methods of ContextUtil (described in Table 8-3). These methods allow you to manipulate the current state of your transactions, evaluate security state, and retrieve any application-specific variables . (ASP.NET provides a set of IIS intrinsic objects that are accessible if your component is called by ASP.NET.)

Table 8-3. Shared Methods of ContextUtil

Method

Description

DisableCommit

Sets both the consistent bit and the done bit to false in the COM+ context.

EnableCommit

Sets the consistent bit to true and the done bit to false in the COM+ context.

GetNamedProperty

Returns a named property from the COM+ context.

IsCallerInRole

Determines whether the caller is in the specified role.

SetAbort

Sets the consistent bit to false and the done bit to true in the COM+ context.

SetComplete

Sets the consistent bit to true and the done bit to true in the COM+ context.

COM+ Object Construction

A difficulty arises when you try to configure a ServicedComponent . The component is activated by the COM+ runtime environment, so you have no opportunity to provide constructor parameters. This presents some challenges when you want to configure a component for use on multiple machines. The solution to this is object construction. COM+ supports an activation model that enables the passing of object construction parameters without using the object's physical constructor.

You need to do two things to support object construction. First, you must specify the ContructionEnabled attribute on your component. Second, you need to override the Construct method of the ServicedComponent class. The following example, also found in the ObjectConstruction sample, illustrates how this works:

 ImportsSystem.EnterpriseServices <ConstructionEnabled([Default]:="Hello")>_ PublicClassMyContructedObject InheritsServicedComponent ProtectedOverridesSubConstruct(ByValsAsString) 'Dostuff EndSub EndClass 

The Construct method will be called only if you first specify the ConstructionEnabled attribute. Using this attribute, you can also specify a default setting for the construction parameter. The above example provides a default value for the construction string in the attribute itself. I set the Default property of the ConstructionEnabled attribute to "Hello" . That's it.

Once we have this component, we should register it. This chapter's samples include the ObjectContruction project to demonstrate this. The project itself is just a component library, so you must manually register the assembly to see it in action. Running the regsvcs.exe tool on the assembly generates output that looks much like the following:

 Microsoft(R).NETFrameworkServicesInstallationUtilityVersion1.0.3705.0 Copyright(C)MicrosoftCorporation1998-2001.Allrightsreserved. InstalledAssembly: Assembly:C:\Samples\ObjectConstruction\bin\ObjectConstruction.dll Application:ObjectConstructionSample TypeLib:c:\samples\objectconstruction\bin\ObjectConstruction.tlb 

After we register this component, we can view its properties through the Component Services management console, shown in Figure 8-4.

Figure 8-4. Our newly registered component with object construction enabled and the correct default settings.

graphics/f08pn04.jpg

You can open the properties sheet for your component, and on the Activation tab you can see, among other things, the settings for object construction. Notice that the Constructor String text box is automatically filled in with the default construction string that we specified in our code. By changing this value in the text box, you can customize the string passed to the Construct method. These things often include connection strings, the machine name, and any other configuration settings you might think appropriate.

Object Pooling

Object pooling is a mechanism that allows you to create a reusable "pool" of certain COM+ objects. Pooling is typically used to save CPU resources by creating a set number of objects and reusing them again and again. When you have an object that requires significant startup and initialization time, being able to create it once and then reuse it allows you to amortize the original startup costs across multiple uses. It also helps improve your application's response time and general throughput because less time is spent on object creation and more time is spent doing real work.

Object pools are created on a component-by-component basis. If I want five components to be pooled, five component pools will be created. Each pool is managed independently and can be configured differently. A COM+ object pool support three parameters: MinPoolSize , MaxPoolSize , and CreationTimeout . The MinPoolSize setting specifies the minimum number of objects in the pool. On startup, COM+ will create as many objects as needed to satisfy the minimum requirement, regardless of whether there are enough clients to use them. Similarly, if an object, once used, cannot be returned to the pool, COM+ will create a new instance to take its place in the pool if it is necessary to maintain the minimum number of pooled objects.

The MaxPoolSize property sets the upper bound on the number of components allowed in a pool. Typically, if a client request comes in and there are no available objects in the pool (they are all in use), COM+ will create a new instance of the component and attempt to add it to the pool when the client is done with it. This causes the pool size to grow over time. The MaxPoolSize prevents COM+ from growing the pool to an arbitrarily large size and consuming excessive resources. If the pool reaches the maximum size, COM+ will queue incoming requests and wait for a pooled object to be made available.

The CreationTimeout property works in conjunction with the MaxPoolSize property. Because incoming requests are queued, if there are no available objects, you can use the CreationTimeout property to manage how long COM+ will wait until a pooled object is made available. If the queued request exceeds the timeout setting, an exception will be thrown.

The Life Cycle of a Pooled Object

A pooled object has a definite and predicable lifecycle starting with its creation. (See Figure 8-5.) There are two reasons for a poolable object to be created. First, a poolable object can be created by COM+ to satisfy the minimum pool size requirement. This creation is not in response to a client invocation and can happen when the pool is first created or when an already existing object cannot be returned to the pool and must be destroyed. In this case, the newly created object will be put directly into the pool and await a client request.

Figure 8-5. The life cycle of a pooled object.

graphics/f08pn05.jpg

The other possibility is that an instance of the object is created by COM+ to satisfy an incoming client request when no other pooled objects are currently available. In this case, the object is created, activated, and then passed back to the client. Otherwise, an incoming request will be satisfied by an existing unused object residing in the pool.

Regardless of why an object is created, at some point before it is passed to a client, it must first be activated. This is a way to signal the object that it must immediately perform whatever client-specific initialization is needed. Then the object reference is passed to the client and is used just like any other object. When the client is done with the object, COM+ signals it to deactivate. This is when the object performs whatever operation is necessary to eliminate any client-specific state or other information it has acquired since it was activated. This also gives the object a chance to evaluate whether it is still poolable.

Note

If an object cannot recover its original state, it should not be returned to the pool. Returning an object to a pool that hasn't reverted to its original state might result in unpredictable behavior. Not good!


Once the object has been deactivated, COM+ will ask it whether it can be returned to the object pool. If the object replies in the affirmative , it is merrily returned to the pool to await another incoming request. If the object states that it cannot be pooled, COM+ will destroy it.

Requirements for Pooled Objects

An object that supports COM+ object pooling must do several things. First and foremost, it must specify the ObjectPooling attribute in the class definition. This attribute allows you to specify not only that an object supports pooling but also various properties of the pool itself. In addition to the ObjectPooling attribute, two key methods of the ServicedComponent class that are related to pooling: Activate and Deactivate . By overriding these methods, you can customize your object's behavior as it is activated and deactivated by COM+.

A poolable object must also be stateless. This means a poolable object can retain no client-specific state between activations. This is necessary to maintain proper security, consistent behavior, and isolation. You can perform any context-specific initialization in your Activate method, but you must clean up and eliminate any context-specific state in your Deactivate method.

Your object must also be threading-model - agnostic . A poolable object can assume that it will run on a single thread at any given time. Poolable objects must run as free threads (the default) or in a multi-threaded apartment. You cannot mark a poolable object as running in an apartment-threaded thread. Poolable objects also should not use such things as thread local storage (TLS).

Other Pooling Considerations

When you define a pooled component, you must also consider what the configuration settings of the pool should be. We've already discussed the three main properties of any pool: MaxSize , MinSize , and CreationTimeout . Each of these properties has defaults, but I can't say I'd recommend them. When you create your pool, there are typically several considerations:

  • How many pooled objects should be available at startup? If your component serves in a high-performance environment, you might want it to be able to service a large number of requests right away. By specifying a pool with a minimum size, you cause that number of objects to be created when the pool is first initialized . This means you have a ready set of components to handle incoming requests when the application first starts up.

  • How long can your application take to start up? The idea behind pooling is to create expensive components once and then reuse them. The problem is that the larger the minimum size of your pool, the longer it takes to get everything initialized. Increasing the minimum pool size will cause your application to take longer to start up. Decreasing the size will cut the initialization time. This is probably something you can experiment with, and your choice will depend highly on what your requirements are.

  • How large should your pool be? Each object in the pool consumes memory, if not processing resources. Obviously, the more objects that are in the pool, the greater the impact on your application's working set. You need to understand how much in the way of system resources you can devote to a pool to help set the upper bound. Also, there are performance considerations. The larger the number of pooled objects, the greater the number of possible concurrent operations. This can consume significant computing resources, and you might need to test to find your ideal threshold. Typically, you want to limit the pool size to where you get a decreasing rate of return on additional pooled objects.

  • How long can a client wait for an object? Remember that all object requests that exceed the maximum available pooled objects will be queued until a pooled object is available. The default timeout setting is an infinite wait time, but this might not work for you, especially when a client application might be hung, waiting for the request to complete. So you have to ask yourself (or your application architects ): what is a reasonable timeout value?

Creating a Poolable Object

I've already discussed the requirements for a pooled object, but let's look at the steps for creating a basic, well-behaved poolable component:

  1. Apply the ObjectPooling attribute to your class, providing the desired settings for your object pool.

  2. Override the Activate method and add your custom activation logic.

  3. Override the Deactivate method and add your custom cleanup logic.

  4. Override the CanBePooled method to indicate your object's ability to be returned to the pool.

Here's what a pooled object might look like:

 'Enablepoolingwithaminimumsizeof10andamaximumsizeof20 'CreationTimeoutisleftatitsdefaultofinfinite. <ObjectPooling(True,10,20)>_ PublicClassMyComponent InheritsServicedComponent ProtectedOverridesSubActivate() 'Performanyinitializationyouneedhere EndSub ProtectedOverridesSubDeactivate() 'Cleanupyourobjectandeliminateanyclient-specificstate EndSub ProtectedOverridesFunctionCanBePooled()AsBoolean 'Returnavaluetoindicateyourabilitytobeplacedback 'intothepool ReturnTrue EndFunction EndClass 

Of course, this sample doesn't do anything. In this respect, it is a perfect pooled component because it retains no client state. Unfortunately, that's not particularly useful. So I created a sample called ObjectPooling (Figure 8-6) that demonstrates how object pooling works.

Figure 8-6. The ObjectPooling sample application.

graphics/f08pn06.jpg

When you launch the application and click the Cleanup button, you cause an instance of the MyPooledObject class to be created, among other things. The code is as simple as the following three lines:

 DimmyObjAsNewMyPooledObject() myObj.HelloWorld() ServicedComponent.DisposeObject(myObj) 

The other button does exactly the same thing but omits the call to Serviced ­Component.DisposeObject . What actually happens under the covers at this point is interesting. Of course, the first time you run this, the COM+ application is registered ”you know that part already. What's also interesting is the pooling mechanism's behavior. But before I get into that, let's take a look at the pooled object in question, MyPooledObject :

 ImportsSystem.EnterpriseServices <ObjectPooling(5,10)>_ PublicClassMyPooledObject InheritsServicedComponent PrivateShareds_CountAsInteger=0 Privatem_CountAsInteger Privatem_CanBePooledAsBoolean=True PublicSubNew() MyBase.New() 'Becausemorethanoneinstancemaybecreatedatonce, 'weimplementsynchronizationtoensurecorrectness m_Count=System.Threading.Interlocked.Increment(s_Count) Debug.WriteLine(m_Count& " MyPooledObject:NewCreated ") EndSub ProtectedOverridesFunctionCanBePooled()AsBoolean Debug.WriteLine(m_Count& " MyPooledObject:CanBePooledcalled") Returnm_CanBePooled EndFunction ProtectedOverridesSubActivate() 'Performactivationcode Debug.WriteLine(m_Count& " MyPooledObject:Activatecalled") EndSub ProtectedOverridesSubDeactivate() 'Performdeactivationcode Debug.WriteLine(m_Count& " MyPooledObject:Deactivatecalled") EndSub PublicFunctionHelloWorld()AsString Return "HelloWorld" EndFunction EndClass 

What's going on here? The ObjectPooling sample is intended to do one thing: demonstrate the actual behavior of COM+ object pooling. To this end, the MyPooledObject does a couple of things. First, it keeps track of the number of instances of the class and assigns each new instance a unique ID. This is implemented in the class's constructor and uses the shared variable s_Count and the member variable m_Count .

Note

You'll notice that the constructor uses the method Interlocked.Increment to increment the shared member s_Count . This is for thread safety reasons ”to prevent incorrect incrementing behavior (which can happen if two instances of MyPooledObject are created concurrently). For more information on the Interlocked class, see Chapter 3.


To make the code easier to debug, I added debug statements to each method and displayed the object's ID with each statement. I also configured the COM+ application as a library application, which means that the object pool is created in the process of the calling application. This makes it easy to see the results of the debug statements ”you only need to view the contents of the Output window in the IDE.

When I first run the application and click the Cleanup button, I get the following output:

 1MyPooledObject:NewCreated 2MyPooledObject:NewCreated 3MyPooledObject:NewCreated 4MyPooledObject:NewCreated 5MyPooledObject:NewCreated 1MyPooledObject:Activatecalled 1MyPooledObject:Deactivatecalled 1MyPooledObject:CanBePooledcalled 

This demonstrates well how the pool works. When I first try to create MyPooledComponent , COM+ sees that there's no pool and creates one. It then fills the pool with objects. (This is the reason for five consecutive calls to New .) Then COM+ activates the first component and returns it to the client. The client then calls DisposeObject when it is done. COM+ deactivates the component and calls CanBePooled . The object is then returned to the pool.

If I click the same button again, I can see that the same object is being returned again and again. Here I click the Cleanup button twice:

 1MyPooledObject:Activatecalled 1MyPooledObject:Deactivatecalled 1MyPooledObject:CanBePooledcalled 1MyPooledObject:Activatecalled 1MyPooledObject:Deactivatecalled 1MyPooledObject:CanBePooledcalled 

Something different happens when I click the Don't Cleanup button. As I mentioned before, this button omits the call to DisposeObject . This keeps the reference to the COM+ object around (at least until the next garbage collection happens ”and who knows when that will be). This is easy to see; the only calls are to Activate :

 1MyPooledObject:Activatecalled 

If I keep clicking this button, I'll quickly go through all of the objects in the pool until I run out. Then a new object will be created and returned ”just like this:

 2MyPooledObject:Activatecalled 3MyPooledObject:Activatecalled 4MyPooledObject:Activatecalled 5MyPooledObject:Activatecalled 6MyPooledObject:NewCreated 6MyPooledObject:Activatecalled 

If you keep clicking this button, the application will eventually hang because it will exceed the pool size and wait indefinitely in the request queue for an object that might never be made available. This helps highlight two things: how the pooling mechanism works, and why it is so important to call DisposeObject .

The only other thing I would point out is the way the CanBePooled method is implemented. I used a member variable to track the ability of the object to be pooled. This object might conceivably have performed an operation that it cannot cleanly recover from, thereby preventing reuse. In this situation, I would cause CanBePooled to return false ” which means the object instance will be destroyed instead of just deactivated. So any method in the class could detect an unrecoverable error that effectively invalidates the class forever. By setting this member variable to false , the object will be destroyed rather than reused.

Next, we'll move on to a related topic: JIT activation.

Just-in-Time Activation

JIT activation is another service provided by COM+. JIT activation can enable more efficient use of computing resources, especially when you're implementing a system that is intended to be scalable. If a component is marked as JIT-activated, COM+ can deactivate an instance of the object while a client still has a reference to it. Each time the client calls a method of that object, COM+ will transparently reactivate the object. In other words, objects are activated and reactivated just in time to service the incoming client request.

Clients can thus obtain references to objects and retain them for as long as needed. At the same time, server resources can be freed up on an as-needed basis. So, from the client perspective, JIT activation makes its life easier. The client doesn't need to be concerned about how it uses server resources and the potential expense. JIT activation frees the client from needing to be aggressive about releasing server resources.

Note

JIT activation isn't a substitute for cleaning up after your components. You should still call DisposeObject . But it does free you from worrying whether you should dispose of an object between method calls. (This has been done in the past to improve system scalability.)


The benefits of JIT combined with long-lived object references become greater the farther the client is from the server. Without JIT activation, the cost of activating and marshaling the object, opening the communications channel, setting up the object proxy and stubs, and so forth becomes an overwhelming performance hit. Doing whatever you can to minimize the number of expensive round-trips to the server is extremely advantageous. Each time you create an instance of a COM+ component remotely requires a round-trip, so using JIT activation with a single long-lived object reference (rather than multiple short-lived references) offers a definite performance advantage.

Note

JIT activation is often combined with object pooling because they complement each other quite nicely . One allows pooling and reuse of expensive objects, and the other allows efficient use of the allocated objects. This significantly speeds object reactivation while retaining whatever resources they might be consuming.


Creating a JIT-Activated Component

Like most of the COM+ features in Visual Basic .NET, JIT activation is enabled with the use of a class-level attribute, in this case JustInTimeActivation . There is little else you need to worry about. You can override the Activate and Deactivate methods because they are called by COM+ before and after the component is activated and reactivated. That is where you should provide any code that you need to reestablish any resources you might be using.

The following example demonstrates how you can enable a class to implement both JIT activation and object pooling:

 ImportsSystem.EnterpriseServices <ObjectPooling(5,10),JustInTimeActivation()>_ PublicClassMyJitPooledObject InheritsServicedComponent PrivateShareds_CountAsInteger=0 Privatem_CountAsInteger Privatem_CanBePooledAsBoolean=True PublicSubNew() MyBase.New() 'Becausemorethanoneinstancemaybecreatedatonce, 'weimplementsynchronizationtoensurecorrectness m_Count=System.Threading.Interlocked.Increment(s_Count) Debug.WriteLine(m_Count& " MyJitPooledObject:NewCreated ") EndSub ProtectedOverridesFunctionCanBePooled()AsBoolean Debug.WriteLine(m_Count& " MyJitPooledObject:CanBePooledcalled") Returnm_CanBePooled EndFunction ProtectedOverridesSubActivate() 'Performactivationcode Debug.WriteLine(m_Count& " MyJitPooledObject:Activatecalled") 'Thisobjcan'tbepooleduntilitisdeactivated m_CanBePooled=False EndSub ProtectedOverridesSubDeactivate() 'Performdeactivationcode Debug.WriteLine(m_Count& " MyJitPooledObject:Deactivatecalled") 'Iamre-allowingpoolingofthisobject m_CanBePooled=True EndSub PublicFunctionHelloWorld()AsString Return "HelloWorld" EndFunction EndClass 

Modeled after the MyPooledObject class from the Object Pooling section, the MyJitPooledObject provides many of the same debugging features you've already seen. You can find this class in the JustInTime sample application.

Now let's move on to the granddaddy of all COM+ technologies: transactions.

COM+ Transactions

A transaction is a collection of tasks that must all succeed, or else they will have no effect. If any part of a transaction fails, the transaction must be aborted, returning the system to its original, pre-transaction, state. Consider a situation in which you need to perform a series of actions that are highly dependent on each other ”say, transferring money from one bank account to another. In this scenario, there are two steps. First, you must deduct the correct amount from the source account. Then you need to add the correct amount to the destination account.

What if the first operation succeeds but the second fails? What happens? If you took no precautions , the money would probably be lost in the ether , never to be found again. However, if you used a transaction, both steps would be a part of a larger, single (or atomic) operation. If the second step were to fail, you would abort the transaction, which would cause the original account deduction to be undone (rolled back).

If, on the other hand, the second operation were to succeed, the transaction would go into a commit phase. This happens when all participants in a transaction agree that any changes they have made should be permanent. This is handled through a voting scheme. Each participant performs some action. When that action is completed, the participant votes to either commit or abort. If all participants vote to commit, the transaction is committed.

A transaction can be confined to a single resource (for example, a database or a message queue) or can span multiple resources (more than one database or message queue or any combination thereof). Distributed transactions allow you to integrate a series of distinct operations that span multiple systems into a single transaction. This provides a great deal of reliability in a complex system. It doesn't prevent system failures, but it can help you manage complex systems, ensure data consistency, and avoid corruption issues.

The ACID Rules

Transactions are not a new concept. In fact, the notion has been around for many years . As with anything that's been around long enough, common rules have developed that all transactions must obey. This set of rules, commonly referred to as the ACID rules , identify the role that transactions play in mission-critical applications. ACID stands for atomicity, consistency, isolation, and durability:

  • Atomicity

    A transaction is a single unit of work in which a series of operations occur. A single transaction executes exactly once, and all work done in a transaction is atomic. All operations contained in a transaction must succeed or all fail. There is no middle ground.

  • Consistency

    A transaction preserves the consistency of data. Transactions transform one consistent state of data into another consistent state of data.

  • Isolation

    Each transaction is isolated from other transactions. This permits multiple, concurrent, transactions to behave as if each is the only transaction running. As a result, each transaction will appear to be the only transaction manipulating a data store, regardless of whether or not there exist other concurrent transactions. This requires that any of the intermediate changes to a system during a transaction are not visible to any other transaction, until the entire operation has been committed.

  • Durability

    If a transaction is successful, the system guarantees that its updates will persist. Period. Regardless of whether the system crashes immediately afterwards, if the transaction has been committed, the information has been stored and will be preserved.

Creating a Transactional Component

Getting started with a transactional component is fairly straightforward. Let's walk through an example of transferring money from one bank account to another. We'll modify the previous example of a simple serviced component:

 ImportsSystem.EnterpriseServices <Transaction()>PublicClassMyTransactionalComponent InheritsServicedComponent EndClass 

We first indicate that the component supports transactions. You can do this by using the Transaction attribute. This attribute has five options: Disabled , NotSupported , Supported , Required , and RequiresNew . Each option implies a different set of rules for client applications. Table 8-4 describes each of these transactional configuration options. You can get away without specifying a transaction type because the default type, with a parameterless constructor, is Required .

Table 8-4. Values for the Transaction Attribute

Value

Description

Disabled

Eliminates the control of automatic transactions on the object. An object with this attribute value applied can engage the Distributed Transaction Coordinator (DTC) directly for transactional support.

NotSupported

Indicates that the object does not run within the scope of transactions. When a request is processed , its object context is created without a transaction, regardless of whether a transaction is active.

Supported

Indicates that the object runs in the context of an existing transaction, if one exists. If no transaction exists, the object runs without a transaction.

Required (default)

Indicates that the object requires a transaction. It runs in the scope of an existing transaction, if one exists. If no transaction exists, the object starts one.

RequiresNew

Indicates that the object requires a transaction, and a new transaction is started for each request.

We now have a basic class that supports transactions. Unfortunately, we don't seem to be doing much with it. We need to correct this. Let's look at a real example to see how we can do this.

Building a Transactional Component

The scenario I have concocted is a simple bank account application. (See Figure 8-7.) The purpose of the application is to transfer money from one account to another. To this end, I created an MSDE database called Bank that contains two user tables: Accounts and Transactions. The general idea is that a monetary transfer between accounts consists of three individual actions: a deduction from the source account, an addition to the destination account, and a new record inserted into the Transactions table. None of these operations can be performed independently. They must all succeed or all fail. Otherwise, you could end up with a database whose contents are suspect. In other words, we need to treat all three actions as a single, atomic super-action. If any part of the transaction fails, no data is affected. Period.

Figure 8-7. The Simple Transaction sample application.

graphics/f08pn07.jpg

So how do you preserve data integrity? Thankfully, you don't really need to track individual values or previous states. This sample communicates with a system that supports transactions. (MSDE, like SQL Server, supports transactions.) Therefore, if I make a call to MSDE, SQL Server, or any other DTC provider, my transactional context will automatically flow to the system. I leave the actual task of preserving the original data up to the underlying system (in this case) MSDE.

Note

If I were manipulating a custom data store, I'd have to implement my own transactional support.


The MyTransactionalClass Object

To support the features I need in the SimpleTransaction sample application, I created the MyTransactionalClass class to implement the transaction. Let's walk through it step by step.

First, we have the class definition. As you might expect, I used the Transaction attribute. The default value for the transaction type is Required , so the class supports two different scenarios. The first possibility is that the class methods are called by a client that is not part of a transaction. In this case, Required forces a new transaction to be created when the client calls into the MyTransactionalClass object. Once that initial call is completed, the transaction is either committed or aborted, depending on what happened during the transaction. The other possibility is that this class is called by a client that's already part of an existing transaction. In this case, method calls on this class will participate as a part of the larger transaction. If I decide that I don't want my class to participate in any other transaction, I can specify TransactionOption.RequiresNew , which will force a new transaction to be created each time.

I also take advantage of object construction for the connection string to the database. Rather than hardcoding the value, I decided to show one useful way to use the ConstructionEnabled attribute:

 <Transaction(),ConstructionEnabled(Default:="server=(local)\NetSdk; " &_  "Trusted_Connection=yes;database=Bank")>_ PublicClassMyTransactionalClass InheritsServicedComponent ... EndClass 

Now that I have a class definition, I need this class to actually do something. First, I create a set of three private methods that actually do the work. Remember that we're trying to tie together three separate operations: deduct, add, and insert. To implement this, I created the following methods: Deduct , Add , and InsertTransaction .

 <AutoComplete()>_ PrivateSubDeduct(ByValacctIdAsInteger,ByValamountAsSingle) DimsqlAsString sql=String.Format("UpdateAccounts " &_  "SetBalance=Balance-{0} " &_  "WhereAccountId={1}",amount,acctId) DimcmdAsNewSqlCommand(sql,m_connection) 'ThrowNewException("Thisisamanualfailure") cmd.ExecuteNonQuery() EndSub PrivateSubAdd(ByValacctIdAsInteger,ByValamountAsSingle) DimsqlAsString sql=String.Format("UpdateAccounts " &_  "SetBalance=Balance+{0} " &_  "WhereAccountId={1}",amount,acctId) DimcmdAsNewSqlCommand(sql,m_connection) cmd.ExecuteNonQuery() 'ThrowNewException("Thisisamanualfailure") ContextUtil.SetComplete() EndSub <AutoComplete()>_ PrivateSubInsertTransaction(ByValsourceAcctIdAsInteger,_ ByValtypeAsString,_ ByValAmountAsSingle,_ ByValdestAcctIdAsInteger) 'Insertarecordinthetransactiontable DimsqlAsString sql= "InsertintoTransactions " &_  "(AccountID,Type,Amount,DestAccount) " sql&=String.Format("Values({0},'{1}',{2},{3})",_ sourceAcctId,type,destAcctId,Amount) DimcmdAsNewSqlCommand(sql,m_connection) cmd.ExecuteNonQuery() 'ThrowNewException("Thisisamanualfailure") EndSub 

The same basic principle applies to all of these methods. Each performs an operation on the database using the SqlClient managed providers. The methods use a shared SqlConnection object (shown later) to do all of their work. Notice that I don't have to do anything explicit to pass the transaction context to the SqlConnection ; it's done transparently. It's actually pretty easy to forget that it is happening at all ”but it is.

You'll also notice the use of a method attribute called AutoCommit . This attribute is used to specify methods that will automatically vote to commit a transaction, unless the method terminates abnormally (generates an exception). This is handy because it frees you from having to explicitly add commit code. You can still vote to abort the transaction ”by calling the ContextUtil.SetAbort method ”if you need to. I designed these three methods to demonstrate how you can implement a method both with and without the AutoCommit attribute. You can always use the ContextUtil.SetCommit and ContextUtil.SetAbort methods explicitly if you want to, but if you can let the environment do some of the work for you, why do it yourself?

You'll also notice that all of these methods are declared as Private . This is for a good reason. No external client should be able to directly call any of these methods. I made one Public method available to allow the client to perform the balance transfer that can then call the methods that do the actual work: Transfer .

 <AutoComplete()>_ PublicFunctionTransfer(ByValsourceAcctIdAsInteger,_ ByValdestAcctIdAsInteger,_ ByValamountAsSingle)AsBoolean Deduct(sourceAcctId,amount) Add(destAcctId,amount) InsertTransaction(sourceAcctId, "Xfer",amount,destAcctId) 'ThrowNewException("Thisisamanualfailure") EndFunction 

Yet again, I used the AutoComplete attribute of this method and then called each private method. Note how easy it is to deal with transactions. The transactional context flows transparently from method to method. You don't need to do anything other than call the methods. Also note that, in most cases, the Transfer method represents the top-level method in the transaction. This means that when the method terminates normally, the transaction will commit. If, on the other hand, an exception is thrown in this method even after the other methods have been called, the transaction will still fail. Each part of the chain must succeed.

Each method includes something a little odd: a commented Throw statement. I added it because it allows you to play with the sample by uncommenting certain exceptions and seeing how it affects the transaction. You should notice that whenever an exception occurs in the transaction call hierarchy, the transaction is automatically aborted and no data is changed.

The following is the complete MyTransactionalClass so you can see everything in place. (This is also available in the SimpleTransaction sample application.)

 ImportsSystem.Data.SqlClient ImportsSystem.EnterpriseServices ImportsSystem.Reflection <Transaction(),ConstructionEnabled(Default:="server=(local)\NetSdk; " &_  "Trusted_Connection=yes;database=Bank",ObjectPooling())>_ PublicClassMyTransactionalClass InheritsServicedComponent Privatem_ConnectionStringAsString ProtectedOverridesSubConstruct(ByValsAsString) m_ConnectionString=s EndSub Privatem_connectionAsSqlConnection ProtectedOverridesSubActivate() m_connection=NewSqlConnection(m_ConnectionString) m_connection.Open() EndSub ProtectedOverridesSubDeactivate() m_connection.Close() EndSub <AutoComplete()>_ PublicFunctionTransfer(ByValsourceAcctIdAsInteger,_ ByValdestAcctIdAsInteger,_ ByValamountAsSingle)AsBoolean Deduct(sourceAcctId,amount) Add(destAcctId,amount) InsertTransaction(sourceAcctId, "Xfer",amount,destAcctId) 'ThrowNewException("Thisisamanualfailure") EndFunction <AutoComplete()>_ PrivateSubDeduct(ByValacctIdAsInteger,ByValamountAsSingle) DimsqlAsString sql=String.Format("UpdateAccounts " &_  "SetBalance=Balance-{0} " &_  "WhereAccountId={1}",amount,acctId) DimcmdAsNewSqlCommand(sql,m_connection) 'ThrowNewException("Thisisamanualfailure") cmd.ExecuteNonQuery() EndSub PrivateSubAdd(ByValacctIdAsInteger,ByValamountAsSingle) DimsqlAsString sql=String.Format("UpdateAccounts " &_  "SetBalance=Balance+{0} " &_  "WhereAccountId={1}",amount,acctId) DimcmdAsNewSqlCommand(sql,m_connection) cmd.ExecuteNonQuery() 'ThrowNewException("Thisisamanualfailure") ContextUtil.SetComplete() EndSub <AutoComplete()>_ PrivateSubInsertTransaction(ByValsourceAcctIdAsInteger,_ ByValtypeAsString,_ ByValAmountAsSingle,_ ByValdestAcctIdAsInteger) 'Insertarecordinthetransactiontable DimsqlAsString sql= "InsertintoTransactions " &_  "(AccountID,Type,Amount,DestAccount) " sql&=String.Format("Values({0},'{1}',{2},{3})",_ sourceAcctId,type,destAcctId,Amount) DimcmdAsNewSqlCommand(sql,m_connection) cmd.ExecuteNonQuery() 'ThrowNewException("Thisisamanualfailure") EndSub EndClass 

That's it for the sample. Go ahead and run it. (You'll first need to attach the database to an instance of MSDE or SQL Server.) You can play with the sample by commenting out different exceptions and seeing what effect that has.

I l @ ve RuBoard


Designing Enterprise Applications with Microsoft Visual Basic .NET
Designing Enterprise Applications with Microsoft Visual Basic .NET (Pro-Developer)
ISBN: 073561721X
EAN: 2147483647
Year: 2002
Pages: 103

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