By Mike Gunderloy
Sometimes it seems to take more code to support a component than to implement its functionality. For example, you might sell a client a server-side component with a license that limits the user to five simultaneous instances of the component. This business decision has development consequences: You now need to come up with a way to enforce that license count. If you’re working in the .NET world, there’s an easy answer. You can use the System.EnterpriseServices namespace to limit the number of simultaneous users without writing a lot of code.
COM+ to the rescue
System.EnterpriseServices is the .NET wrapper around COM+, a part of the Windows operating system that provides various infrastructure-level services to interested applications. These services include automatic transaction management, just-in-time activation, component queuing, and (central to this article) object pooling. .NET components that use COM+ are called serviced components. Here are the typical steps in creating a serviced component:
- Create a class that inherits from System.EnterpriseServices.ServicedComponent.
- Assign a strong name to the assembly containing the class.
- Install the assembly into the Global Assembly Cache (GAC).
- Use the Services Installation tool (Regsvcs.exe) to install the assembly to the COM+ catalog.
A serviced component example
To see how this works, follow along as I create a very simple serviced component. To begin, you’ll need to create a key file to use in assigning a strong name to the assembly. This key file is essential to the cryptographic signing that .NET uses to verify assembly integrity. You can create a key file from the Visual Studio .NET command prompt by running the sn utility:
sn –k trsc.snk
Now, launch Visual Studio .NET and create a new Visual Basic .NET Class Library project, naming it TRSC. Right-click the project and add a reference to the System.EnterpriseServices component. Rename the default Class1.vb to TimeServer.vb and fill in its code:
Enabled:=True, MaxPoolSize:=2)> _
Public Class TimeServer
' Return the current time
Public Function GetTime() As String
GetTime = DateTime.Now.ToLongTimeString()
' Enable object pooling
Protected Overrides Function CanBePooled() _
CanBePooled = True
Obviously, this is an extremely simple class, but the same principles will work with your thousands-of-lines-long business rule extravaganza. Note the scaffolding code to enable object pooling; I’ll come back to that later.
Next, you need to make some changes to the AssemblyInfo.vb file. In particular, you need to change and add some assembly attributes. If you haven’t looked at attributes before, you can think of them as a way to add metadata to your assemblies. The Common Language Runtime (CLR) uses these attributes to determine what to do with your assembly and how it relates to other pieces of software such as COM+. At the top of the AssemblyInfo.vb file, you need to make the System.EnterpriseServices namespace available:
And the assembly attributes go at the bottom:
<Assembly: ApplicationName( _
<Assembly: Description( _
"Delivers the current time on demand")>
The first of these assigns the version number for the library. The ApplicationName and ApplicationDescription will help you identify the library in the future. The AssemblyKeyFile attribute locates the file containing the key pair for strong naming.
At this point, you can build the assembly by selecting Build | Build Solution. Then switch to a Visual Studio .NET command prompt and register it, first in the GAC and then with COM+:
gacutil /i TRSC.dll
Managing the COM+ application
At this point, the serviced component is installed in the COM+ catalog and can be instantiated by client programs or can be administered through the Component Services administrative tool. To launch the tool, choose Start | Programs | Administrative Tools | Component Services. The tool runs in the familiar MMC interface, as shown in Figure A.
Right-click on the TRSC.TimeServer component and you can view its properties, as shown in Figure B. You can now see how the ObjectPooling attribute that you applied to the class is translated into COM+ properties.
Object pooling is the COM+ feature that solves the problem of limited concurrent usage for this library. When you set up an object pool, you’re telling COM+ how many copies of the object it can make available to client applications. You can specify the minimum number of copies of the object to keep in memory at all times (in this case, zero), the maximum number to make available (in this case, two, which I’m assuming as the license count for this demonstration), and the amount of time to wait for an object if the pool is exhausted (here, 10,000 milliseconds, or 10 seconds).
COM+ implements a Pooling Manager that handles the details of object pooling. When the COM+ application is started, the Pooling Manager creates the minimum number of objects and thereafter maintains them in the pool at all times when the application is running. Each time that the Pooling Manager receives a request to create an object, it checks to see 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 there’s not an object available, the Pooling Manager will create one, as long as the pool isn’t already at its maximum size. If no object is available and no new object can be created because of the size restriction of the pool, 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.
When the client is done with an object, it should invoke the object’s Dispose() method. The Pooling Manager intercepts this request and calls the CanBePooled() method on the object to check if 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, the object is destroyed forever.
Pooling in action
To see how this works in practice, you can create a new Visual Basic .NET Windows application (I named mine TRSCClient). To begin, add references to System.EnterpriseServices and to the TRSC.dll file created by compiling the class library. Place a button (btnLaunch) on the default Form1 and add a bit of code behind it:
Private Sub btnLaunch_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnLaunch.Click
' Create a new client form
Dim f As New frmClient()
Next, add a new form, frmClient, to the application. This form should contain a single TextBox control named txtTime. Here’s the code to go behind frmClient:
' Instance of the pooled class
Dim t As TRSC.TimeServer
Private Sub frmClient_Load( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
' Create the pooled object and
' execute its GetTime method
t = New TRSC.TimeServer()
txtTime.Text = t.GetTime()
Catch ex As Exception
Private Sub frmClient_Closing( _
ByVal sender As Object, _
ByVal e As System.ComponentModel.CancelEventArgs) _
' Give up the pooled object
If Not t Is Nothing Then
Now simply run the application and click the Launch button. This will create a new pooled object, open the client form, and retrieve the displayed time. Repeat this process and you’ll have two client forms open. Now try to launch a third client form. It won’t appear, because the object pool is exhausted. Instead, after the 10-second timeout elapses, you’ll get an error message: “COM+ activation failed because the activation could not be completed in the specified amount of time.” Close one of the first two forms and you’ll be able to launch another instance.
Managing the pool
The nice thing about this technique is that you don’t have to recompile the component to adjust the pool size. Instead, you can just go into the properties of the class, which you saw in Figure B. Of course, there’s nothing to prevent your customers from doing the same. You’ll be depending on the honor system to keep the license count set properly. But if you can’t trust your customers to do that, will they respect any other system, or will they try to crack it? The System.EnterpriseServices approach has the advantage of being easy to implement and built right into the operating system, which ultimately means less custom code to harbor bugs.