Understanding and Using Enterprise Services
Although you know how to create and consume a serviced component, it's of little use unless you use one of the automatic services provided by COM+. In this section, you will learn how some of the key COM+ component services work and how to use them in an application. In particular, I discuss the following COM+ services:
Object Pooling
When you request that an object be created on the server, the server creates a process space, instantiates the object, and performs necessary initialization. For some large and complex objects, the number of resources consumed in creating them may be significantly high. An application may perform poorly if expensive-to-create objects are frequently created.
Wouldn't it be nice if you could maintain a pool of already created objects and then efficiently reuse them repeatedly without creating them from scratch? That's what the object pooling service of COM+ does. The object pooling service enables you to increase an application's scalability and performance by minimizing the time and resources required in creating objects repeatedly.
Configuring a Serviced Component to Use the Object Pooling Service
You can configure a class to use the object pooling service by applying the ObjectPooling attribute on the class. Figure lists various properties of the ObjectPooling attribute that you can use to configure the way object pooling works for the class.
Figure Properties of the ObjectPooling Attribute|
CreationTimeout | Specifies a length of time (in milliseconds) to wait for an object to become available in the pool before throwing an exception. | Enabled | Specifies whether object pooling is enabled for a component; the default value for this property is true. | MaxPoolSize | Specifies the maximum number of pooled objects that should be created for a component. | MinPoolSize | Specifies the minimum number of objects that should be available to a component at all times. |
In addition to using the ObjectPooling attribute, a object-pooled class also overrides the CanBePooled() method of the ServicedComponent class. The overridden version of this method should return either true or false. You'll see in the following section that an object is pooled only if the CanBePooled() method returns true.
How Object Pooling Works
At a higher level, you can think of COM+ as placing a pooling manager between the client and the server, as shown in Figure.

The pooling manager is responsible for maintaining and controlling the object pool. All client requests to the server for an object are intercepted and instead processed by the pooling manager. The pooling manager follows an internal logic as shown in Figure.

The functionality of the pooling manager can be summarized by the following list:
When the COM+ application is started, a MinPoolSize number of objects are created and thereafter maintained in the pool at all times when the application is running. Each time the pooling manager receives a request to create an object, the pooling manager checks whether the object is available in the pool. If the object is available, the pooling manager provides an already created object from the pool. If no objects are currently available in the pool, the pooling manager checks to see whether the number of objects currently in the pool has reached the MaxPoolSize. If it has not, then the pooling manager creates new objects to fulfill the request. The pooling manager tends to create as many objects as needed to keep the number of available objects at the level of MinPoolSize while not exceeding the MaxPoolSize. If no object is available and no new object can be created because of the size restriction of the pool, then the client requests are queued to receive the first available object from the pool. If an object cannot be made available within the time specified in the CreationTimeOut property, an exception is thrown. EXAM TIP
To Pool or Not to Pool?
Using the object pooling service with every application might not be a good idea. Although object pooling has benefits, it also has its own share of overheads. You should use object pooling in those applications where the benefits of object pooling exceed the overheads. Some of the scenarios suitable for object pooling are
When the costs of creating an object are relatively high. When usage costs are relatively low. When an object will be reused often. When you want to limit the number of object instances.
Some scenarios where object pooling may not be useful are
When an object is inexpensive to create. When the object does not maintain any server-specific state. When the object must be activated in the caller's context. When you do not want to restrict the number of object instances.
When the client finishes with an object, it invokes a Dispose() method on the object. The pooling manager intercepts this request and calls the CanBePooled() method on the object to check whether the object is interested in being pooled. If the method returns true, the object is stored in the object pool. On the other hand, if the CanBePooled() method returns false, then the object is destroyed. The pooling manager ensures that an optimum number of objects are always available in the object pool. If the number of available objects in the pool drops below the specified minimum, new objects are created to meet any outstanding object requests and refill the pool. If the number of available objects in the pool is greater than the minimum number, those surplus objects are destroyed during a clean-up cycle.
WARNING
Don't Pool Objects with Client-Specific States
When a client program repeatedly requests an instance of an object-pooled serviced component, there is no guarantee that the client will receive exactly the same instance of the object that it received in the earlier requests. Any object that needs to maintain client-specific state should not be pooled. Otherwise, you'll get undesirable results.
Creating an Object-Pooled Serviced Component
Step-by-Step 7.8 shows how to create an object-pooled serviced component by applying the ObjectPooling attribute and overriding the CanBePooled() method. In addition, the serviced component in Step-by-Step 7.8 also overrides the Activate() and Deactivate() methods of the ServicedComponent class. Recall from Figure that the Activate() method is invoked by enterprise services either when an object is created afresh or when an object is activated from the pool. The Deactivate() method is called just before an object is deactivated and returned to the pool or destroyed.
In Step-by-Step 7.8, I use the constructor, Activate(), and Deactivate() methods to write messages to the Windows event log. This helps you monitor how object activation and deactivation is being performed by the system.
|
7.8 Creating an Object-Pooled Serviced Component
Add a new Visual C# .NET Class library named StepByStep7_8 to the solution. In the Solution Explorer, right-click project StepByStep7_8 and select Add Reference from the context menu to add a reference to the System.EnterpriseServices component. In the Solution Explorer, rename the default Class1.cs to NorthwindSC.cs. Open the NorthwindSC.cs and replace the code with the following code:
using System;
using System.Data;
using System.Data.SqlClient;
using System.EnterpriseServices;
using System.Runtime.InteropServices;
using System.Diagnostics;
namespace StepByStep7_8
{
public interface INorthwind
{
DataSet ExecuteQuery(string strQuery);
int UpdateData(string strQuery, DataSet ds);
}
[ObjectPooling(true, 2, 4)]
[ClassInterface(ClassInterfaceType.None)]
public class NorthwindSC : ServicedComponent,
INorthwind
{
private SqlConnection sqlcnn;
private SqlDataAdapter sqlda;
private DataSet ds;
private EventLog eventLog;
public NorthwindSC()
{
// Perform expensive start up operations
// Create a Sqlconnection object
sqlcnn = new SqlConnection(
"data source=(local);" +
"initial catalog=Northwind;" +
"integrated security=SSPI");
// Create an EventLog object
// and assign its source
eventLog = new EventLog();
eventLog.Source = "NorthwindPool";
// Write an entry to the event log
eventLog.WriteEntry(
"NorthwindSC object is created" +
" and added to the object pool");
}
protected override bool CanBePooled()
{
return true;
}
protected override void Activate()
{
eventLog.WriteEntry(
"A NorthwindSC object is activated" +
" from the object pool");
}
protected override void Deactivate()
{
eventLog.WriteEntry(
"A NorthwindSC object is deactivated"+
" and is returned to the object pool");
}
// This method executes a SELECT query and
// returns the results in a DataSet object
public DataSet ExecuteQuery(string strQuery)
{
// Create a SqlDataAdapter object to
// talk to the database
sqlda =
new SqlDataAdapter(strQuery, sqlcnn);
// Create a DataSet object
// to hold the results
ds = new DataSet();
// Fill the DataSet object
sqlda.Fill(ds, "Results");
return ds;
}
// This method updates the database with
// the changes in a DataSet object
public int UpdateData(string strQuery,
DataSet ds)
{
sqlda =
new SqlDataAdapter(strQuery, sqlcnn);
// Update the database
// and return the result
SqlCommandBuilder sqlcb =
new SqlCommandBuilder(sqlda);
return sqlda.Update(ds.Tables["Results"]);
}
}
}
Select Tools, Create GUID to open the Create GUID dialog box. Select the Registry Format option in the dialog box and click the Copy button. Paste the copied GUID to the Guid attribute of the NorthwindSC class (after removing the curly brackets) as shown here:
[Guid("2166AC45-7E24-407a-967A-DD614FE8727B")]
public class NorthwindSC : ServicedComponent
Note that you will get a different GUID than the one mentioned earlier. Open the AssemblyInfo.cs file in the project and add the following using directive:
using System.EnterpriseServices;
Add the following assembly-level attributes to the AssemblyInfo.cs file:
[assembly: ApplicationName("Northwind Data " +
"Application with Object Pooling")]
[assembly: Description("Retrieve and Update data " +
"from the Northwind database")]
[assembly: ApplicationActivation(
ActivationOption.Server)]
Change the AssemblyVersion and AssemblyKeyFile attribute in the AssemblyInfo.cs file, as shown here:
[assembly: AssemblyVersion("1.0.0")]
[assembly: AssemblyKeyFile(@"..\..\..\70320.snk")]
Build the project. A StepByStep7_8.dll file is generated, and a strong name is assigned to the file based on the specified key file. Launch the Visual Studio .NET command prompt and change the directory to the folder where the DLL file generated in step 9 resides. Issue the following command to install the assembly to the GAC:
gacutil /i StepByStep7_8.dll
At the command prompt, issue the following command to install the service component assembly to the COM+ Catalog:
regsvcs StepByStep7_8.dll
|
In Step-by-Step 7.8, I selected the ActivationOption to be Server instead of Library because I want to create the object in context of the server and not that of the client.
I also registered the assembly as a COM+ application. Now when you use the Component Services administrative tool to access the properties of the NorthwindSC component in this application, you get the options to configure the object pooling parameters as shown in Figure.

