Search the Catalog
COM and .NET Component Services

COM and .NET Component Services

By Juval Löwy
September 2001
0-596-00103-7, Order Number: 1037
384 pages, $39.95

Chapter 10
.NET Serviced Components

.NET is the new platform from Microsoft used to build component-based applications, from standalone desktop applications to web-based applications and services. The platform will be available on forthcoming Microsoft operating systems and supported by the next release of Visual Studio, called Visual Studio.NET. In addition to providing a modern object-oriented framework for building distributed applications, .NET also provides several specialized application frameworks. These frameworks include Windows Forms for rich Windows clients, ADO.NET for data access, and ASP.NET for dynamic web applications. Another important framework is Web Services, which is used to expose and consume remote objects using the emerging SOAP and other XML-based protocols.

.NET is Microsoft's next-generation component technology. It is designed from the ground up to simplify component development and deployment, as well as to support interoperability between programming languages.

Despite its innovations and modern design, .NET is essentially a component technology. Like COM, .NET provides you with the means to rapidly build binary components, and Microsoft intends for .NET to eventually succeed COM. Like COM, .NET does not provide its own component services. Instead, .NET relies on COM+ to provide it with instance management, transactions, activity-based synchronization, granular role-based security, disconnected asynchronous queued components, and loosely coupled events. The .NET namespace that contains the types necessary to use COM+ services was named System.EnterpriseServices to reflect the pivotal role it plays in building .NET enterprise applications.

