Mark Wilson I am the creator of TopXML. I am available for international and local (Australia) contracts. I am a Solution Architect/Business Analyst. I have worked in IT in several countries (NZ, Australia, South Africa, UK) building and training teams for government and very large non-governmental organizations. I am ex-Microsoft Consulting Services. I wrote the first book on Microsoft XML published in 2000 called XML Programming with VB and ASP. Most recently I have been building tools for the SEO industry. Ask me for a 37 point SEO health-checkup for your website.
First posted :
09/03/2003
Times viewed :
399
COM and .NET Component Services
By Juval Löwy
September 2001
0-596-00103-7, Order Number: 1037
384 pages, $39.95
.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.
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:
Manually, using a command line utility called
RegSvcs.exe.
Dynamically, by having the client program register
your assembly automatically.
Programmatically, by writing code that does the
registration for you using a utility class provided by .NET.
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:
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:
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:
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:
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:
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:
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:
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.
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:
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:
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
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:
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
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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.
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:
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:
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:
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.