Using an Object-Pooled Serviced Component
In this section, I'll demonstrate how to use an object-pooled serviced component from a client program. I'll show you two different ways to use a serviced component—the greedy approach and a non-greedy approach. You'll understand what each of these approaches is and why you should use one over the other as you proceed. I'll first start with an example of the greedy approach to call a serviced component in Step-by-Step 7.9.
|
7.9 Using an Object-Pooled Serviced Component: Greedy Approach
Add a new Visual C# .NET Windows application named StepByStep7_9 to the solution. In the Solution Explorer, right-click project StepByStep7_9 and select Add Reference from the context menu to add references to the System.EnterpriseServices and StepByStep7_8 components. In the Solution Explorer, copy the NorthwindSCClient.cs file from the StepByStep7_7 project to the current project. Open the form in code view and change all occurrences of NorthwindSCClient to refer to NorthwindSCGreedyClient instead. Also, change the namespace name to StepByStep7_9 and the using directive to refer to StepByStep7_8 instead of StepByStep7_5. Delete the default Form1.cs. Modify the btnUpdate_Click() method as follows:
private void btnUpdate_Click(
object sender, System.EventArgs e)
{
try
{
// Call the UpdateData() method of the
// NorthwindSC serviced component to update
// the database and display the number
// of updated rows in the database
int intRows = nsc.UpdateData(
txtQuery.Text,
(DataSet) dgResults.DataSource);
MessageBox.Show(String.Format(
"{0} row(s) updated", intRows),
"Row(s) Updated", MessageBoxButtons.OK,
MessageBoxIcon.Information);
// Load the updates and bind the grid
// with the updates
dgResults.DataSource =
nsc.ExecuteQuery(txtQuery.Text);
dgResults.DataMember = "Results";
}
catch(Exception ex)
{
MessageBox.Show(ex.Message, "Update Failed",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
Build the project. Set project StepByStep7_9 as the startup project. Select Debug, Start Without Debugging to launch the client form. Open the Component Services administrative tool and use the tree view on the left to navigate to the Event Viewer node. View the Application Log; you should notice that three messages from the NorthwindSC source are logged. When the application starts, the number of objects created equals the minimum size specified for the object pool. In this case, two objects are created and each of them logs a message to the event log when executing the constructor. The third message is generated from the Activate() method because the client program activates an object from the pool. Select Debug, Start Without Debugging to launch another client form. View the messages in the event log and you'll see that one new message is added to the event log. This message is generated from the Activate() method because the second serviced component object is already created and is just activated from the object pool. Now launch the third form and view the messages in the event log. You should notice this adds two more messages to the event log; one is added because the third serviced component object is created, and the other added is because the client also activates the object. Repeat the same process and launch the fourth form; you can see that two more messages are added to the event log because the fourth object is created and then activated from the pool. Now try launching the fifth form. The fifth form will not be created because the maximum size of the pool is 4, and the four objects are already created. If you wait for long (that is, the default timeout value of 60,000 milliseconds), you will get a connection timed out error. If instead you close one of the four open forms, the fifth form is launched immediately. View the event log and you will notice two messages, one from the Deactivate() method because you closed a form and another from the Activate() method because the fifth form does not create an object from scratch but instead fetches one from the object pool.
|
When the form in Step-by-Step 7.9 is loaded, it creates an object on the server and then holds the reference to this object until the form is closed. The client is holding the resource for longer than it actually needs to; therefore, I call this program a greedy client. You can also note from Step-by-Step 7.9 that the solution involving greedy clients does not scale well with an increasing number of clients.
An alternative to the greedy client is to create clients that are not greedy. That means the client occupies server resources as late as possible and releases them as early as possible. This scheme ensures that the server's resources are occupied for only the period during which they are actually used. As soon as a client frees a resource, the resource can be used by another client that may be waiting for it. Step-by-Step 7.10 shows one such solution.
|
7.10 Using an Object-Pooled Serviced Component: Non-greedy Approach
Add a new Visual C# .NET Windows Application project named StepByStep7_10 to the solution. In the Solution Explorer, right-click project StepByStep7_10 and select Add Reference from the context menu to add references to the System.EnterpriseServices and StepByStep7_8 components. In the Solution Explorer, rename the default Form1.cs to NorthwindSCNonGreedyClient.cs. Open the form in code view and change all occurrences of Form1 to refer to NorthwindSCNonGreedyClient instead. Add the following using directives:
using System.Data.SqlClient;
using StepByStep7_8;
Place two GroupBox controls, a TextBox control (txtQuery), two Button controls (btnExecute and btnUpdate), and a DataGrid control (dgResults) on the form (refer to Figure). Double-click the Button controls and add the following code to their Click event handlers:
private void btnExecute_Click(
object sender, System.EventArgs e)
{
// Declare and Instantiate the serviced component
NorthwindSC nsc= new NorthwindSC();
try
{
// Call the ExecuteQuery() method of the
// NorthwindSC serviced component to execute the
// query and bind the results to the data grid
dgResults.DataSource =
nsc.ExecuteQuery(txtQuery.Text);
dgResults.DataMember = "Results";
}
catch(Exception ex)
{
MessageBox.Show(ex.Message, "Invalid Query",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
nsc.Dispose();
}
}
private void btnUpdate_Click(
object sender, System.EventArgs e)
{
// Declare and instantiate the serviced component
NorthwindSC nsc= new NorthwindSC();
try
{
// Call the UpdateData() method of the
// NorthwindSC serviced component to update
// the database and display the number
// of updated rows in the database
int intRows = nsc.UpdateData(
txtQuery.Text,
(DataSet) dgResults.DataSource);
MessageBox.Show(String.Format(
"{0} row(s) updated", intRows),
"Row(s) Updated", MessageBoxButtons.OK,
MessageBoxIcon.Information);
// Load the updates and bind the grid
// with the updates
dgResults.DataSource =
nsc.ExecuteQuery(txtQuery.Text);
dgResults.DataMember = "Results";
}
catch(Exception ex)
{
MessageBox.Show(ex.Message, "Update Failed",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
nsc.Dispose();
}
}
Build the project. Set project StepByStep7_10 as the startup project. Open the Component Services administrative tool. If "Northwind Data Application with Object Pooling" is still running, shut down the application. This purges any existing object pool. Select Debug, Start Without Debugging to launch the client form. Open the Event Viewer and view the Application Log; you should not see any messages added to the event log because in this version of the client the object is created only when a method is invoked. Enter a query in the text box and click the Execute Query button. View the Application Event log; you should notice four messages from the NorthwindSC source. Two of these messages are generated by the constructor because 2 is the minimum size of the pool and when the application starts, that many new objects are created. The third message comes from the Activate() method when the client program activates an object from the pool. Just before the ExecuteQuery() method finishes, the method calls a Dispose() method on the serviced component object. This causes the Deactivate() method to generate the fourth message when the object is being sent back to the pool. Launch four more client forms. You will notice the fifth form is also launched, even though the maximum size of the pool is 4. This is because the object is not created when the form loads; instead the objects are created when you fire the method and are then destroyed. Execute queries in the forms; you notice that only messages from the Activate() and Deactivate() methods are added to the event log, which means that no new objects are created. The existing two objects (minimum size of the pool) in the pool can process all calls because they are tied with a client for only a small duration when a method is invoked. The object is returned to the pool as soon as the method returns. However, if you call more than two methods on the object simultaneously, then the third object is created. However, the maximum size of the pool is still not achieved and the clients don't have to wait for a server resource.
|
In Step-by-Step 7.10, I am creating the object each time within a method call and destroying it as soon as the method is completed. This ensures that the client holds a reference to the server object for only the period for which it is actually using the object.
From the perspective of conventional programming, this approach may look outright inefficient because if the client is repeatedly creating objects it will be slower compared to a client that creates an object only once and then holds onto it. However, in Step-by-Step 7.10, you can see that the benefits of scalability surpass the difference in speed. That is very important for enterprise applications, which must scale with increasing numbers of users.
In Step-by-Step 7.10, it is important for the client to call the Dispose() method on server objects as soon as the objects are not needed. If the client program doesn't do so, it will keep on holding the server resources for its lifetime. C# provides a using statement especially designed to help programmers with requirements like this. The btnExecute_Click() method can be programmed with the help of a using statement, like this:
private void btnExecute_Click(
object sender, System.EventArgs e)
{
// Declare and instantiate the serviced component
using (NorthwindSC nsc= new NorthwindSC())
{
try
{
// Call the ExecuteQuery() method of the
// NorthwindSC serviced component to
// execute the query and bind the
// results to the data grid
dgResults.DataSource =
nsc.ExecuteQuery(txtQuery.Text);
dgResults.DataMember = "Results";
}
catch(Exception ex)
{
MessageBox.Show(ex.Message, "Invalid Query",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
In this example, the NorthwindSC object is created in the scope of the using statement. C# automatically calls Dispose() on such objects when the using statement is exited.
Although the using statement provides a good option for dealing with the Dispose() problem, it is still not a good idea for an enterprise application to make scalability a factor for determining how well a client program is designed. Ideally, in these scenarios, a server application should automatically dispose of server objects as soon as the client finishes using them. So what is the solution? I'll tell you about a server-side solution to deal with the dispose problem in a forthcoming section, "Just-in-Time Activation."
NOTE
Using Object Pooling to Control Licensing
Using object pooling, you can set the upper limit for the consumption of a server resource. This feature of object pooling can help you scale your application as the number of licensed users increases. Moreover, because of the declarative nature of COM+ services, it is easy to configure this setting at runtime with minimal administrative efforts.
Monitoring Statistics of a Serviced Component
When an object writes messages to the event log, that's a way to monitor how an object is functioning. However, you may also want to aggregate information about all objects that are currently instantiated and monitor how a component as a whole is working. In this section, you'll learn how to use the Component Services administrative tool to monitor the usage statistics for a serviced component.
|
7.11 Monitoring Object Statistics of a Serviced Component
Open the Component Services administrative tool. Using the left pane, navigate to the StepByStep7_8.NorthwindSC component within the Northwind Data Application with Object Pooling COM+ application. Right-click on the component to see the Properties dialog box for the StepByStep7_8.NorthwindSC component. Select the Activation tab and check the Component supports Event and Statistics option, as shown in Figure.

Shut down the Northwind Data Application with Object Pooling COM+ application. Then start the application. Using the left pane, navigate to the Components node within the Northwind Data Application with Object Pooling COM+ application. Select Status View from the View menu. Launch several instances of the resource hog program StepByStep7_9.exe. As you instantiate and work with the program, you will see the statistics of the component in the right pane of the Component Services administrative tool as shown in Figure.

|
Figure shows what each of the status view columns for a serviced component in the right pane of the Component Services administrative tool mean:
What Each Status View Column Represents for a Serviced Component|
ProgID | Identifies a specific component. | Objects | Shows the number of objects that are currently held by the client programs. | Activated | Shows the number of objects that are currently activated. | Pooled | Shows the total number of objects created by the pooling manager. This number is the sum of objects that are in use and the objects that are deactivated. | In Call | Shows the number of objects that are currently executing some client request. | Call Time (ms) | Shows the average call duration (in milliseconds) of method calls made in the last 20 seconds (up to 20 calls). |
NOTE
Fine Tuning an Application
The initial estimated settings of the MinPoolSize, MaxPoolSize, and CreationTimeOut properties may not be optimal for your specific application and its environment. Usually administrators monitor an application's behavior and then fine-tune its configuration settings to suit to the environment. An administrator may have to experiment with several different values to reach the desired performance level. The fine-tuning of an application can be easily performed with the Component Services administrative tool.
In Step-by-Step 7.11, you checked the Component Supports Events and Statistics check box to specify that you are interested in recording the statistics for a serviced component. If you want your components to always install in the COM+ Catalog with this option turned on, you should apply the EventTrackingEnabled attribute to your class and set its Value property to true as shown in the following code snippet:
[EventTrackingEnabled(true)]
public class NorthwindPoolSC : ServicedComponent
{
...
}
Just-in-Time Activation
In the "Object Pooling" section, I used two different approaches for designing the client program:
In the first approach, a client creates an object and holds onto it until the client no longer needs it. The advantage of this approach is that it's faster because the client need not create objects repeatedly. The disadvantage is that this approach can be expensive in terms of server resources in a large-scale application. In the second approach, a client can create, use, and release an object. The next time it needs the object, it creates it again. The advantage to this technique is that it conserves server resources. The disadvantage is that as your application scales up, your performance slows down. If the object is on a remote computer, each time an object is created, there must be a network round-trip, which negatively affects performance.
Although either of these approaches might be fine for a small-scale application, as your application scales up, they're both inefficient. Moreover, in an enterprise application, the server scalability should not be a factor of how the clients are designed. The just-in-time activation service of COM+ provides a server-side solution that includes advantages of both of the previous approaches while avoiding the disadvantages of each.
How Just-in-Time Activation Works
Just-in-time activation is an automatic service provided by COM+. To use this service in a class, all you need to do is to mark the class with the JustInTimeActivation attribute set to true.
Figure shows how the just-in-time activation service works. I have also summarized the process in the following list:
When the client requests an object from the server, COM+ intercepts that request, creates a proxy, and returns the proxy to the client. The client maintains a long-lived reference to the proxy, thinking that it is a reference to the actual object. This way the client does not spend time repeatedly creating the object. The server also saves resources because it can delay creating the object until the client invokes a method on it. When the client invokes a method on the object (using the proxy), COM+ actually creates the object, calls the method, return the results, and then destroys the object. Because the objects are short-lived, the server resources are consumed for only a small period and the server is readily available to serve other waiting clients.

As shown in Figure, any JIT-activated object will be created in its own context. The context maintains a "done bit" to specify when the object will be deactivated. The interception mechanism checks the done bit after each method call finishes. If the value of the done bit is true, the object is deactivated; otherwise the object continues to exist. The default value of the done bit is false. Nevertheless, you can programmatically set the value of the done bit by using any of the following techniques:
The ContextUtil.SetComplete() or ContextUtil.SetAbort() Methods—
Usually these methods are used to vote for the success or failure of a transaction, but they also set the done bit to true.
The ContextUtil.DeactivateOnReturn Property—
When you set this property to true in a method, the property sets the done bit to true.
The AutoComplete Attribute—
When you always want to deactivate an object when the method call returns, you can apply this attribute to the method definition. This attribute automatically calls ContextUtil.SetComplete() if the method completes successfully. If the method throws an exception, the ContextUtil.SetAbort() method is invoked. In both cases the done bit is automatically set to true.
After understanding how the JIT activation works, you can appreciate how JIT activation enables scalable programming. However, the server still has lots of work to do. If the client is frequently calling methods, the server frequently creates and destroys objects. The benefits of JIT activation are beaten when the cost of object creation is significantly high. However, you already know a COM+ service that saves the cost of object creation—object pooling. What will happen if you combine JIT activation and object pooling services together for a serviced component? You will create a recipe for high throughput.
Using Just-in-Time Activation with Object Pooling—A Recipe for High Throughput
Throughput is a measure of the processing done by an application during a given time. Often, as the number of users increases, so does the competition for a server's resources. This normally results in an overall decrease in throughput.
NOTE
TPC-C Benchmarks
Microsoft Windows 2000 and COM+ hold the top six places in the Transaction Processing Performance Council's TPC-C benchmark for performance. Additionally, Microsoft Windows 2000 and COM+ also hold all ten of the top ten positions in the TPC-C benchmark for price/performance ratio. One of the important parameters of the TPC-C benchmark is application throughput. Microsoft heavily relied on object pooling and just-in-time activation services to maximize application throughput and achieve world-record performance. TPC-C benchmark results can be found at the Transaction Processing Performance Council's web site at www.tpc.org.
The JIT activation and object pooling services complement each other's features. When these services are used in combination, they can maximize the throughput for an application by providing the following benefits:
JIT activation enables clients to hold long-lived references on the server object (through a proxy) without consuming server resources. JIT activation enables the server objects to be destroyed as soon as their work is over to minimize the resource consumption on the server. Object pooling caches the already created objects and saves time by activating and deactivating the objects from the pool rather than re-creating them from scratch.
Design Considerations for Using Just-in-Time Activation and Object Pooling
When using JIT activation in your programs, you need to consider the following points:
An object's lifetime is controlled by the server instead of the client. Therefore, you need not call the Dispose() method on the server object from the client. Actually, if you do so, the object will be re-created on the server, just to be disposed of. The server does not automatically deactivate an object. You need to set the done bit to true for COM+ to destroy an object after the current method call has completed. You can use any of the techniques mentioned in the previous section to set the done bit to true. You can also configure a method administratively to control this behavior. The objects are created and destroyed after each method call. Therefore, you should consider the server object as stateless. JIT activation is not suitable for the objects that need to maintain state across method calls.
Creating a JIT-Activated Object-Pooled Serviced Component
In this section, you'll learn how to use just-in-time activation and object pooling to create an application that is more scalable and that has improved performance. In Step-by-Step 7.12, you will see how a few simple changes to a serviced component allow it to scale efficiently and support a large number of clients, without depending on the client to call the Dispose() method on the server objects.
|
7.12 Creating a JIT-Activated Serviced Component
Add a new Visual C# .NET Class library project named StepByStep7_12 to the solution. In the Solution Explorer, right-click project StepByStep7_12 and select Add Reference from the context menu to add a reference to the System.EnterpriseServices component. In the Solution Explorer, copy the NorthwindSC.cs file from the StepByStep7_8 project to the current project. Open the form in code view and change the namespace name to StepByStep7_12. Delete the default Class1.cs. Apply the JustInTimeActivation and EventTrackingEnabled attributes on the NorthWindSC class and change the GUID in the Guid attribute:
[EventTrackingEnabled(true)]
[JustInTimeActivation(true)]
[ObjectPooling(true, 2,4)]
[Guid("EBC55B5E-090A-4515-9B91-A812E74A40E7")]
[ClassInterface(ClassInterfaceType.None)]
public class NorthwindSC : ServicedComponent,
INorthwind
{
...
}
Modify the ExecuteQuery() and UpdateData() methods as follows:
// This method executes a SELECT query and
// returns the results in a DataSet object
public DataSet ExecuteQuery(string strQuery)
{
// Create a SqlDataAdapter object to
// talk to the database
sqlda =
new SqlDataAdapter(strQuery, sqlcnn);
// Create a DataSet object
// to hold the results
ds = new DataSet();
// Fill the DataSet object
sqlda.Fill(ds, "Results");
// Deactivate the object
// when the method returns
ContextUtil.DeactivateOnReturn = true;
return ds;
}
// This method updates the database with
// the changes in a DataSet object
[AutoComplete]
public int UpdateData(string strQuery, DataSet ds)
{
// Update the database
sqlda =
new SqlDataAdapter(strQuery, sqlcnn);
SqlCommandBuilder sqlcb =
new SqlCommandBuilder(sqlda);
return sqlda.Update(ds.Tables["Results"]);
}
Open the AssemblyInfo.cs file in the project and add the following using directive:
using System.EnterpriseServices;
Add the following assembly level attributes in the AssemblyInfo.cs file:
[assembly: ApplicationName("Northwind Data " +
"Application with JIT activation")]
[assembly: Description("Retrieve and Update data " +
"from the Northwind database")]
[assembly: ApplicationActivation(
ActivationOption.Server)]
Change the AssemblyVersion and AssemblyKeyFile attribute in the AssemblyInfo.cs file as shown here:
[assembly: AssemblyVersion("1.0")]
[assembly: AssemblyKeyFile(@"..\..\..\70320.snk")]
Build the project. A StepByStep7_12.dll file is generated, and a strong name is assigned to the file based on the specified key file. Launch the Visual Studio .NET command prompt and change the directory to the folder where the DLL file generated in step 9 resides. Issue the following command to install the assembly to the GAC:
gacutil /i StepByStep7_12.dll
At the command prompt, issue the following command to install the service component assembly to the COM+ Catalog:
regsvcs StepByStep7_12.dll
|
In Step-by-Step 7.12, I have applied the JustInTimeActivation attribute on the NorthwindSC class. This attribute instructs the runtime to check the value of the done bit after each method call. The object is deactivated if the done bit evaluated to true after a method call.
I use two different ways to set the done bit in this program. In the ExecuteQuery() method, I set the DeactivateOnReturn property of the ContextUtil class to true, whereas in the UpdateData() method, I apply the AutoComplete attribute. The AutoComplete attribute always deactivates the object after the method call is completed, whereas the ContextUtil.DeactivateOnReturn property is more flexible because it can be set to true or false, depending on a condition.
You can also configure the Just-in-Time Activation service using the Component Services administrative tool. To enable JIT activation for a component you need to check the Enable Just In Time Activation check box in the component's Properties dialog box, as shown in Figure.

To set the done bit after a method completes, you need to check the Automatically Deactivate This Object When This Method Returns check box in the method's Properties dialog box as shown in Figure.

Using a JIT-Activated Object-Pooled Serviced Component
Step-by-Step 7.13 demonstrates how to use a JIT-activated object-pooled serviced component. This program is similar to that of Step-by-Step 7.10, but is simpler because this time the server automatically takes care of disposing of the server objects and thus the client is not required to call Dispose() on the server objects.
|
7.13 Using a JIT-Activated Serviced Component
Add a new Visual C# .NET Windows Application project named StepByStep7_13 to the solution. In the Solution Explorer, right-click project StepByStep7_13 and select Add Reference from the context menu to add references to the System.EnterpriseServices and StepByStep7_12 components. In the Solution Explorer, copy the form NorthwindSCNonGreedyClient.cs from the StepByStep7_10 project to the current project. Delete the default Form1.cs. Open the form in code view and change using StepByStep7_8 to using StepByStep7_12 instead. Change the namespace to StepByStep7_13. Remove the finally block from the btnExecute_Click() and btnUpdate_Click() methods. Build the project. Set the project StepByStep7_13 as the startup project.
|
Run the project as you did in Step-by-Step 7.10. Note that the results are the same despite the fact that the client program is not calling Dispose() on the server objects.
Object Construction
The object construction service of COM+ allows you to specify initialization information for a serviced component. The advantage of using the construction service is that it enables you to configure the string externally, using tools such as the Component Services administrative tool.
To use the object construction service in a serviced component you need to do the following:
Apply the ConstructionEnabled Attribute—
You need to apply the ConstructionEnabled attribute on a class that uses the object construction service. This attribute has two properties: Enabled and Default. The default values of these properties are true and empty string, respectively. The Default property specifies the constructor string.
Override the Construct() Method—
To receive the currently configured constructor string from the COM+ Catalog, an object must override the Construct() method of the ServicedComponent class. When the ConstructionEnabled attribute is true, enterprise services calls this method automatically after the constructor of the component has been executed. The Construct() method receives the currently configured construction string as its only parameter.
NOTE
Using Object Construction with Object Pooling
You can pair object construction with object pooling to have more control over how the clients reuse resources. For example, you can design a generic serviced component that exposes resources to the clients based on the constructor string. You can install this component several times in a COM+ application (ensuring that each component has a different CLSID) and then assign each of these components a distinct constructor string. The COM+ application now has distinct pools of objects, each usable by distinct groups of clients.
After the ConstructionEnabled attribute is applied, you can configure the constructor string by using the component's property sheet in the Component Services administrative tool.
The COM+ object pooling service enables you to maintain already created instances of a component that are ready to be used by any client that requests the component. To use the object pooling service, a class must override the CanBePooled() method of the ServicedComponent class. In addition, you need to apply the ObjectPooling attribute on the class or configure the component via the Component Services administrative tool to use object pooling. The COM+ Just-in-Time activation service allows you to minimize the period of time an object lives and consumes resources on the server. You can enable the just-in-time activation service declaratively in your programs by using the JustInTimeActivation attribute or administratively via the Component Services administrative tool. You must, however, indicate to COM+ whether an object should be deactivated after a method call by setting the done bit to true. The COM+ object construction service allows you to specify initialization information for a serviced component. It is possible to use the Component Services administrative tool and configure the initialization information for a component to change the way a component behaves.
|
|
You need to modify the serviced component created in Step-by-Step 7.10 in such a way that system administrators should be able to configure the database connection string for the component. To do this, there should not be a need to recompile the application. You do not want to log events to the event log from the serviced component.
How would you create such a serviced component and then configure the component with the help of Component Services administrative tool?
This exercise helps you practice creating serviced components that use COM+ services. You should try working through this problem on your own first. If you are stuck, or if you would like to see one possible solution, follow these steps:
Add a new Visual C# .NET Class library named GuidedPracticeExercise7_2 to the solution. In the Solution Explorer, right-click the project GuidedPracticeExercise7_2 and select Add Reference from the context menu to add reference to the System.EnterpriseServices component. In the Solution Explorer, rename the default Class1.cs to NorthwindSC.cs. Open the NorthwindSC.cs and replace the code with the following code:
using System;
using System.Data;
using System.Data.SqlClient;
using System.EnterpriseServices;
using System.Runtime.InteropServices;
namespace GuidedPracticeExercise7_2
{
public interface INorthwind
{
DataSet ExecuteQuery(string strQuery);
int UpdateData(string strQuery, DataSet ds);
}
// Default value for the Construction
// string specified here
[ConstructionEnabled(
Default="data source=(local);" +
"initial catalog=Northwind;" +
"integrated security=SSPI")]
[Guid("18C7C90F-FD2C-4a1a-88D4-E8FDEC530CDA")]
[EventTrackingEnabled(true)]
[JustInTimeActivation(true)]
[ObjectPooling(true, 2,4)]
[ClassInterface(ClassInterfaceType.None)]
public class NorthwindSC : ServicedComponent,
INorthwind
{
private SqlConnection sqlcnn;
private SqlDataAdapter sqlda;
private DataSet ds;
public NorthwindSC()
{
}
protected override void Construct(string s)
{
// Open a connection to the specified
// sample SQL Server database
sqlcnn = new SqlConnection(s);
}
protected override bool CanBePooled()
{
return true;
}
// This method executes a SELECT query and
// returns the results in a DataSet object
public DataSet ExecuteQuery(string strQuery)
{
// Create a SqlDataAdapter object to
// talk to the database
sqlda =
new SqlDataAdapter(strQuery, sqlcnn);
// Create a DataSet object
// to hold the results
ds = new DataSet();
// Fill the DataSet object
sqlda.Fill(ds, "Results");
// Deactivate the object
// when the method returns
ContextUtil.DeactivateOnReturn = true;
return ds;
}
// This method updates the database with
// the changes in a DataSet object
[AutoComplete]
public int UpdateData(string strQuery,
DataSet ds)
{
// Update the database
sqlda =
new SqlDataAdapter(strQuery, sqlcnn);
// Update the database
// and return the result
SqlCommandBuilder sqlcb =
new SqlCommandBuilder(sqlda);
return sqlda.Update(ds.Tables["Results"]);
}
}
}
Open the AssemblyInfo.cs file in the project and add the following using directive:
using System.EnterpriseServices;
Add the following attributes to the AssemblyInfo.cs file:
[assembly: ApplicationName("Northwind Data " +
"Application with Object Construction")]
[assembly: Description("Retrieve and Update data " +
"from the Northwind database")]
[assembly: ApplicationActivation(
ActivationOption.Server)]
Change the AssemblyVersion and AssemblyKeyFile attribute in the AssemblyInfo.cs file as shown here:
[assembly: AssemblyVersion("1.0.0")]
[assembly: AssemblyKeyFile(@"..\..\..\70320.snk")]
Build the project. A GuidedPracticeExercise7_2.dll file is generated, and a strong name is assigned to the file based on the specified key file. Launch the Visual Studio .NET command prompt and change the directory to GuidedPracticeExercise7_2.dll file, and issue the following command to install the assembly to the GAC:
gacutil /i GuidedPracticeExercise7_2.dll
At the command prompt, issue the following command to install the service component assembly to the COM+ Catalog:
regsvcs GuidedPracticeExercise7_2.dll
Open the Component Services Administrative tool by selecting Administrative Tools, Component Services, Computers, My Computer, COM+ Applications from the Windows Control Panel. You should be able to see that Northwind Data Application with Object Construction has been added to the COM+ Catalog. Right-click the NorthwindConstructSC serviced component and select Properties from the context menu. Click the Activation tab; notice that the Enable Object Construction check box is selected and the Constructor string text box contains the connection string supplied to the ConstructionEnabled attribute in the NorthwindSC.cs file, as shown in Figure.

Guided Practice Exercise 7.2 registers the serviced component in the COM+ Catalog. Before you can use the component from a client application, you should deploy the component to the GAC. The design of the client application is unaffected by the application of the ConstructionEnabled attribute. You can easily modify the client program in Step-by-Step 7.13 to work with this serviced component.
If you have difficulty following this exercise, review the sections "Object Pooling," "Just-in-Time Activation," and "Object Construction," earlier in this chapter. After doing that review, try this exercise again. |
Automatic Transaction Processing
As discussed earlier, in the section "Microsoft Transaction Server (MTS)," a transaction is a series of operations performed as a single unit. A transaction is successful only when all the operations in that transaction succeed.
For example, consider a banking application that needs to perform a balance transfer from account A to account B for amount X. In this transaction, two atomic operations are involved. First, the balance in account A should be decreased by amount X, and second, the balance in account B should increased by amount X. For the balance transfer action to succeed, both of these atomic operations should succeed. If one of the actions fails, the system is in an inconsistent state (with wrong account balances) and the complete operation should be undone (rolled back). However, if both the atomic operations succeed, the balance transfer action also succeeds and the changes can be made permanent (committed).
Using transactions to ensure the reliability of applications is not a new concept. Transaction processing has been a part of database management systems long before the concept of transactions came to business objects. The COM+ transaction processing mechanism provides two significant advantages over traditional transaction processing techniques:
Distributed Transactions—
A local transaction is one whose scope is limited to a single transactional resource, such as a SQL Server database. On the other hand, a distributed transaction can span over several transaction-aware resources. The transaction-aware resources in a distributed transaction may be heterogeneous (such as a SQL Server database, Oracle database, or a Microsoft Messaging Queue) and may involve multiple computers over a network.
Automatic Transactions—
A typical database transaction, such as the one implemented with Transact SQL code or ADO.NET code, is manual in nature. With manual transactions, you explicitly begin and end the transaction and implement all the necessary logic to take care of the commit and rollback process. However, COM+ allows automatic transaction services that you can use in your program without writing any additional code. COM+ implements this with the help of a Windows service called Microsoft Distributed Transaction Coordinator (MSDTC).
In this section, I discuss how the COM+ automatic transaction processing service works and how to use this service to implement transactions across single-transaction–aware resources, as well as multiple-transaction–aware resources residing on separate processes.
Using Automatic Transaction Service for Local Transactions
Before I get any further, let me give you first-hand exposure to using automatic transaction processing for a local transaction in Step-by-Step 7.14.
In this example, I use the serviced component created in Step-by-Step 7.12 and demonstrate how you can use COM+ automatic transaction services without writing even a single line of additional code.
|
7.14 Using a Local Automatic Transaction Processing Service
Start the Windows Application StepByStep7_13.exe. This is the client application that uses an object-pooled and just-in-time–enabled serviced component (StepByStep7_12.NorthwindSC). Together they enable you to query and update data from the Northwind sample database. In the Windows form, type the following query and click on the Execute Query button.
SELECT * FROM CUSTOMERS
You see that a list of customers is displayed. Change the ContactName for a customer (for example, change "Maria Anders" to "Maria Anderson"). In the next record, change the CustomerID to any string of your choice (for example, change "ANATR" to "ANAT"). Click on the Update button. You get an error message because the CustomerID field has a check for referential integrity and the database doesn't allow you to change that. Click on the Execute Query button. The fresh result shows that although you got an error in the previous execution, the database saved the record where you modified the ContactName but did not save the erroneous record where you modified the CustomerID. You now decide that both the database update operations performed in the previous step should be part of a transaction. If both of them succeed, the database should be updated with the changes; otherwise, the record should be rolled back to its old values. Open the Component Services administrative tool. Open the property sheet for the StepByStep7_12.NorthwindSC serviced component. Select the Transactions tab and select the Required option in the Transaction Support group box, as shown in Figure.

Switch to the Windows form and click on the Execute Query button for the same query. Now try updating the ContactName in one row and CustomerID in another row and click on the Update button. You get the same error message that you got in step 3. To verify the changes to the database, click on the Execute Query button and observe the results. Notice that both ContactName and CustomerID are unchanged. When you got an error while updating data, COM+ rolled back the complete operation, leaving the database in its original shape.
|
In Step-by-Step 7.14, you use the Component Services administrative tool to configure the automatic transaction service for a component. Like the other COM+ services, you can also accomplish this by writing declarative attributes in your programs. You'll learn about various attributes related to transaction processing in the next few sections.
Elements of Transaction Processing
The System.EnterpriseServices namespace provides various classes to work with transactions in your programs. These classes hide the complexity of automatic transaction processing and provide most of the functionality with declarative attributes. Under the covers, these classes negotiate with COM+ and MSDTC services to implement the transaction.
In this section, you'll learn how these classes work together to provide automatic transaction services.
The Transaction Attribute
Applying the Transaction attribute to a class is equivalent to enabling transactions through the Component Services administrative tool. The benefit of applying an attribute in the program is that the component can execute in a pre-configured state straight out of the box. The Transaction attribute takes a value from the TransactionOption enumeration to specify how a component participates in a transaction. The values of the TransactionOption enumeration are listed in Figure.
Figure Members of the TransactionOption Enumeration|
Disabled | Specifies that the component's capability to participate with COM+ in managing transactions has been disabled. This setting is used for compatibility reasons only. | NotSupported | Specifies that the component will never participate in transactions. | Required | Specifies that the component uses transactional resources such as databases and will participate in a transaction if one already exists; otherwise a new transaction must be created. | RequiresNew | Specifies that the component requires a new transaction to be created even if a transaction already exists. | Supported | Specifies that the component will participate in a transaction if one already exists. This setting is mostly used by components that do not themselves use any transactional resources. |
For example, ComponentA may use the following code to request that a new transaction be created:
[Transaction(TransactionOption.RequiresNew)]
class ComponentA : ServicedComponent
{
...
}
EXAM TIP
JIT Activation Is Required with Automatic Transaction Processing
To preserve the consistency of a transaction, a component must not carry state from one transaction to another. To enforce statelessness for all transactional components, COM+ uses JIT activation. JIT activation forces an object to deactivate and lose state before the object can be activated in another transaction.
For a component, if you apply the Transaction attribute and set its value to TransactionOption.Supported, TransactionOption.Required, or TransactionOption.RequiresNew, then COM+ automatically sets the JustInTimeActivation attribute to true.
When a transaction is created, it is uniquely identified by a transaction id. Several components can share a transaction, as in the following, when ComponentA calls a method on ComponentB, which has been defined as
[Transaction(TransactionOption.Supported)]
class ComponentB : ServicedComponent
{
...
}
Then ComponentB shares the transaction started by ComponentA. In this case, the tasks accomplish by ComponentA and ComponentB belong to the same unit of transaction. As a result, these tasks fail and succeed together.
Context and Transaction
Each component that participates in a transaction has its own context. The context stores various flags that specify an object's state. Two such flags are the done bit and the consistent bit. In addition to objects, the transaction itself also has a context. The context of a transaction maintains an abort bit. The purpose of the abort bit is to determine whether the transaction as a whole failed or succeeded. I have summarized these bits and their influence on the outcome of a transaction in Figure.
The Abort, Done, and Consistent Bit and Their Effect on the Transaction Outcome|
abort | Entire transaction | This bit is also called the doomed bit. COM+ sets this bit to false when creating a new transaction. In a transaction lifetime, if this bit is set to true, it cannot be changed back. | If the abort bit is set to true, then the transaction is aborted. | consistent | Each context | This bit is also called the happy bit. COM+ sets this bit to true when creating an object. A programmer can choose to set this bit to true or false, depending on the program logic to indicate that the object is either consistent or inconsistent. | If the consistent bit in any context is set to false, then the transaction is aborted. If the consistent bit in all the contexts is set to true, then the transaction is committed. | done | Each context | Each COM+ object that participates in a transaction must also support just-in-time activation and therefore must maintain a done bit. When a method call begins, the done bit is set to false. When a method call finishes, COM+ checks the status of the done bit. If the bit is true then the active object is deactivated. | When exiting a method, if the done bit is set to true and the consistent bit is set to false then the abort bit is set to true. |
The .NET enterprise services library provides the ContextUtil class to work with an object's context. Figure shows those methods of the ContextUtil class that influence an object's done bit and its consistent bit.
Figure How the Methods of the ContextUtil Class Affect the Consistent Bit and the Done Bit|
DisableCommit() | false | false | EnableCommit() | true | false | SetAbort() | false | true | SetComplete() | true | true |
How Automatic Transaction Works
In this section, you learn how the COM+ automatic transaction service works in a distributed scenario. Consider a scenario as shown in Figure. It shows an Ordering application that is divided into four layers:
Sales representatives use a Windows application to enter the order that they receive over the telephone. The client Windows application interacts with the Ordering application for order fulfillment. The Ordering application works as a service provider and interacts with other applications to fulfill an order. The main objective of this application is to keep the client from knowing how an order is processed. This scheme gives you flexibility in changing processes at the server side without making any changes to the client program. The Shipping application knows how to ship an order and the Billing application knows how to bill customers. These applications can reside on the same computer as the Ordering application, or may reside on different computers. If the applications are on different computers, they can use technologies such as remoting or XML Web services to communicate with the Ordering application. The Shipping application and the Billing application maintain their own sets of data that are independent of each other. The databases themselves may reside on different servers.

In this scenario, the need for transactions is clear. When a customer places an order, the order should be both billed and shipped as an integrated unit of work. Just billing the customer without shipping anything or vice-versa is not what most organizations want to do.
Let us now see how the automatic transaction service works in this scenario. For the sake of simplicity, I assume that each of the Ordering, Billing, and Shipping applications have just one component. The name of the component is the same as the name of application. Figure shows how these components interact with each other to create a transaction.

The process in Figure is explained in the following steps:
When the client instantiates the Ordering component, an object context is created. The done bit is set to false, whereas the consistent bit is set to true. COM+ intercepts object invocation to check whether transaction services are needed. The Ordering component has a Transaction attribute set to the TransactionOption.RequiresNew value. COM+ creates a new transaction and sets the abort bit to false. The Ordering component is designated as the root object of the transaction. A root object coordinates with all other objects in a transaction. The client fires a method on the Ordering component.
NOTE
MSDTC and Two-phase Commit
In a transaction, MSDTC works in two phases. The first phase is the prepare phase in which MSDTC interacts with resources managers for resources such as databases, message queues, and so on, and asks them to record new and old values in a durable place. Based on the resource manager's feedback, MSDTC determines the success or failure of an operation.
The second phase is the commit phase. In this phase, MSDTC asks all the individual resource managers to perform a commit operation on their resources. MSDTC collects the votes, and if all the commit operations are successful, then MSDTC instructs all resource managers to make the changes permanent. Otherwise, if any of the commits failed, MSDTC instructs all the resource managers to roll back their operations based on the information that they collected in the prepare phase. When the Ordering component instantiates the Billing component, COM+ intercepts to check whether transaction services are needed. The Billing component has a Transaction attribute set to the TransactionOption.Supported value. COM+ determines that the Billing component can support the transaction started by the Ordering component and extends the scope of the transaction to cover the Billing component. The context of the Billing object is initialized with the consistent bit set to true and the done bit set to false. The Ordering component calls a method on the Billing component. The Billing object interacts with a SQL Server database to update a table of confirmed shipments. COM+ takes the help of MSDTC to record any new and old changes done to the database so that the changes can be rolled back at a later stage. If the update is successful, the consistent bit is set to true, but if there were any errors then the consistent bit is set to false. If the Billing object wishes to deactivate itself after the method call, it sets the done bit to true; otherwise the done bit remains set to false. If the done bit is true and the consistent bit is false then the abort bit of the transaction is set to true and the control transfers to Step 6. When the method returns from the Billing object, COM+ intercepts the call and records the status of the consistent bit. If the done bit is true, the Billing object is deactivated. The control comes back to the Ordering object and the Ordering component now repeats steps 2 to 4 to instantiate the Shipping component and invoke a method on the resulting object. The control comes to the Ordering object. COM+ checks the status of the abort bit. If the abort bit is true then the entire transaction is aborted and COM+ requests DTC to roll back any changes that were made to the database. If the abort bit is false then the status of all the consistent bits is checked. If any of these bits is false then the transaction is aborted and a rollback is performed. If all the consistent bits are true, then COM+ requests DTC to finally commit all the changes to the database. Finally, the root object sets its done bit to true and returns from the method that was invoked by the client. COM+ intercepts to deactivate the root object and destroys the transaction. The control is transferred to the client.
In the preceding steps, if you wish to programmatically control the success or failure of an operation, you can do so by calling the methods of the ContextUtil class (see Figure). However, a common choice is to apply the AutoComplete attribute on the method call. This attribute automatically calls ContextUtil.SetComplete() if the method completes successfully. Otherwise if the method throws an exception, ContextUtil.SetAbort() is called to abort the transaction.
Using Automatic Transaction Service for Distributed Transactions
Now that you are familiar with how transactions work, it's time to write a program that makes use of automatic transaction services in a distributed application. In this section, I use the distributed Ordering application already discussed and write a Shipping component, a Billing component, an Ordering component and a client application.
Step-by-Step 7.15 shows how to create a Shipping component that supports transactions and updates a database table with shipping records.
|
7.15 Using Distributed Transactions: Creating a Shipping Component
Create a new table named Shipping in the Northwind database with the structure shown in Figure.
Figure Design of the Shipping Table|
ShippingID | int | 4 | No | Yes | CustomerID | nchar | 5 | No | No | ProductID | int | 4 | No | No | DateTime | datetime | 8 | No | No |
Add a new Visual C# .NET Class library named StepByStep7_15 to the solution. In the Solution Explorer, right-click project StepByStep7_15 and select Add Reference from the context menu to add a reference to the System.EnterpriseServices component. In the Solution Explorer, rename the default Class1.cs to Shipping.cs. Open Shipping.cs and replace the code with the following code:
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using System.EnterpriseServices;
using System.Runtime.InteropServices;
namespace StepByStep7_15
{
public interface IShipping
{
void ShipItem(
string customerID, int productID);
}
[Transaction(TransactionOption.Supported)]
[ClassInterface(ClassInterfaceType.None)]
[Guid("660E0672-54E8-443f-9946-23C22E248034")]
public class Shipping : ServicedComponent,
IShipping
{
SqlConnection sqlConn;
public Shipping()
{
sqlConn = new SqlConnection(
"data source=(local);" +
"initial catalog=Northwind;" +
"integrated security=SSPI");
}
[AutoComplete(true)]
public void ShipItem(string customerID,
int productID)
{
SqlCommand sqlCmd = new SqlCommand();
SqlDateTime dt =
new SqlDateTime(DateTime.Now);
sqlCmd.CommandText = String.Format(
"insert into Shipping (CustomerID, " +
"ProductID, DateTime) values " +
"('{0}', {1}, '{2}')",
customerID, productID, dt);
sqlCmd.Connection = sqlConn;
sqlConn.Open();
sqlCmd.ExecuteNonQuery();
}
}
}
Open the AssemblyInfo.cs file in the project and add the following using directive:
using System.EnterpriseServices;
Add the following assembly-level attributes to the AssemblyInfo.cs file:
[assembly: ApplicationName("Shipping Application")]
[assembly: Description("Ship Orders")]
[assembly: ApplicationActivation(
ActivationOption.Server)]
Change the AssemblyVersion and AssemblyKeyFile attribute in the AssemblyInfo.cs file as shown here:
[assembly: AssemblyVersion("1.0.0")]
[assembly: AssemblyKeyFile(@"..\..\..\70320.snk")]
Build the project. A StepByStep7_15.dll file is generated, and a strong name is assigned to the file based on the specified key file. Launch the Visual Studio .NET command prompt and change the directory to the folder where the DLL file generated in step 9 resides. Issue the following command to install the assembly to the GAC:
gacutil /i StepByStep7_15.dll
At the command prompt, issue the following command to install the service component assembly to the COM+ Catalog:
regsvcs StepByStep7_15.dll
|
Note that in Step-by-Step 7.15 I used the TransactionOption.Supported option because the Shipping component is not invoked on its own. Instead, this component is invoked by the Ordering application as part of the order fulfillment process.
Step-by-Step 7.16 shows how to create a Billing component that supports transactions and updates a database table with billing records.
|
7.16 Using Distributed Transactions: Creating a Billing Component
Create a new table named Billing in the Northwind database with the structure shown in Figure.
Figure Design of the Billing Table|
BillingID | int | 4 | No | Yes | CustomerID | nchar | 5 | No | No | ProductID | int | 4 | No | No | DateTime | datetime | 8 | No | No |
Add a new Visual C# .NET Class library named StepByStep7_16 to the solution. In the Solution Explorer, right-click project StepByStep7_16 and select Add Reference from the context menu to add a reference to the System.EnterpriseServices component. In the Solution Explorer, rename the default Class1.cs to Billing.cs. Open Billing.cs and replace the code with the following code:
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using System.EnterpriseServices;
using System.Runtime.InteropServices;
namespace StepByStep7_16
{
public interface IBilling
{
void BillCustomer(
string customerID, int productID);
}
[Transaction(TransactionOption.Supported)]
[ClassInterface(ClassInterfaceType.None)]
[Guid("2F4B34B2-6140-41ce-9B57-AC7491F31203")]
public class Billing : ServicedComponent,
IBilling
{
SqlConnection sqlConn;
public Billing()
{
sqlConn = new SqlConnection(
"data source=(local);" +
"initial catalog=Northwind;" +
"integrated security=SSPI");
}
[AutoComplete(true)]
public void BillCustomer(string customerID,
int productID)
{
SqlCommand sqlCmd = new SqlCommand();
SqlDateTime dt =
new SqlDateTime(DateTime.Now);
sqlCmd.CommandText = String.Format(
"insert into Billing (CustomerID, " +
"ProductID, DateTime) values " +
"('{0}', {1}, '{2}')",
customerID, productID, dt);
sqlCmd.Connection = sqlConn;
sqlConn.Open();
sqlCmd.ExecuteNonQuery();
}
}
}
Open the AssemblyInfo.cs file in the project and add the following using directive:
using System.EnterpriseServices;
Add the following assembly-level attributes to the AssemblyInfo.cs file:
[assembly: ApplicationName("Billing Application")]
[assembly: Description("Bill Customers")]
[assembly: ApplicationActivation(
ActivationOption.Server)]
Change the AssemblyVersion and AssemblyKeyFile attributes in the AssemblyInfo.cs file as shown here:
[assembly: AssemblyVersion("1.0.0")]
[assembly: AssemblyKeyFile(@"..\..\..\70320.snk")]
Build the project. A StepByStep7_16.dll file is generated, and a strong name is assigned to the file based on the specified key file. Launch the Visual Studio .NET command prompt and change the directory to the folder where the DLL file generated in step 9 resides. Issue the following command to install the assembly to the GAC:
gacutil /i StepByStep7_16.dll
At the command prompt, issue the following command to install the service component assembly to the COM+ Catalog:
regsvcs StepByStep7_16.dll
|
Note that in Step-by-Step 7.16, I used the TransactionOption.Supported option because the Billing component is not invoked on its own. Instead, this component is invoked by the Ordering component as part of the order fulfillment process. Step-by-Step 7.17 shows how to create such an Ordering component.
|
7.17 Using Distributed Transactions: Creating an Ordering Component
Add a new Visual C# .NET Class library named StepByStep7_17 to the solution. In the Solution Explorer, right-click project StepByStep7_17 and select Add Reference from the context menu to add references to the System.EnterpriseServices component and to the projects StepByStep7_15 and StepByStep7_16. In the Solution Explorer, rename the default Class1.cs to Ordering.cs. Open Ordering.cs and replace the code with the following code:
using System;
using System.EnterpriseServices;
using System.Runtime.InteropServices;
using StepByStep7_15;
using StepByStep7_16;
namespace StepByStep7_17
{
public interface IOrdering
{
void PlaceOrder(
string customerID, int productID);
}
[Transaction(TransactionOption.RequiresNew)]
[ClassInterface(ClassInterfaceType.None)]
[Guid("C261BA4C-0B3E-49a7-87B5-7F6F909726A1")]
public class Ordering : ServicedComponent,
IOrdering
{
public Ordering()
{
}
[AutoComplete(true)]
public void PlaceOrder(string customerID,
int productID)
{
Billing billing = new Billing();
billing.BillCustomer(
customerID, productID);
Shipping shipping = new Shipping();
shipping.ShipItem(
customerID, productID);
}
}
}
Open the AssemblyInfo.cs file in the project and add the following using directive:
using System.EnterpriseServices;
Add the following assembly-level attributes to the AssemblyInfo.cs file:
[assembly: ApplicationName("Ordering Application")]
[assembly: Description("Places an order")]
[assembly: ApplicationActivation(
ActivationOption.Server)]
Change the AssemblyVersion and AssemblyKeyFile attribute in the AssemblyInfo.cs file as shown here:
[assembly: AssemblyVersion("1.0.0")]
[assembly: AssemblyKeyFile(@"..\..\..\70320.snk")]
Build the project. A StepByStep7_17.dll file is generated, and a strong name is assigned to the file based on the specified key file. Launch the Visual Studio .NET command prompt and change the directory to the folder where the DLL file generated in step 8 resides. Issue the following command to install the assembly to the GAC:
gacutil /i StepByStep7_17.dll
At the command prompt, issue the following command to install the service component assembly to the COM+ Catalog:
regsvcs StepByStep7_17.dll
|
The Ordering component needs to work as a root object for a transaction. For this reason the Transaction attribute in Step-by-Step 7.17 uses the value TransactionOption.RequiresNew.
Now you have all the server-side components ready. In Step-by-Step 7.18, I create a client application that calls the Ordering component. I have created the client application as a Windows application; however, creating the client program as a Web application is not much different.
|
7.18 Using Distributed Transactions: Creating a Client Order Form
Add a new Visual C# .NET Windows application named StepByStep7_18 to the solution. In the Solution Explorer, right-click project StepByStep7_18 and select Add Reference from the context menu to add references to the System.EnterpriseServices and StepByStep7_17 components. In the Solution Explorer, rename the default Form1.cs to OrderForm.cs. Open the form in code view and change all occurrences of Form1 to refer to OrderForm instead. Add the following using directives:
using System.Data.SqlClient;
using StepByStep7_17;
Place one GroupBox control, two Label controls, two ComboBox controls (cboCustomers and cboProducts) and one Button control (btnPlaceOrder) on the form. Arrange the controls as shown in Figure.

Open Server Explorer and drag the Customers and Products table from the Northwind data connection node to the form. A SqlConnection object and two SqlDataAdapter objects are created on the form. Right-click the first SqlDataAdapter object and select Generate DataSet from the context menu. In the Generate DataSet dialog box, choose the New radio button and name the new DataSet dsOrders. Select the Customers table and click OK. Right-click the second SqlDataAdapter object and select Generate DataSet from the context menu. In the Generate DataSet dialog box, choose the existing DataSet dsOrders. Select the Products table and click OK. Set the DataSource property to dsOrders1.Customers, DisplayMember to ContactName, ValueMember to CustomerID, and DropDownStyle to DropDownList for the cboCustomers combo box. Set the DataSource property to dsOrders1.Product, DisplayMember to ProductName, ValueMember to ProductID and DropDownStyle to DropDownList for the cboProduct combo box. Double-click the form and add the following code in the Load event handler:
private void OrderForm_Load(
object sender, System.EventArgs e)
{
sqlDataAdapter1.Fill(dsOrders1, "Customers");
sqlDataAdapter2.Fill(dsOrders1, "Products");
}
Double-click the Button control and add the following code to the Click event handler:
private void btnPlaceOrder_Click(
object sender, System.EventArgs e)
{
try
{
Ordering ordering = new Ordering();
ordering.PlaceOrder(
cboCustomers.SelectedValue.ToString(),
(int) cboProducts.SelectedValue);
MessageBox.Show("Order placed successfully");
}
catch(Exception ex)
{
MessageBox.Show(ex.Message);
}
}
Build the project. Set the project StepByStep7_18 as the startup project. Run the solution. Select a customer and a product from the Customers and Products combo boxes and click the Place Order button. You will see a message box confirming that the order is successfully placed. You can retrieve records from the Billing table and the Shipping table to find that one new record is added to each of them. This is the case when everything was as expected and the transaction was successfully committed. Now consider a case where the Billing component is working fine but there is a problem with the Shipping component. To simulate this, open the Component Service administrative tool, right-click on the Shipping application, and select Delete from the context menu. COM+ 1.5 (Windows XP and Windows Server 2003) supports a less destructive way: You can select Disable from the context menu. Now run the client program. You get an error message showing that the Shipping component is not available. However, if you retrieve the data from the Billing and Shipping tables, neither the Billing nor Shipping table contains the record for sales that presented you with an error. Because there was an error, the transaction was automatically rolled back.
|
The client application created in Step-by-Step 7.18 does not itself take part in the transaction, but various server components that the client application uses do take part in a transaction. Automatic transaction processing increases the reliability of applications without putting lot of pain in programming.
In the last step of Step-by-Step 7.18, I simulated a scenario that an enterprise application should always be ready to deal with. This is a scenario where one or more of an application's components are unavailable. When the Shipping component was unavailable, you were not able to record orders in the system. For many applications, availability is the number one priority. Availability becomes a major challenge when applications are distributed because the points of failure are now increased and some of them are not even in your control.
I discuss how another COM+ service—Queued Components—solve the problem of availability in the next section.
Queued Components
From the perspective of a client, a queued component is a serviced component that can be invoked and executed asynchronously. The queued components are based on the Microsoft Message Queuing (MSMQ) technology, which is a part of the Windows operating system.
How Queued Components Work
Communication between a client and a queued component involves four basic components between the client and server, as shown in Figure. These components are
Recorder—
The Recorder kicks in when a client makes a call to the queued component. The recorder records the call, packages it as a message, and stores the message in the message queue.
Queue—
The Queue is a repository of messages. Each COM+ application has a set of queues assigned to it. There is one primary queue, five retry queues, and one dead-letter queue. When the message arrives from the recorder, the message waits in the primary queue to be picked up by the queued component. If there is an error in processing, the message is sent to the first retry queue. If the message processing fails in the first retry, the message is moved to the second retry queue, and so on. The retry queues differ from each other in the frequency with which they retry a message. The first retry queue retries messages most frequently, whereas the fifth one is the slowest. If there is an error in processing the message in the fifth queue, the message is finally moved to a dead-letter queue where no further retries are made. Occasionally, you may want to check the dead letter queue and custom process any failed messages.
Listener—
The Listener's role is to poll the queue for incoming messages and, when there is one, pass the message to the player.
Player—
The Player unpacks a message and calls the invoke methods that were recorded by the client on the queued component.

Creating a Queued Component
Creating a queued component is just like creating any other serviced component. To configure a component to work as a queued component you need to apply the following two attributes:
ApplicationQueuing Attribute—
You apply this attribute at the assembly level to enable queuing support for an application. If a client will call the queued component, then the QueueListenerEnabled property must be set to true.
[assembly: ApplicationQueuing(
Enabled=true, QueueListenerEnabled=true)]
InterfaceQueueing Attribute—
You apply this attribute at the component level to specify the interface through which COM+ allows the calls on the component to be recorded and stored in the queue. For example:
[InterfaceQueuing(Enabled=true,
Interface="IOrdering")]
The execution lifetime of a queued component and the client application may be different. Therefore, when creating a queued component, you must follow these guidelines:
Methods should not return any value or reference parameters. All calls made by the client should be self-sufficient. The queued component has no way to generate a callback to the client program if more information is needed. Methods should not throw any application-specific exceptions because the client may not be available to respond to the exceptions.
Step-by-Step 7.19 shows how to use a queued component that is capable of listening to a message queue. When a message arrives, the component receives orders and calls other components to process them.
|
7.19 Using Queued Components: Creating an Ordering Component
Add a new Visual C# .NET Class library named StepByStep7_19 to the solution. In the Solution Explorer, right-click project StepByStep7_19 and select Add Reference from the context menu to add references to the System.EnterpriseServices component and projects StepByStep7_15 and StepByStep7_16. In the Solution Explorer, copy the Ordering.cs file from the StepByStep7_17 project to the current project. Open the file and change the namespace name to StepByStep7_19. Delete the default Class1.cs. Apply the InterfaceQueuing attribute on the Ordering class and change the GUID in the Guid attribute:
[InterfaceQueuing(
Enabled=true, Interface="IOrdering")]
[Transaction(TransactionOption.RequiresNew)]
[ClassInterface(ClassInterfaceType.None)]
[Guid("E02FF390-1E02-4a39-8C6D-DA8DDE2E9779")]
public class Ordering : ServicedComponent,
IOrdering
{
...
}
Open the AssemblyInfo.cs file in the project and add the following using directive:
using System.EnterpriseServices;
Add the following assembly-level attributes to the AssemblyInfo.cs file:
[assembly: ApplicationQueuing(
Enabled=true, QueueListenerEnabled=true)]
[assembly: ApplicationName(
"Ordering Application With Queued Components")]
[assembly: Description("Places an order")]
[assembly: ApplicationActivation(
ActivationOption.Server)]
Note that you have to add an ApplicationQueuing attribute to enable queuing for the application. Change the AssemblyVersion and AssemblyKeyFile attribute in the AssemblyInfo.cs file as shown here:
[assembly: AssemblyVersion("1.0.0")]
[assembly: AssemblyKeyFile(@"..\..\..\70320.snk")]
Build the project. A StepByStep7_19.dll is generated, and a strong name is assigned to the file based on the specified key file. Launch the Visual Studio .NET command prompt and change the directory to the folder where the DLL file generated in step 8 resides. Issue the following command to install the assembly to the GAC:
gacutil /i StepByStep7_19.dll
At the command prompt, issue the following command to install the service component assembly to the COM+ Catalog:
regsvcs StepByStep7_19.dll
|
After you have registered a serviced component in the COM+ Catalog, as you did in Step-by-Step 7.19, access the Properties window of the COM+ application via the Component Services administrative tool. In the Queuing tab, you will see that the options for queuing and listening are already configured as shown in Figure.

You can also configure an interface to support queuing through its Properties dialog box, as shown in Figure.

Creating a Client for a Queued Component
One of the ways you create an object in the client program is by using the new operator. However, you don't want to use that when working with a queue. With a queued component your objective is not to create an instance of an object but instead you need a way in which you can record a message for the client and store that in a queue so that the server object can read that message when possible.
Recording a message for a queued component is generally a three-step process:
Call the Marshal.BindToMoniker() method and pass it a moniker string that corresponds to the interface of the queued component. The moniker string is formed by preceding the full type name (qualified with namespace) with the string "queue:/new:". For example:
Marshal.BindToMoniker(@"queue:/new:StepByStep7_19.Ordering");
The Marshal.BindToMoniker() method returns a reference to the interface identified by the given moniker string. Use the interface reference obtained in step 1 to execute methods on the queued component. These methods are not executed immediately; instead, they are recorded and placed in the message queue. When you've finished calling methods, call the Marshal.ReleaseComObject() method to release the reference to the interface reference obtained in step 1.
Step-by-Step 7.20 shows how to create a client program that uses the Marshal.BindToMoniker() method to record messages for a queued component.
|
7.20 Using Queued Components: Creating a Client Order Form
Add a new Visual C# .NET Windows application named StepByStep7_20 to the solution. In the Solution Explorer, right-click project StepByStep7_20 and select Add Reference from the context menu to add references to the System.EnterpriseServices and StepByStep7_19 components. In the Solution Explorer, rename the default Form1.cs to OrderForm.cs. Open the form in code view and change all occurrences of Form1 to refer to OrderForm instead. Add the following using directives:
using System.Data.SqlClient;
using StepByStep7_19;
Place one GroupBox control, two Label controls, two ComboBox controls (cboCustomers and cboProducts), and one Button control (btnPlaceOrder) on the form. You can use the design of the form in Figure. Open Server Explorer and drag the Customers and Products tables from the Northwind data connection node to the form. A SqlConnection object and two SqlDataAdapter objects are created on the form. Right-click the first SqlDataAdapter object and select Generate DataSet from the context menu. In the Generate DataSet dialog box, choose the New radio button and name the new DataSet dsOrders. Select the Customers table and click OK. Right-click the second SqlDataAdapter object and select Generate DataSet from the context menu. In the Generate DataSet dialog box, choose the existing DataSet dsOrders. Select the Products table and click OK. Set the DataSource property to dsOrders1.Customers, DisplayMember to ContactName, ValueMember to CustomerID, and DropDownStyle to DropDownList for the cboCustomers combo box. Set the DataSource property to dsOrders1.Product, DisplayMember to ProductName, ValueMember to ProductID, and DropDownStyle to DropDownList for the cboProduct combo box. Double-click the form and add the following code in the Load event handler:
private void OrderForm_Load(
object sender, System.EventArgs e)
{
sqlDataAdapter1.Fill(dsOrders1, "Customers");
sqlDataAdapter2.Fill(dsOrders1, "Products");
}
Double-click the Button control and add the following code to the Click event handler:
private void btnPlaceOrder_Click(
object sender, System.EventArgs e)
{
IOrdering ord = null;
try
{
ord = (IOrdering) Marshal.BindToMoniker(
@"queue:/new:StepByStep7_19.Ordering");
ord.PlaceOrder(
cboCustomers.SelectedValue.ToString(),
(int) cboProducts.SelectedValue);
MessageBox.Show("Order placed in the queue");
}
catch(Exception ex)
{
MessageBox.Show(ex.Message);
}
finally
{
Marshal.ReleaseComObject(ord);
}
}
Build the project. Set the project StepByStep7_20 as the startup project. Run the solution. Select a customer and a product from the Customers and Products combo boxes and click the Place Order button. You will see that because both Billing and Shipping components are available, the message is immediately processed. Try disabling or uninstalling either the Billing or Shipping component and run the client application again. Note that the records for a sale are now not immediately created in the database tables. Instead, this information is now part of the message queue, waiting for the serviced component to be started. You can view the message queue for a computer via the Computer Management tool available in the Administrative Tools section of the Windows Control Panel.
|
In Step-by-Step 7.20 you learned how to record messages in a queue for a serviced component. You also experimented with making an application available without regard to whether one or more components failed.
A transaction is a series of operations performed as a single unit. A transaction is successful only when all the operations in that transaction succeed. The outcome of a transaction depends on the status of the abort bit that is maintained in the context of a transaction and the consistent and done bits, which are maintained in each context. You can use the methods of the ContextUtil class to change the status of the consistent and done bits programmatically. The AutoComplete attribute automatically calls the ContextUtil.SetComplete() method if a method completes successfully; otherwise, ContextUtil.SetAbort() is called to abort the transaction. A queued component is a serviced component that can be invoked and executed asynchronously. With the help of a queued component, you can make your application available even when some of its components are not.
|
|