A .NET component that uses COM+ services is called a serviced component to distinguish it from the standard managed components in .NET. If you are not familiar with .NET, you should first read Appendix C or pick up a copy of .NET Framework Essentials by Thuan Thai and Hoang Lam (O'Reilly, 2001).

If you are already familiar with the basic .NET concepts, such as the runtime, assemblies, garbage collection, and C# (pronounced "C sharp"), continue reading. This chapter shows you how to create .NET serviced components that can take advantage of the COM+ component services that you have learned to apply throughout this book.

Developing Serviced Components

A .NET component that takes advantage of COM+ services needs to derive from the .NET base class ServicedComponent. ServicedComponent is defined in the System.EnterpriseServices namespace. Example 10-1 demonstrates how to write a .NET serviced component that implements the IMessage interface and displays a message box with "Hello" in it when the interface's ShowMessage( ) method is called.

Example 10-1: A simple .NET serviced component

namespace MyNamespace
 
{   
   using System.EnterpriseServices;   
   using System.Windows.Forms;//for the MessageBox class 
 
 
   public interface IMessage
   {    
      void ShowMessage(  );   
   }
   /// <summary>
   ///    Plain vanilla .NET serviced component
   /// </summary>
   public class MyComponent:ServicedComponent,IMessage
   {
      public MyComponent(  ) {}//constructor
      public void ShowMessage(  )
      {
        MessageBox.Show("Hello!","MyComponent");
      }
   }
}

WARNING:   A serviced component is not allowed to have parameterized constructors. If you require such parameters, you can either design around them by introducing a Create( ) method that accepts parameters, or use a constructor string.

There are two ways to configure a serviced component to use COM+ services. The first is COM-like: you derive from ServicedComponent, add the component to a COM+ application, and configure it there. The second way is to apply special attributes to the component, configuring it at the source-code level. When the component is added to a COM+ application, it is configured according to the values of those attributes. Attributes are discussed in greater detail throughout this chapter as you learn about configuring .NET components to take advantage of the various COM+ services.

.NET allows you to apply attributes to your serviced components with great flexibility. If you do not apply your own attributes, a serviced component is configured using default COM+ settings when it is added to a COM+ application. You can apply as many attributes as you like. A few COM+ services can only be configured via the Component Services Explorer. These services are mostly deployment-specific configurations, such as persistent subscriptions to COM+ Events and allocation of users to roles. In general, almost everything you can do with the Component Services Explorer can be done with attributes. I recommend that you put as many design-level attributes as possible (such as transaction support or synchronization) in the code and use the Component Services Explorer to configure deployment-specific details.

.NET Assemblies and COM+ Applications

When you wish to take advantage of COM+ component services, you must map the assembly containing your serviced components to a COM+ application. That COM+ application then contains your serviced components, just like any other component--COM+ does not care whether the component it provides services to is a managed .NET serviced component or a classic COM, unmanaged, configured component. A COM+ application can contain components from multiple assemblies, and an assembly can contribute components to more than one application, as shown in Figure 10-1. Compare Figure 10-1 to Figure 1-8. There is an additional level of indirection in .NET because an assembly can contain multiple modules.

Figure 10-1. COM+ applications and assemblies

 

However, setting up an assembly to contribute components to more than one COM+ application is not straightforward and is susceptible to future registrations of the assembly. As a rule, avoid mapping an assembly to more than one COM+ application.

Registering Assemblies

To add the serviced components in your assembly to a COM+ application, you need to register that assembly with COM+. You can perform that registration in three ways:

Regardless of the technique you use, the registration process adds your serviced components to a COM+ application and configures them according to the default COM+ settings or according to their attributes (if present in the code). If the assembly contains incompatible attributes, the incompatibility is detected during registration and the registration is aborted. Future versions of the .NET compilers may detect incompatibilities during compilation time.

Signing Assembly and Assembly Location

To add an assembly to a COM+ application, the assembly must be signed (have a strong name) so the assembly resolver can map a client activation request to the corresponding assembly. Although in theory you need not install the assembly in the global assembly cache (GAC), in practice you should install it because the assembly DLL must be in a known location--either the system directory (for server applications that run in DllHost) or the hosting client process directory (if the client is not a COM+ server application). The other known location that the assembly resolver uses is the GAC. To maintain flexibility (to change from server to library application) and consistency, make sure you always install your serviced component assembly in the GAC.

Specifying Application Name

You can provide .NET with an assembly attribute, specifying the name of the COM+ application you would like your components to be part of, by using the ApplicationName assembly attribute:

[assembly: ApplicationName("MyApp")]

If you do not provide an application name, .NET uses the assembly name. The ApplicationName attribute (and the rest of the serviced components attributes) is defined in the System.EnterpriseServices namespace. You must add this namespace to your project references and reference that namespace in your assembly information file:

using System.EnterpriseServices;

Understanding Serviced Component Versions

Before exploring the three registration options, you need to understand the relationship between an assembly's version and COM+ components.

Every managed client of your assembly is built against the particular version of the assembly that contains your components, whether they are serviced or regular managed components. .NET zealously enforces version compatibility between the client's assembly and any other assembly it uses. The assembly's version is the product of its version number (major and minor numbers, such as 3.11) and the build and revision numbers. The version number is provided by the developer as an assembly attribute, and the build or revision numbers can be generated by the compiler--or the developer can provide them himself.

The semantics of the version and build or revision numbers tell .NET whether two particular assembly versions are compatible with each other, and which of the two assemblies is the latest. Assemblies are compatible if the version number is the same. The default is that different build and revision numbers do not indicate incompatibility, but a difference in either major or minor number indicates incompatibility. A client's manifest contains the version of each assembly it uses. At runtime, .NET loads for the client the latest compatible assemblies to use, and latest is defined using the build and revision numbers.

All this is fine while everything is under tight control of the .NET runtime. But how would .NET guarantee compatibility between the assembly's version and the configuration of the serviced components in the COM+ Catalog? The answer is via the COM+ component's ID.

The first time a serviced component is added to a COM+ application, the registration process generates a CLSID for it, based on a hash of the class definition and its assembly's version and strong name. Subsequent registration of the same assembly with an incompatible version is considered a new registration for that serviced component, and the component is given a new CLSID.

This way, the serviced component's CLSID serves as its configuration settings version number. Existing managed clients do not interfere with one another because each gets to use the assembly version it was compiled with. Each managed client also uses a particular set of configuration parameters for the serviced components, captured with a different CLSID. When a managed client creates a serviced component, the .NET runtime creates for it a component from an assembly with a compatible version and applies the COM+ configuration of the matching CLSID.

Manual Registration

To register your component manually, use the RegSvcs.exe command-line utility. (In the future, Visual Studio.NET will probably allow you to invoke RegSvcs from the visual environment itself.) RegSvcs accepts as a parameter the name of the file containing your assembly's metadata. In a single DLL assembly, that file is simply the assembly file. If you do not specify as an assembly attribute the name of the COM+ application that should host your components, RegSvcs must be told that name explicitly as a command-line parameter, using the /appname: switch.

For example, if your single DLL assembly resides in MyAssembly.dll and you wish to add the serviced components in that assembly to the MyApp COM+ application, you would use RegSvcs in this manner:

RegSvcs.exe /appname:MyApp  MyAssembly.dll

The command-line application name is ignored if the assembly contains an application name.

In any case, you must create that COM+ application in the Component Services Explorer beforehand; otherwise, the previous command line will fail. You can instruct RegSvcs to create the application for you using the /c switch:

RegSvcs.exe  /c  MyApp  MyAssembly.dll  

Or if the name is specified in the assembly:

RegSvcs.exe  /c  MyAssembly.dll  

When using the /c switch, RegSvcs creates a COM+ application, names it accordingly, and adds the serviced components to it. If the Catalog already contains an application with that name, the registration fails.

You can also ask RegSvcs to try to find a COM+ application with that name and, if none is found, create one. This is done using the /fc switch:

RegSvcs.exe  /fc MyApp MyAssembly.dll  

Or if the name is specified in the assembly:

RegSvcs.exe  /fc  MyAssembly.dll  

If you don't specify a COM+ application name, either in the assembly or as a command-line parameter, RegSvcs uses the assembly name for the application name. If your assembly is called MyAssembly, RegSvcs adds the components to the MyAssembly COM+ application. This behavior is the same for all the command-line switches.

By default, RegSvcs does not override the existing COM+ application (and its components) settings. If that assembly version is already registered with that COM+ application, then RegSvcs does nothing. If that version is not registered yet, it adds the new version and assigns new CLSIDs. Reconfiguring an existing version is done explicitly using the /reconfig switch:

RegSvcs.exe  /reconfig  /fc  MyApp  MyAssembly.dll  

The /reconfig switch causes RegSvcs to reapply any application, component, interface, and method attributes found in the assembly to the existing version and use the COM+ default settings for the rest, thus reversing any changes you made using the Component Services Explorer.

When RegSvcs adds a serviced component to the COM+ Catalog, it must give it a class-ID (CLSID) and a prog-ID. RegSvcs creates a GUID for every component (based on the assembly's version and the class definition) and names it <Namespace>.<Component name>. For example, when you add the serviced component in Example 10-1 to the COM+ Catalog, RegSvcs names it MyNamespace.MyComponent. You can also specify the CLSID and the prog-ID of your serviced components using attributes.

In addition to adding the serviced components in the assembly to a COM+ application, RegSvcs creates a type library. This library contains interface and CoClass definitions to be used by nonmanaged clients (COM clients). The default type library filename is <Assembly name>.tlb--the name of the assembly with a .tlb extension.

Dynamic Registration

When a managed client creates a serviced component, the .NET runtime resolves which assembly version to use for that client. Next, the runtime verifies that the required version is registered with COM+. If it is not registered, the runtime installs it automatically. This process is called dynamic registration. As with RegSvcs, if the assembly contains an application name, then that name is used; if it does not, then the assembly's name is used for the COM+ application's name.

Note that only .NET clients can rely on having dynamic registration done when they instantiate a .NET serviced component. For COM clients, you must use the RegSvcs utility. Another limitation of dynamic registration is that serviced components in the assembly are configured according to the attributes in the assembly and the COM+ defaults. If you require configuring some services (such as events subscriptions) using the Component Services Explorer for your application to function properly, you must use RegSvcs to register your components and provide the additional configuration using the Component Services Explorer. Only then can clients use your serviced components. As a result, dynamic registration is only useful for serviced components that contain all the service configurations they need in their code through the use of attributes. Finally, dynamic registration requires that the user invoking the call that triggers dynamic registration be a member of the Windows 2000 Administrator group. It has this requirement because dynamic registration makes changes to the COM+ Catalog; if the user invoking it is not a member of the Windows 2000 Administrator group, dynamic registration will fail.

In general, you should use RegSvcs and the Component Services Explorer rather than relying on dynamic registration. If you want to rely on dynamic registration of your serviced components, you should increment the version number of your assembly every time you make a change to one of the components' attributes, to ensure that you trigger dynamic registration.

Programmatic Registration

Both RegSvcs and dynamic registration use a .NET class called RegistrationHelper to perform the registration. RegistrationHelper implements the IRegistrationHelper interface, whose methods are used to register and unregister assemblies. For example, the InstallAssembly( ) method registers the specified assembly in the specified COM+ application (or the application specified in the assembly). This method is defined as:

public void InstallAssembly(string assembly, 
                            ref string application,
                            ref string tlb,
                            InstallationFlags installFlags ); 

The installation flags correspond to the various RegSvcs switches. See the MSDN Library for additional information on RegistrationHelper. You can use RegistrationHelper yourself as part of your installation program; for more information, see the section "Programming the COM+ Catalog" later in this chapter.

The ApplicationID Attribute

Every COM+ application has a GUID identifying it called the application ID. You can provide an assembly attribute specifying the application ID in addition to the application name:

[assembly: ApplicationID("8BE192FA-57D0-49a0-8608-6829A314EEBE")]

Unlike the application name, the application ID is guaranteed to be unique, and you can use it alongside the application name. Once an application ID is specified, all searches for the application during registration are done using the application ID only, and the application name is only useful as a human-readable form of the application identity. Using application ID comes in handy when deploying the assembly in foreign markets--you can provide a command-line localized application name for every market while using the same application ID for your administration needs internally. The ApplicationID attribute is defined in the System.EnterpriseServices namespace.

The Guid Attribute

Instead of having the registration process generate a CLSID for your serviced component, you can specify one for it using the Guid attribute:

using System.Runtime.InteropServices;
 
[Guid("260C9CC7-3B15-4155-BF9A-12CB4174A36E")]
public class MyComponent :ServicedComponent,IMyInterface
{...} 

The Guid attribute is defined in the System.Runtime.InteropServices namespace.

When you specify a class ID, subsequent registrations of the assembly don't generate a new CLSID for the component, regardless of the version of the assembly being registered. Registrations always reconfigure the same component in the COM+ Catalog. Specifying a class ID is useful during development, when you have multiple cycles of code-test-fix. Without it, every invocation by the test client triggers a dynamic registration--you very quickly clutter the COM+ application with dozens of components, when you actually only use the latest one.

The ProgId Attribute

Instead of having the registration process generate a name for your serviced component (namespace plus component name), you can specify one for it using the ProgID attribute:

using System.Runtime.InteropServices;
 
[ProgId("My Serviced Component")]
public class MyComponent :ServicedComponent,IMyInterface
{...} 

The ProgId attribute is defined in the System.Runtime.InteropServices namespace.

Configuring Serviced Components

You can use various .NET attributes to configure your serviced components to take advantage of COM+ component services. The rest of this chapter demonstrates this service by service, according to the order in which the COM+ services are presented in this book.

Application Activation Type

To specify the COM+ application's activation type, you can use the ApplicationActivation assembly attributes. You can request that the application be a library or a server application:

[assembly: ApplicationActivation(ActivationOption.Server)]

or:

[assembly: ApplicationActivation(ActivationOption.Library)]

If you do not provide the ApplicationActivation attribute, then .NET uses a library activation type by default. Note that this use differs from the COM+ default of creating a new application as a server application.

TIP:   The next release of Windows 2000, Windows XP (see Appendix B), allows a COM+ application to be activated as a system service, so I expect that ApplicationActivation will be extended to include the value of ActivationOption.Service.

Before I describe other serviced components attributes, you need to understand what attributes are. Every .NET attribute is actually a class, and the attribute class has a constructor (maybe even a few overloaded constructors) and, usually, a few properties you can set. The syntax for declaring an attribute is different from that of any other class. In C#, you specify the attribute type between square brackets [...]. You specify constructor parameters and the values of the properties you wish to set between parentheses (...).

In the case of the ApplicationActivation attribute, there are no properties and the constructor must accept an enum parameter of type ActivationOption, defined as:

enum ActivationOption{Server,Library}

There is no default constructor for the ApplicationActivation attribute.

The ApplicationActivation attribute is defined in the System.EnterpriseServices namespace. Your must add this namespace to your project references and reference that namespace in your assembly information file:

using System.EnterpriseServices;

The rest of this chapter assumes that you have added these references and will not mention them again.

TIP:   A client assembly that creates a serviced component or uses any of its base class ServicedComponent methods must add a reference to System.EnterpriseServices to its project. Other clients, which only use the interfaces provided by your serviced components, need not add the reference.

The Description Attribute

The Description attribute allows you to add text to the description field on the General Properties tab of an application, component, interface, or method. Example 10-2 shows how to apply the Description attribute at the assembly, class, interface, and method levels. After registration, the assembly-level description string becomes the content of the hosting COM+ application's description field; the class description string becomes the content of the COM+ component description field. The interface and method descriptions are mapped to the corresponding interface and method in the Component Services Explorer.

Example 10-2: Applying the Description attribute at the assembly, class, interface, and method levels

[assembly: Description("My Serviced Components Application")]
 
[Description("IMyInterface description")]
public interface IMyInterface
{ 
   [Description("MyMethod description")]   
   void MyMethod(  );
}
 
[Description("My Serviced Component description")]
public class MyComponent :ServicedComponent,IMyInterface
{
   public void MyMethod(  ){}
} 

Accessing the COM+ Context

To access the COM+ context object's interfaces and properties, .NET provides you with the helper class ContextUtil. All context object interfaces (including the legacy MTS interfaces) are implemented as public static methods and public static properties of the ContextUtil class. Because the methods and properties are static, you do not have to instantiate a ContextUtil object--you should just call the methods. For example, if you want to trace the current COM+ context ID (its GUID) to the Output window, use the ContextId static property of ContextUtil:

using System.Diagnostics;//For the Trace class  
 
Guid contextID = ContextUtil.ContextId;
String traceMessage = "Context ID is " + contextID.ToString(  );
Trace.WriteLine(traceMessage);

ContextUtil has also properties used for JITA deactivation, transaction voting, obtaining the transactions and activity IDs, and obtaining the current transaction object. You will see examples for how to use these ContextUtil properties later in this chapter.

COM+ Context Attributes

You can decorate (apply attributes to) your class with two context-related attributes. The attribute MustRunInClientContext informs COM+ that the class must be activated in its creator's context:

[MustRunInClientContext(true)]
public class MyComponent :ServicedComponent
{...}

When you register the class above with COM+, the "Must be activated in caller's context" checkbox on the component's Activation tab is selected in the Component Services Explorer. If you do not use this attribute, the registration process uses the default COM+ setting when registering the component with COM+ --not enforcing same-context activation. As a result, using MustRunInClientContext with a false parameter passed to the constructor is the same as using the COM+ default:

[MustRunInClientContext(false)]

Using attributes with the COM+ default values (such as constructing the MustRunInClientContext attribute with false) is useful when you combine it with the /reconfig switch of RegSvcs. For example, you can undo any unknown changes made to your component configuration using the Component Services Explorer and restore the component configuration to a known state.

The MustRunInClientContext attribute class has an overloaded default constructor. If you use MustRunInClientContext with no parameters, the default constructor uses true for the attribute value. As a result, the following two statements are equivalent:

[MustRunInClientContext]
[MustRunInClientContext(true)]

The second COM+ context-related attribute is the EventTrackingEnabled attribute. It informs COM+ that the component supports events and statistics collection during its execution:

[EventTrackingEnabled(true)]
public class MyComponent2:ServicedComponent
{...}

The statistics are displayed in the Component Services Explorer. When you register this class with COM+, the "Component supports events and statistics" checkbox on the component's Activation tab is checked in the Component Services Explorer. If you do not use this attribute, the registration process does not use the default COM+ setting of supporting events when registering the component with COM+. The .NET designers made this decision consciously to minimize creation of new COM+ contexts for new .NET components; a component that supports statistics is usually placed in it own context.

The EventTrackingEnabled attribute class also has an overloaded default constructor. If you construct it with no parameters, the default constructor uses true for the attribute value. As a result, the following two statements are equivalent:

[EventTrackingEnabled]
[EventTrackingEnabled(true)]

COM+ Object Pooling

The ObjectPooling attribute is used to configure every aspect of your component's object pooling. The ObjectPooling attribute enables or disables object pooling and sets the minimum or maximum pool size and object creation timeout. For example, to enable object pooling of your component's objects with a minimum pool size of 3, a maximum pool size of 10, and a creation timeout of 20 milliseconds, you would write:

[ObjectPooling(MinPoolSize = 3,MaxPoolSize = 10,CreationTimeout = 20)]
public class MyComponent :ServicedComponent
{...}

The MinPoolSize, MaxPoolSize, and CreationTimeout properties are public properties of the ObjectPooling attribute class. If you do not specify values for these properties (all or just a subset) when your component is registered, the default COM+ values are used for these properties (a minimum pool size of 0, a maximum pool size of 1,048,576, and a creation timeout of 60 seconds).

The ObjectPooling attribute has a Boolean property called the Enabled property. If you do not specify a value for it (true or false), the attribute's constructor sets it to true. In fact, the attribute's constructor has a few overloaded versions--a default constructor that sets the Enabled property to true and a constructor that accepts a Boolean parameter. All constructors set the pool parameters to the default COM+ value. As a result, the following three statements are equivalent:

[ObjectPooling]
[ObjectPooling(true)]  
[ObjectPooling(Enabled = true)]

TIP:   If your pooled component is hosted in a library application, then each hosting Application Domain will have its own pool. As a result, you may have multiple pools in a single physical process, if that process hosts multiple Application Domains.

Under COM, the pooled object returns to the pool when the client releases its reference to it. Managed objects do not have reference counting--.NET uses garbage collection instead. A managed pooled object returns to the pool only when it is garbage collected. The problem with this behavior is that a substantial delay between the time the object is no longer needed by its client and the time the object returns to the pool can occur. This delay may have serious adverse effects on your application scalability and throughput. An object is pooled because it was expensive to create. If the object spends a substantial portion of its time waiting for the garbage collector, your application benefits little from object pooling.

There are two ways to address this problem. The first solution uses COM+ JITA (discussed next). When you use JITA, the pooled object returns to the pool after every method call from the client. The second solution requires client participation.

ServicedComponent has a public static method called DisposeObject( ), defined as:

public static void DisposeObject(ServicedComponent sc);

When the client calls DisposeObject( ), passing in an instance of a pooled serviced component, the object returns to the pool immediately. DisposeObject( ) has the effect of notifying COM+ that the object has been released. Besides returning the object to the pool, DisposeObject( ) disposes of the context object hosting the pooled object and of the proxy the client used.

For example, if the component definition is:

public interface IMyInterface
{    
   void MyMethod(  );
}
  
[ObjectPooling]
public class MyComponent : ServicedComponent,IMyInterface
{
   public void MyMethod(  ){}
} 

When the client is done using the object, to expedite returning the object to the pool, the client should call DisposeObject( ):

IMyInterface obj;
Obj = (IMyInterface) new MyComponent(  );
obj.MyMethod(  );
ServicedComponent sc = obj as ServicedComponent;
If(sc != null)
	ServicedComponent.DisposeObject(sc);

However, calling DisposeObject( ) directly is ugly. First, the client has to know that it is dealing with an object derived from ServicedComponent, which couples the client to the type used and renders many benefits of interface-based programming useless. Even worse, the client only has to call DisposeObject( ) if this object is pooled, which couples the client to the serviced component's configuration. What if you use object pooling in only one customer site, but not in others? This situation is a serious breach of encapsulation--the core principle of object-oriented programming.

The solution is to have ServicedComponent implement a special interface (defined in the System namespace) called IDisposable, defined as:

public interface IDisposable
{
    void Dispose(  );
}

ServicedComponent implementation of Dispose( ) returns the pooled object to the pool.

Having the Dispose( ) method on a separate interface allows the client to query for the presence of IDisposable and always call it, regardless of the object's actual type:

IMyInterface obj;
obj = (IMyInterface) new MyComponent(  );
obj.MyMethod(  );
 
//Client wants to expedite whatever needs expediting:
IDisposable disposable = obj as IDisposable;
if(disposable != null)     
   disposable.Dispose(  );

The IDisposable technique is useful not only with serviced components, but also in numerous other places in .NET. Whenever your component requires deterministic disposal of the resources and memory it holds, IDisposable provides a type-safe, component-oriented way of having the client dispose of the object without being too coupled to its type.

COM+ Just-in-Time Activation

.NET managed components can use COM+ JITA to efficiently handle rich clients (such as .NET Windows Forms clients), as discussed in Chapter 3.

To enable JITA support for your component, use the JustInTimeActivation attribute:

[JustInTimeActivation(true)]
public class MyComponent :ServicedComponent
{..}

When you register this component with COM+, the JITA checkbox in the Activation tab on the Component Services Explorer is selected. If you do not use the JustInTimeActivation attribute, JITA support is disabled when you register your component with COM+ (unlike the COM+ default of enabling JITA). The JustInTimeActivation class default constructor enables JITA support, so the following two statements are equivalent:

[JustInTimeActivation]
[JustInTimeActivation (true)]

Enabling JITA support is just one thing you need to do to use JITA. You still have to let COM+ know when to deactivate your object. You can deactivate the object by setting the done bit in the context object, using the DeactivateOnReturn property of the ContextUtil class. As discussed at length in Chapter 3, a JITA object should retrieve its state at the beginning of every method call and save it at the end. Example 10-3 shows a serviced component using JITA.

Example 10-3: A serviced component using JITA

public interface IMyInterface
{    
   void MyMethod(long objectIdentifier);
}
  
[JustInTimeActivation(true)]
public class MyComponent :ServicedComponent,IMyInterface
{ 
   public void MyMethod(long objectIdentifier)   
   {
      GetState(objectIdentifier);     
      DoWork(  );     
      SaveState(objectIdentifier);
	  //inform COM+ to deactivate the object upon method return
      ContextUtil.DeactivateOnReturn = true;
   }
   //other methods
   protected void GetState(long objectIdentifier){...}
   protected void DoWork(  ){...}
   protected void SaveState(long objectIdentifier){...}
} 

You can also use the Component Services Explorer to configure the method to use auto-deactivation. In that case, the object is deactivated automatically upon method return, unless you set the value of the DeactivateOnReturn property to false.

Using IObjectControl

If your serviced component uses object pooling or JITA (or both), it may also need to know when it is placed in a COM+ context to do context-specific initialization and cleanup. Like a COM+ configured component, the serviced component can use IObjectControl for that purpose. The .NET base class ServicedComponent already implements IObjectControl, and its implementation is virtual--so you can override the implementation in your serviced component, as shown in Example 10-4.

Example 10-4: A serviced component overriding the ServicedComponent implementation of IObjectControl

public class MyComponent :ServicedComponent
{ 
   public override void Activate(  ) 
   {     
      //Do context specific initialization here
   }   
   public override void Deactivate(  ) 
   {     
      //Do context specific cleanup here
   }   
   public override bool CanBePooled(  )   
   {
	    return true;   
   }
   //other methods
}

If you encounter an error during Activate( ) and throw an exception, then the object's activation fails and the client is given an opportunity to catch the exception.

IObjectControl, JITA, and Deterministic Finalization

To maintain JITA semantics, when the object deactivates itself, .NET calls DisposeObject( ) on it explicitly, thus destroying it. Your object can do specific cleanup in the Finalize( ) method (the destructor in C#), and Finalize( ) will be called as soon as the object deactivates itself, without waiting for garbage collection. If the object is a pooled object (as well as a JITA object), then it is returned to the pool after deactivation, without waiting for the garbage collection.

You can also override the ServicedComponent implementation of IObjectControl.Deactivate( ) and perform your cleanup there.

In any case, you end up with a deterministic way to dispose of critical resources without explicit client participations. This situation makes sharing your object among clients much easier because now the clients do not have to coordinate who is responsible for calling Dispose( ).

TIP:  COM+ JITA gives managed components deterministic finalization, a service that nothing else in .NET can provide out of the box.

COM+ Constructor String

Any COM+ configured component that implements the IObjectConstruct interface has access during construction to a construction string (discussed in Chapter 3), configured in the Component Services Explorer. Serviced components are no different. The base class, ServicedComponent, already implements the IObjectConstruct interface as a virtual method (it has only one method). Your derived serviced component can override the Construct( ) method, as shown in this code sample:

public class MyComponent :ServicedComponent
{ 
   public override void Construct(string constructString) 
   {     
      //use the string. For example:
      MessageBox.Show(constructString);      
   }
} 

If the checkbox "Enable object construction" on the component Activation tab is selected, then the Construct( ) method is called after the component's constructor, providing it with the configured construction string.

You can also enable construction string support and provide a default construction string using the ConstructionEnabled attribute:

[ConstructionEnabled(Enabled = true,Default = "My String")]
public class MyComponent :ServicedComponent
{ 
   public override void Construct(string constructString) 
   {...}
} 

The ConstructionEnabled attribute has two public properties. Enabled enables construction string support for your serviced component in the Component Services Explorer (once the component is registered) and Default provides an initial string value. When your component is registered with COM+, the registration process assigns the default string to the constructor string field on the component Activation tab. The default string has no further use after registration. New instances of your component receive as a constructor string the current value of the constructor string field. For example, if the default string is String A, when the serviced component is registered, the value of the constructor string field is set to String A. If you set it to a different value, such as String B, new instances of the component get String B as their construction string. They receive the current value, not the default value.

The ConstructionEnabled attribute has two overloaded constructors. One constructor accepts a Boolean value for the Enabled property; the default constructor sets the value of the Enabled property to true. You can also set the value of the Enabled property explicitly. As a result, the following three statements are equivalent:

[ConstructionEnabled] 
[ConstructionEnabled(true)]
[ConstructionEnabled(Enabled = true)]

COM+ Transactions

You can configure your serviced component to use the five available COM+ transaction support options by using the Transaction attribute. The Transaction attribute's constructor accepts an enum parameter of type TransactionOption, defined as:

public enum TransactionOption
{
   Disabled, 
   NotSupported, 
   Supported, 
   Required, 
   RequiresNew
}

For example, to configure your serviced component to require a transaction, use the TransactionOption.Required value:

[Transaction(TransactionOption.Required)]
public class MyComponent :ServicedComponent 
{...} 

The five enum values of TransactionOption map to the five COM+ transaction support options discussed in Chapter 4.

When you use the Transaction attribute to mark your serviced component to use transactions, you implicitly set it to use JITA and require activity-based synchronization as well.

The Transaction attribute has an overloaded default constructor, which sets the transaction support to TransactionOption.Required. As a result, the following two statements are equivalent:

[Transaction]
[Transaction(TransactionOption.Required)]

Voting on the Transaction

Not surprisingly, you use the ContextUtil class to vote on the transaction's outcome. ContextUtil has a static property of the enum type TransactionVote called MyTransactionVote. TransactionVote is defined as:

public enum TransactionVote {Abort,Commit}

Example 10-5 shows a transactional serviced component voting on its transaction outcome using ContextUtil. Note that the component still has to do all the right things that a well-designed transactional component has to do (see Chapter 4); it needs to retrieve its state from a resource manager at the beginning of the call and save it at the end. It must also deactivate itself at the end of the method to purge its state and make the vote take effect.

Example 10-5: A transactional serviced component voting on its transaction outcome using the ContextUtil MyTransactionVote property

public interface IMyInterface
{ 
   void MyMethod(long objectIdentifier);
} 
 
[Transaction]
public class MyComponent :ServicedComponent,IMyInterface
{
   public void MyMethod(long objectIdentifier)   
   {     
      try
      {
          GetState(objectIdentifier);
          DoWork(  );
          SaveState(objectIdentifier);
          ContextUtil.MyTransactionVote = TransactionVote.Commit;
       }
       catch
       {
          ContextUtil.MyTransactionVote = TransactionVote.Abort;
       }
       //Let COM+ deactivate the object once the method returns  
       finally
       {
          ContextUtil.DeactivateOnReturn = true;
       }
   }
   //helper methods 
   protected void GetState(long objectIdentifier){...}
   protected void DoWork(  ){...}
   protected void SaveState(long objectIdentifier){...}
} 

Compare Example 10-5 to Example 4-3. A COM+ configured component uses the returned HRESULT from the DoWork( ) helper method to decide on the transaction's outcome. A serviced component, like any other managed component, does not use HRESULT return codes for error handling; it uses exceptions instead. In Example 10-5 the component catches any exception that was thrown in the try block by the DoWork( ) method and votes to abort in the catch block.

Alternatively, if you do not want to write exception-handling code, you can use the programming model shown in Example 10-6. Set the context object's consistency bit to false (vote to abort) as the first thing the method does. Then set it back to true as the last thing the method does (vote to commit). Any exception thrown in between causes the method exception to end without voting to commit.

Example 10-6: Voting on the transaction without exception handling

public interface IMyInterface
{ 
   void MyMethod(long objectIdentifier);
} 
 
[Transaction]
public class MyComponent :ServicedComponent,IMyInterface
{
   public void MyMethod(long objectIdentifier)   
   {
       //Let COM+ deactivate the object once the method returns and abort the 
       //transaction. You can use ContextUtil.SetAbort(  ) as well
       ContextUtil.DeactivateOnReturn = true;
       ContextUtil.MyTransactionVote = TransactionVote.Abort;
 
       GetState(objectIdentifier);
       DoWork(  );
       SaveState(objectIdentifier);
            
       ContextUtil.MyTransactionVote = TransactionVote.Commit;
   }
   //helper methods 
   protected void GetState(long objectIdentifier){...}
   protected void DoWork(  ){...}
   protected void SaveState(long objectIdentifier){...}
}

Example 10-6 has another advantage over Example 10-5: having the exception propagated up the call chain once the transaction is aborted. By propagating it, callers up the chain know that they can also abort their work and avoid wasting more time on a doomed transaction.

The AutoComplete Attribute

Your serviced components can take advantage of COM+ method auto-deactivation using the AutoComplete method attribute. During the registration process, the method is configured to use COM+ auto-deactivation when AutoComplete is used on a method, and the checkbox "Automatically deactivate this object when the method returns" on the method's General tab is selected. Serviced components that use the AutoComplete attribute do not need to vote explicitly on their transaction outcome. Example 10-7 shows a transactional serviced component using the AutoComplete method attribute.

Example 10-7: Using the AutoComplete method attribute

public interface IMyInterface
{ 
   void MyMethod(long objectIdentifier);
}
  
[Transaction]
public class MyComponent : ServicedComponent,IMyInterface
{
   [AutoComplete(true)]
   public void MyMethod(long objectIdentifier)   
   {
       GetState(objectIdentifier);
       DoWork(  );
       SaveState(objectIdentifier);    
   }
   //helper methods 
   protected void GetState(long objectIdentifier){...}
   protected void DoWork(  ){...}
   protected void SaveState(long objectIdentifier){...}
} 

When you configure the method to use auto-deactivation, the object's interceptor sets the done and consistency bits of the context object to true if the method did not throw an exception and the consistency bit to false if it did. As a result, the transaction is committed if no exception is thrown and aborted otherwise.

Nontransactional JITA objects can also use the AutoComplete attribute to deactivate themselves automatically on method return.

The AutoComplete attribute has an overloaded default constructor that uses true for the attribute construction. Consequently, the following two statements are equivalent:

[AutoComplete]
[AutoComplete(true)]

The AutoComplete attribute can be applied on a method as part of an interface definition:

public interface IMyInterface
{ 
   //Avoid this:
   [AutoComplete] 
   void MyMethod(long objectIdentifier);
}

However, you should avoid using the attribute this way. An interface and its methods declarations serve as a contract between a client and an object; using auto completion of methods is purely an implementation decision. For example, one implementation of the interface on one component may chose to use autocomplete and another implementation on another component may choose not to.

The TransactionContext Object

A nontransactional managed client creating a few transactional objects faces a problem discussed in Chapter 4 (see the section "Nontransactional Clients"). Essentially, if the client wants to scope all its interactions with the objects it creates under one transaction, it must use a middleman to create the objects for it. Otherwise, each object created will be in its own separate transaction. COM+ provides a ready-made middleman called TransactionContext. Managed clients can use TransactionContext as well. To use the TransactionContext object, add to the project references the COM+ services type library. The TransactionContext class is in the COMSVCSLib namespace.

The TransactionContext class is especially useful in situations in which the class is a managed .NET component that derives from a class other than ServicedComponent. Remember that a .NET component can only derive from one concrete class and since the class already derives from a concrete class other than ServicedComponent, it cannot use the Transaction attribute. Nevertheless, the TransactionContext class gives this client an ability to initiate and manage a transaction.

Example 10-8 demonstrates usage of the TransactionContext class, using the same use-case as Example 4-6.

Example 10-8: A nontransactional managed client using the TransactionContext helper class to create other transactional objects

using COMSVCSLib;
 
IMyInterface obj1,obj2,obj3;
ITransactionContext transContext;
 
transContext = (ITransactionContext) new TransactionContext(  );
 
obj1 = (IMyInterface)transContext.CreateInstance("MyNamespace.MyComponent");
obj2 = (IMyInterface)transContext.CreateInstance("MyNamespace.MyComponent");
obj3 = (IMyInterface)transContext.CreateInstance("MyNamespace.MyComponent");
 
try
{   
   obj1.MyMethod(  );    
   obj2.MyMethod(  );    
   obj3.MyMethod(  );    
   transContext.Commit(  );   
}
catch//Any error - abort the transaction 
{   
   transContext.Abort(  );   
}

Note that the client in Example 10-8 decides whether to abort or commit the transaction depending on whether an exception is thrown by the internal objects.

COM+ Transactions and Nonserviced Components

Though this chapter focuses on serviced components, it is worth noting that COM+ transactions are used by other parts of the .NET framework besides serviced components--in particular, ASP.NET and Web Services.

Web services and transactions

Web services are the most exciting piece of technology in the entire .NET framework. Web services allow a middle-tier component in one web site to invoke methods on another middle-tier component at another web site, with the same ease as if that component were in its own assembly. The underlying technology facilitating web services serializes the calls into text format and transports the call from the client to the web service provider using HTTP. Because web service calls are text based, they can be made across firewalls. Web services typically use a protocol called Simple Object Access Protocol (SOAP) to represent the call, although other text-based protocols such as HTTP-POST and HTTP-GET can also be used. .NET successfully hides the required details from the client and the server developer; a web service developer only needs to use the WebMethod attribute on the public methods exposed as web services. Example 10-9 shows the MyWebService web service that provides the MyMessage web service--it returns the string "Hello" to the caller.

Example 10-9: A trivial web service that returns the string "Hello"

using System.Web.Services;
 
public class MyWebService : WebService
{
    public MyWebService(  ){}
    [WebMethod]
    public string MyMessage(  )
    {
       return "Hello";
    }
}

The web service class can optionally derive from the WebService base class, defined in the System.Web.Services namespace (see Example 10-9). The WebService base class provides you with easy access to common ASP.NET objects, such as those representing application and session states. Your web service probably accesses resource managers and transactional components. The problem with adding transaction support to a web service that derived from WebService is that it is not derived from ServicedComponent, and .NET does not allow multiple inheritance of implementation.

To overcome this hurdle, the WebMethod attribute has a public property called TransactionOption, of the enum type Enterprise.Services.TransactionOption discussed previously.

The default constructor of the WebMethod attribute sets this property to TransactionOption.Disabled, so the following two statements are equivalent:

[WebMethod]
[WebMethod(TransactionOption = TransactionOption.Disabled)]

If your web service requires a transaction, it can only be the root of a transaction, due to the stateless nature of the HTTP protocol. Even if you configure your web method to only require a transaction and it is called from within the context of an existing transaction, a new transaction is created for it. Similarly, the value of TransactionOption.Supported does not cause a web service to join an existing transaction (if called from within one).

Consequently, the following statements are equivalent--all four amount to no transaction support for the web service:

[WebMethod]
[WebMethod(TransactionOption = TransactionOption.Disabled)]
[WebMethod(TransactionOption = TransactionOption.NotSupported)]
[WebMethod(TransactionOption = TransactionOption.Supported)]

Moreover, the following statements are also equivalent--creating a new transaction for the web service:

[WebMethod(TransactionOption = TransactionOption.Required)]
[WebMethod(TransactionOption = TransactionOption.RequiresNew)]

The various values of TransactionOption are confusing. To avoid making them the source of errors and misunderstandings, use TransactionOption.RequiresNew when you want transaction support for your web method; use TransactionOption.Disabled when you want to explicitly demonstrate to a reader of your code that the web service does not take part in a transaction. The question is, why did Microsoft provide four overlapping transaction modes for web services? I believe that it is not the result of carelessness, but rather a conscious design decision. Microsoft is probably laying down the foundation in .NET for a point in the future when it will be possible to propagate transactions across web sites.

Finally, you do not need to explicitly vote on a transaction from within a web service. If an exception occurs within a web service method, the transaction is automatically aborted. Conversely, if no exceptions occur, the transaction is committed automatically (as if you used the AutoComplete attribute). Of course, the web service can still use ContextUtil to vote explicitly to abort instead of throwing an exception, or when no exception occurred and you still want to abort.

ASP.NET and transactions

An ASP.NET web form may access resource managers (such as databases) directly, and it should do so under the protection of a transaction. The page may also want to create a few transactional components and compose their work into a single transaction. The problem again is that a web form derives from the System.Web.UI.Page base class, not from ServicedComponent, and therefore cannot use the [Transaction] attribute.

To provide transaction support for a web form, the Page base class has a write-only property called TransactionMode of type TransactionOption. You can assign a value of type TransactionOption to TransactionMode, to configure transaction support for your web form. You can assign TransactionMode programmatically in your form contractor, or declaratively by setting that property in the visual designer. The designer uses the Transaction page directive to insert a directive in the aspx form file. For example, if you set the property using the designer to RequiresNew, the designer added this line to the beginning of the aspx file:

<@% Page Transaction="RequiresNew" %>

Be aware that programmatic setting will override any designer setting. The default is no transaction support (disabled).

The form can even vote on the outcome of the transaction (based on its interaction with the components it created) by using the ContextUtil methods. Finally, the form can subscribe to events notifying it when a transaction is initiated and when a transaction is aborted.

COM+ Synchronization

Multithreaded managed components can use .NET-provided synchronization locks. These are classic locks, such as mutexes and events. However, these solutions all suffer from the deficiencies described at the beginning of Chapter 5. .NET serviced components should use COM+ activity-based synchronization by adding the Synchronization attribute to the class definition. The Synchronization attribute's constructor accepts an enum parameter of type SynchronizationOption, defined as:

public enum SynchronizationOption
{
    Disabled,
    NotSupported,
    Supported,
    Required,
    RequiresNew
}

For example, use the SynchronizationOption.Required value to configure your serviced component to require activity-based synchronization:

[Synchronization(SynchronizationOption.Required)]
public class MyComponent :ServicedComponent 
{...} 

The five enum values of SynchronizationOption map to the five COM+ synchronization support options discussed in Chapter 5.

The Synchronization attribute has an overloaded default constructor, which sets synchronization support to SynchronizationOption.Required. As a result, the following two statements are equivalent:

[Synchronization]
[Synchronization(SynchronizationOption.Required)]

TIP:   The System.Runtime.Remoting.Context namespace contains a context attribute called Synchronization that can be applied to context-bound .NET classes. This attribute accepts synchronization flags similar to SynchronizationOption, and initially looks like another version of the Synchronization class attribute. However, the Synchronization attribute in the Context namespace provides synchronization based on physical threads, unlike the Synchronization attribute in the EnterpriseServices namespace, which uses causalities. As explained in Chapter 5, causality and activities are a more elegant and fine-tuned synchronization strategy.

Programming the COM+ Catalog

You can access the COM+ Catalog from within any .NET managed component (not only serviced components). To write installation or configuration code (or manage COM+ events), you need to add to your project a reference to the COM+ Admin type library. After you add the reference, the Catalog interfaces and objects are part of the COMAdmin namespace. Example 10-10 shows how to create a catalog object and use it to iterate over the application collection, tracing to the Output window the names of all COM+ applications on your computer.

Example 10-10: Accessing the COM+ Catalog and tracing the COM+ application names

using COMAdmin;
 
ICOMAdminCatalog catalog;
ICatalogCollection applicationCollection;
ICatalogObject application;
 
int applicationCount;
int i;//Application index 
 
catalog = (ICOMAdminCatalog)new COMAdminCatalog(  );
applicationCollection = (ICatalogCollection)catalog.GetCollection("Applications");
         
//Read the information from the catalog
applicationCollection.Populate(  ); 
applicationCount = applicationCollection.Count;
  
for(i = 0;i< applicationCount;i++)
{   
   //Get the current application   
   application= (ICatalogObject)applicationCollection.get_Item(i);   
   int index = i+1;
   String traceMessage = index.ToString()+". "+application.Name.ToString(  );
 
   Trace.WriteLine(traceMessage);
}

TIP:  The System.EnterpriseServices.Admin namespace contains the COM+ Catalog object and interface definitions. However, in the Visual Studio.NET Beta 2, the interfaces are defined as private to that assembly. As a result, you cannot access them. The obvious workaround is to import the COM+ Admin type library yourself, as demonstrated in Example 10-10. In the future, you will probably be able to use System.EnterpriseServices.Admin namespace directly. The resulting code, when programming directly using the System.EnterpriseServices.Admin namespace, is almost identical to Example 10-10.

COM+ Security

.NET has an elaborate component-oriented security model. .NET security model manages what the component is allowed to do and what permissions are given to the component and all its clients up the call chain. You can (and should) still manage the security attributes of your hosting COM+ application to authenticate incoming calls, authorize callers, and control impersonation level.

.NET also has what .NET calls role-based security, but that service is limited compared with COM+ role-based security. A role in .NET is actually a Windows NT user group. As a result, .NET role-based security is only as granular as the user groups in the hosting domain. Usually, you do not have control over your end customer's IT department. If you deploy your application in an environment where the user groups are coarse, or where they do not map well to actual roles users play in your application, then .NET role-based security is of little use to you. COM+ roles are unrelated to the user groups, allowing you to assign roles directly from the application business domain.

Configuring Application-Level Security Settings

The assembly attribute ApplicationAccessControl is used to configure all the settings on the hosting COM+ application's Security tab.

You can use ApplicationAccessControl to turn application-level authentication on or off:

[assembly: ApplicationAccessControl(true)]

The ApplicationAccessControl attribute has a default constructor, which sets authorization to true if you do not provide a construction value. Consequently, the following two statements are equivalent:

[assembly: ApplicationAccessControl] 
[assembly: ApplicationAccessControl(true)]

If you do not use the ApplicationAccessControl attribute at all, then when you register your assembly, the COM+ default takes effect and application-level authorization is turned off.

The ApplicationAccessControl attribute has three public properties you can use to set the access checks, authentication, and impersonation level. The AccessChecksLevel property accepts an enum parameter of type AccessChecksLevelOption, defined as:

public enum AccessChecksLevelOption
{
    Application,
    ApplicationComponent
}

AccessChecksLevel is used to set the application-level access checks to the process only (AccessChecksLevelOption.Application) or process and component level (AccessChecksLevelOption.ApplicationComponent). If you do not specify an access level, then the ApplicationAccessControl attribute's constructors set the access level to AccessChecksLevelOption.ApplicationComponent, the same as the COM+ default.

The Authentication property accepts an enum parameter of type AuthenticationOption, defined as:

public enum AuthenticationOption
{
   None,
   Connect,
   Call,
   Packet,
   Integrity,
   Privacy,
   Default
}

The values of AuthenticationOption map to the six authentication options discussed in Chapter 7. If you do not specify an authentication level or if you use the Default value, the ApplicationAccessControl attribute's constructors set the authentication level to AuthenticationOption.Packet, the same as the COM+ default.

The Impersonation property accepts an enum parameter of type ImpersonationLevelOption, defined as:

public enum ImpersonationLevelOption
{
   Anonymous,
   Identify,
   Impersonate,
   Delegate,
   Default
}

The values of ImpersonationLevelOption map to the four impersonation options discussed in Chapter 7. If you do not specify an impersonation level or if you use the Default value, then the ApplicationAccessControl attribute's constructors set the impersonation level to ImpersonationLevelOption.Impersonate, the same as the COM+ default.

Example 10-11 demonstrates using the ApplicationAccessControl attribute with a server application. The example enables application-level authentication and sets the security level to perform access checks at the process and component level. It sets authentication to authenticate incoming calls at the packet level and sets the impersonation level to Identify.

Example 10-11: Configuring a server application security

[assembly: ApplicationActivation(ActivationOption.Server)]
 
[assembly: ApplicationAccessControl(
           true,//Authentication is on
           AccessChecksLevel=AccessChecksLevelOption.ApplicationComponent,
           Authentication=AuthenticationOption.Packet,
           ImpersonationLevel=ImpersonationLevelOption.Identify)]

A library COM+ application has no use for impersonation level, and it can only choose whether it wants to take part in its hosting process authentication level (that is, it cannot dictate the authentication level). To turn authentication off for a library application, set the authentication property to AuthenticationOption.None. To turn it on, use any other value, such as AuthenticationOption.Packet. Example 10-12 demonstrates how to use the ApplicationAccessControl to configure the security setting of a library application.

Example 10-12: Configuring a library application security

[assembly: ApplicationActivation(ActivationOption.Library)]
 
[assembly: ApplicationAccessControl(
           true,//Authentication 
           AccessChecksLevel=AccessChecksLevelOption.ApplicationComponent,
           //use AuthenticationOption.None to turn off authentication, 
           //and any other value to turn it on
           Authentication=AuthenticationOption.Packet)]

Component-Level Access Checks

The component attribute ComponentAccessControl is used to enable or disable access checks at the component level. Recall from Chapter 7 that this is your component's role-based security master switch. The ComponentAccessControl attribute's constructor accepts a Boolean parameter, used to turn access control on or off. For example, you can configure your serviced component to require component-level access checks:

[ComponentAccessControl(true)]
public class MyComponent :ServicedComponent 
{...}

The ComponentAccessControl attribute has an overloaded default constructor that uses true for the attribute construction. Consequently, the following two statements are equivalent:

[ComponentAccessControl]
[ComponentAccessControl(true)]

Adding Roles to an Application

You can use the Component Services Explorer to add roles to the COM+ application hosting your serviced components. You can also use the SecurityRole attribute to add the roles at the assembly level. When you register the assembly with COM+, the roles in the assembly are added to the roles defined for the hosting COM+ application. For example, to add the Manager and Teller roles to a bank application, simply add the two roles as assembly attributes:

[assembly: SecurityRole("Manager")]
[assembly: SecurityRole("Teller")] 

The SecurityRole attribute has two public properties you can set. The first is Description. Any text assigned to the Description property will show up in the Component Services Explorer in the Description field on the role's General tab:

[assembly: SecurityRole("Manager",Description = "Can access all components")]
[assembly: SecurityRole("Teller",Description = "Can access IAccountsManager only")] 

The second property is the SetEveryoneAccess Boolean property. If you set SetEveryoneAccess to true, then when the component is registered, the registration process adds the user Everyone as a user for that role, thus allowing everyone access to whatever the role is assigned to. If you set it to false, then no user is added during registration and you have to explicitly add users during deployment using the Component Services Explorer. The SecurityRole attribute sets the value of SetEveryoneAccess by default to true. As a result, the following statements are equivalent:

[assembly: SecurityRole("Manager")]
[assembly: SecurityRole("Manager",true)] 
[assembly: SecurityRole("Manager",SetEveryoneAccess = true)]

Automatically granting everyone access is a nice debugging feature; it eliminates security problems, letting you focus on analyzing your domain-related bug. However, you must suppress granting everyone access in a release build, by setting the SetEveryoneAccess property to false:

#if DEBUG
[assembly: SecurityRole("Manager")]
#else
[assembly: SecurityRole("Manager",SetEveryoneAccess = false)]
#endif

Assigning Roles to Component, Interface, and Method

The SecurityRole attribute is also used to grant access for a role to a component, interface, or method. Example 10-13 shows how to grant access to Role1 at the component level, to Role2 at the interface level, and to Role3 at the method level.

Example 10-13: Assigning roles at the component, interface, and method levels

[assembly: SecurityRole("Role1")]
[assembly: SecurityRole("Role2")]
[assembly: SecurityRole("Role3")]
 
[SecurityRole("Role2")]
public 
interface IMyInterface
{  
   [SecurityRole("Role3")]  
   void MyMethod(  );
}
  
[SecurityRole("Role1")]
public class MyComponent :ServicedComponent,IMyInterface
{...}

Figure 10-2 shows the resulting role assignment in the Component Services Explorer at the method level. Note that Role1 and Role2 are inherited from the component and interface levels.

Figure 10-2. The resulting role assignment of Example 10-13 in the Component Services Explorer, as seen at the method level

 

If you only assign a role (at the component, interface, or method level) but do not define it at the assembly level, then that role is added to the application automatically during registration. However, you should define roles at the assembly level to provide one centralized place for roles description and configuration.

Verifying Caller's Role Membership

Sometimes it is useful to verify programmatically the caller's role membership before granting it access. Your serviced components can do that just as easily as configured COM components. .NET provides you the helper class SecurityCallContext that gives you access to the security parameters of the current call. SecurityCallContext encapsulates the COM+ call-object's implementation of ISecurityCallContext, discussed in Chapter 7. The class SecurityCallContext has a public static property called CurrentCall. CurrentCall is a read-only property of type SecurityCallContext (it returns an instance of the same type). You use the SecurityCallContext object returned from CurrentCall to access the current call. Example 10-14 demonstrates the use of the security call context to verify a caller's role membership, using the same use-case as Example 7-1.

Example 10-14: Verifying the caller's role membership using the SecurityCallContext class

public class Bank :ServicedComponent,IAccountsManager 
{   
   void TransferMoney(int sum,ulong accountSrc,ulong accountDest)   
   {      
      bool callerInRole = false;      
      callerInRole = SecurityCallContext.CurrentCall.IsCallerInRole("Customer");   
	  if(callerInRole)//The caller is a customer      
      {        
         if(sum > 5000)            
           throw(new UnauthorizedAccessException(@"Caller does not have sufficient 
                                              credentials to transfer this sum"));      
      }      
      DoTransfer(sum,accountSrc,accountDest);//Helper method
   }
   //Other methods
}

You should use the Boolean property IsSecurityEnabled of SecurityCallContext to verify that security is enabled before accessing the IsCallerInRole( ) method:

bool securityEnabled = SecurityCallContext.CurrentCall.IsSecurityEnabled;
if(securityEnabled)
{
   //the rest of the verification process
}

COM+ Queued Components

.NET has a built-in mechanism for invoking a method call on an object: using a delegate asynchronously. The client creates a delegate class that wraps the method it wants to invoke synchronously, and the compiler provides definition and implementation for a BeginInvoke( ) method, which asynchronously calls the required method on the object. The compiler also generates the EndInvoke( ) method to allow the client to poll for the method completion. Additionally, .NET provides a helper class called AsyncCallback to manage asynchronous callbacks from the object once the call is done.

Compared with COM+ queued components, the .NET approach leaves much to be desired. First, .NET does not support disconnected work. Both the client and the server have to be running at the same time, and their machines must be connected to each other on the network. Second, the client's code in the asynchronous case is very different from the usual synchronous invocation of the same method on the object's interface. Third, there is no built-in support for transactional forwarding of calls to the server, nor is there an auto-retry mechanism. In short, you should use COM+ queued components if you want to invoke asynchronous method calls in .NET.

The ApplicationQueuing assembly attribute is used to configure queuing support for the hosting COM+ application. The ApplicationQueuing attribute has two public properties that you can set. The Boolean Enabled property corresponds to the Queued checkbox on the application's queuing tab. When set to true, it instructs COM+ to create a public message queue, named as the application, for the use of any queued components in the assembly. The second public property of ApplicationQueuing is the Boolean QueueListenerEnabled property. It corresponds to the Listen checkbox on the application's queuing tab. When set to true, it instructs COM+ to activate a listener for the application when the application is launched. For example, here is how you enable queued component support for your application and enable a listener:

//Must be a server application to use queued components
[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: ApplicationQueuing(Enabled = true,QueueListenerEnabled = true)] 

The ApplicationQueuing attribute has an overloaded default constructor that sets the Enabled attribute to true and the QueueListenerEnabled attribute to false. As a result, the following two statements are equivalent:

[assembly: ApplicationQueuing]
[assembly: ApplicationQueuing(Enabled = true,QueueListenerEnabled = false)]

Configuring Queued Interfaces

In addition to enabling queued component support at the application level, you must mark your interfaces as capable of receiving queued calls. You do that by using the InterfaceQueuing attribute. InterfaceQueuing has one public Boolean property called Enabled that corresponds to the Queued checkbox on the interface's Queuing tab.

[InterfaceQueuing(Enabled = true)]
public interface IMyInterface
{
   void MyMethod(  );
}

The InterfaceQueuing attribute has an overloaded default constructor that sets the Enabled property to true and a constructor that accepts a Boolean parameter. As a result, the following three statements are equivalent:

[InterfaceQueuing] 
[InterfaceQueuing(true)]
[InterfaceQueuing(Enabled = true)]

Note that your interface must adhere to the queued components design guidelines discussed in Chapter 8, such as no out or ref parameters. If you configure your interface as a queued interface using the InterfaceQueuing attribute and the interface is incompatible with queuing requirements, the registration process fails.

A Queued Component's Managed Client

The client of a queued component cannot create the queued component directly. It must create a recorder for its calls using the queue moniker. A C++ or a Visual Basic 6.0 program uses the CoGetObject( ) or GetObject( ) calls. A .NET managed client can use the static method BindToMoniker( ) of the Marshal class, defined as:

public static object BindToMoniker(string monikerName);

BindToMoniker( ) accepts a moniker string as a parameter and returns the corresponding object. The Marshal class is defined in the System.Runtime.InteropServices namespace.

The BindToMoniker( ) method of the Marshal class makes writing managed clients for a queued component as easy as if it were a COM client:

using System.Runtime.InteropServices;//for the Marshal class
 
IMyInterface obj;
obj =(IMyInterface)Marshal.BindToMoniker("queue:/new:MyNamespace.MyComponent");
obj.MyMethod(  );//call is recorded 

In the case of a COM client, the recorder records the calls the client makes. The recorder only dispatches them to the queued component queue (more precisely, to its application's queue) when the client releases the recorder. A managed client does not use reference counting, and the recorded calls are dispatched to the queued component queue when the managed wrapper around the recorder is garbage collected. The client can expedite dispatching the calls by explicitly forcing the managed wrapper around the recorder to release it, using the static DisposeObject( ) method of the ServicedComponent class, passing in the recorder object:

using System.Runtime.InteropServices;//for the Marshal class
 
IMyInterface obj;
obj =(IMyInterface)Marshal.BindToMoniker("queue:/new:MyNamespace.MyComponent");
obj.MyMethod(  );//call is recorded
 
//Expedite dispatching the recorded calls by disposing of the recorder
ServicedComponent sc = obj as ServicedComponent;
If(sc !=null)
  ServicedComponent.DisposeObject(sc);

You can use the IDisposable interface instead of calling DisposeObject().

Queued Component Error Handling

Due to the nature of an asynchronous queued call, managing a failure on both the client's side (failing to dispatch the calls) and the server's side (repeatedly failing to execute the call--a poison message) requires a special design approach. As discussed in Chapter 8, both the clients and server can use a queued component exception class to handle the error. You can also provide your product administrator with an administration utility for moving messages between the retry queues.

Queued component exception class

You can designate a managed class as the exception class for your queued component using the ExceptionClass attribute. Example 10-15 demonstrates using the ExceptionClass attribute.

Example 10-15: Using the ExceptionClass attribute to designate an error-handling class for your queued component

using COMSVCSLib;
 
public class MyQCException : IPlaybackControl,IMyInterface
{   
   public void FinalClientRetry(  ) {...}   
   public void FinalServerRetry(  ) {...}   
   public void MyMethod(  ){...}
}
[ExceptionClass("MyQCException")]
public class MyComponent :ServicedComponent,IMyInterface
{...}

In Example 10-15, when you register the assembly containing MyComponent with COM+, on the component's Advanced tab, the Queuing exception class field will contain the name of its exception class--in this case, MyQCException, as shown in Figure 10-3.

Figure 10-3. After registering the component in Example 10-15 with COM+, its Advanced tab contains the exception class

 

You need to know a few more things about designating a managed class as a queued component's exception class. First, it has nothing to do with .NET error handling via exceptions. The word exception is overloaded. As far as .NET is concerned, a queued component's exception class is not a .NET exception class. Second, the queued component exception class has to adhere to the requirements of a queued component exception class described in Chapter 8. These requirements include implementing the same set of queued interfaces as the queued component itself and implementing the IPlaybackControl interface. To add IPlaybackControl to your class definition you need to add a reference in your project to the COM+ Services type library. IPlaybackControl is defined in the COMSVCSLib namespace.

The MessageMover class

As explained in Chapter 8, COM+ provides you with the IMessageMover interface, and a standard implementation of it, for moving all the messages from one retry queue to another. Managed clients can access this implementation by importing the COM+ Services type library and using the MessageMover class, defined in the COMSVCSLib namespace. Example 10-16 implements the same use-case as Example 8-2.

Example 10-16: MessageMover is used to move messages from the last retry queue to the application's queue

using COMSVCSLib;
 
IMessageMover messageMover;
int moved;//How many messages were moved
 
messageMover = (IMessageMover) new MessageMover(  );
 
//Move all the messages from the last retry queue to the application's queue 
messageMover.SourcePath = @".\PRIVATE$\MyApp_4";
messageMover.DestPath   = @".\PUBLIC$\MyApp"; 
 
moved = messageMover.MoveMessages(  );

COM+ Loosely Coupled Events

.NET provides managed classes with an easy way to hook up a server that fires events with client sinks. The .NET mechanism is certainly an improvement over the somewhat cumbersome COM connection point protocol, but the .NET mechanism still suffers from all the disadvantages of tightly coupled events, as explained at the beginning of Chapter 9. Fortunately, managed classes can easily take advantage of COM+ loosely coupled events.

The EventClass attribute is used to mark a serviced component as a COM+ event class, as shown in Example 10-17.

Example 10-17: Designating a serviced component as an event class using the EventClass attribute

public interface IMySink
{   
   void OnEvent1(  );   
   void OnEvent2(  );
}
 
[EventClass]
public class MyEventClass : ServicedComponent,IMySink
{
   public void OnEvent1(  )
   {
      throw(new NotImplementedException(exception));
   }
   public void OnEvent2(  )
   {
      throw(new NotImplementedException(exception));
   }
   const string exception = @"You should not call an event class directly. 
                            Register this assembly using RegSvcs /reconfig";
}

The event class implements a set of sink interfaces you want to publish events on. Note that it is pointless to have any implementation of the sink interface methods in the event class, as the event class's code is never used. It is used only as a template, so that COM+ could synthesize an implementation, as explained in Chapter 9 (compare Example 10-17 with Example 9-1). This is why the code in Example 10-17 throws an exception if anybody tries to actually call the methods (maybe as a result of removing the event class from the Component Services Explorer).

When you register the assembly with COM+, the event class is added as a COM+ event class, not as a regular COM+ component. Any managed class (not just serviced components) can publish events. Any managed class can also implement the sink's interfaces, subscribe, and receive the events. For example, to publish events using the event class from Example 10-17, a managed publisher would write:

IMySink sink;
sink =  (IMySink)new MyEventClass(  );
sink.OnEvent1(  );

The OnEvent1( ) method returns once all subscribers have been notified, as explained in Chapter 9.

Persistent subscriptions are managed directly via the Component Services Explorer because adding a persistent subscription is a deployment-specific activity. Transient subscriptions are managed in your code, similar to COM+ transient subscribers.

The EventClass attribute has two public Boolean properties you can set, called AllowInprocSubscribers and FireInParallel. These two properties correspond to the Fire in parallel and Allow in-process subscribers, respectively, on the event class's Advanced tab. You can configure these values on the event class definition:

[EventClass(AllowInprocSubscribers = true,FireInParallel=true)]
public class MyEventClass : ServicedComponent,IMySink
{...}

The EventClass attribute has an overloaded default constructor. If you do not specify a value for the AllowInprocSubscribers and FireInParallel properties, it sets them to true and false, respectively. Consequently, the following two statements are equivalent:

EventClass]
[EventClass(AllowInprocSubscribers = true,FireInParallel=false)]

Summary

Throughout this book, you have learned that you should focus your development efforts on implementing business logic in your components and rely on COM+ to provide the component services and connectivity they need to operate. With .NET, Microsoft has reaffirmed its commitment to this development paradigm. From a configuration management point of view, the .NET integration with COM+ is superior to COM under Visual Studio 6.0 because .NET allows you to capture your design decisions in your code, rather than use the separate COM+ Catalog. This development is undoubtedly just the beginning of seamless support and better integration of the .NET development tools, runtime, component services, and the component administration environment. COM+ itself (see Appendix B) continues to evolve, both in features and in usability, while drawing on the new capabilities of the .NET platform. The recently added ability to expose any COM+ component as a web service is only a preview of the tighter integration of .NET and COM+ we can expect to see in the future.

Back to: COM and .NET Component Services


oreilly.com Home | O'Reilly Bookstores | How to Order | O'Reilly Contacts
International | About O'Reilly | Affiliated Companies | Privacy Policy

© 2001, O'Reilly & Associates, Inc.
webmaster@oreilly.com