|
|
|
|
COM and .NET Component ServicesBy Juval LöwySeptember 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.ServicedComponentis defined in theSystem.EnterpriseServicesnamespace. Example 10-1 demonstrates how to write a .NET serviced component that implements theIMessageinterface and displays a message box with "Hello" in it when the interface'sShowMessage( )method is called.
Example 10-1: A simple .NET serviced component namespace MyNamespace{using System.EnterpriseServices;using System.Windows.Forms;//for the MessageBox classpublic interface IMessage{void ShowMessage( );}/// <summary>/// Plain vanilla .NET serviced component/// </summary>public class MyComponent:ServicedComponent,IMessage{public MyComponent( ) {}//constructorpublic 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
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
ApplicationNameassembly attribute:[assembly: ApplicationName("MyApp")]If you do not provide an application name, .NET uses the assembly name. The
ApplicationNameattribute (and the rest of the serviced components attributes) is defined in theSystem.EnterpriseServicesnamespace. 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.dllThe 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
/cswitch:RegSvcs.exe /c MyApp MyAssembly.dllOr if the name is specified in the assembly:
RegSvcs.exe /c MyAssembly.dllWhen using the
/cswitch, 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
/fcswitch:RegSvcs.exe /fc MyApp MyAssembly.dllOr if the name is specified in the assembly:
RegSvcs.exe /fc MyAssembly.dllIf 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
/reconfigswitch:RegSvcs.exe /reconfig /fc MyApp MyAssembly.dllThe
/reconfigswitch 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
RegistrationHelperto perform the registration.RegistrationHelperimplements theIRegistrationHelperinterface, whose methods are used to register and unregister assemblies. For example, theInstallAssembly( )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 useRegistrationHelperyourself 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
ApplicationIDattribute is defined in theSystem.EnterpriseServicesnamespace.The Guid Attribute
Instead of having the registration process generate a CLSID for your serviced component, you can specify one for it using the
Guidattribute:using System.Runtime.InteropServices;[Guid("260C9CC7-3B15-4155-BF9A-12CB4174A36E")]public class MyComponent :ServicedComponent,IMyInterface{...}The
Guidattribute is defined in theSystem.Runtime.InteropServicesnamespace.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
ProgIDattribute:using System.Runtime.InteropServices;[ProgId("My Serviced Component")]public class MyComponent :ServicedComponent,IMyInterface{...}The
ProgIdattribute is defined in theSystem.Runtime.InteropServicesnamespace.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
ApplicationActivationassembly 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
ApplicationActivationattribute, 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
ApplicationActivationwill be extended to include the value ofActivationOption.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
ApplicationActivationattribute, there are no properties and the constructor must accept an enum parameter of typeActivationOption, defined as:enum ActivationOption{Server,Library}There is no default constructor for the
ApplicationActivationattribute.The
ApplicationActivationattribute is defined in theSystem.EnterpriseServicesnamespace. 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
ServicedComponentmethods must add a reference toSystem.EnterpriseServicesto its project. Other clients, which only use the interfaces provided by your serviced components, need not add the reference.The Description Attribute
The
Descriptionattribute 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 theDescriptionattribute 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 theContextUtilclass. Because the methods and properties are static, you do not have to instantiate aContextUtilobject--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 theContextIdstatic property ofContextUtil:using System.Diagnostics;//For the Trace classGuid contextID = ContextUtil.ContextId;String traceMessage = "Context ID is " + contextID.ToString( );Trace.WriteLine(traceMessage);
ContextUtilhas 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 theseContextUtilproperties later in this chapter.COM+ Context Attributes
You can decorate (apply attributes to) your class with two context-related attributes. The attribute
MustRunInClientContextinforms 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
MustRunInClientContextwith afalseparameter 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
MustRunInClientContextattribute withfalse) is useful when you combine it with the/reconfigswitch 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
MustRunInClientContextattribute class has an overloaded default constructor. If you useMustRunInClientContextwith no parameters, the default constructor usestruefor the attribute value. As a result, the following two statements are equivalent:[MustRunInClientContext][MustRunInClientContext(true)]The second COM+ context-related attribute is the
EventTrackingEnabledattribute. 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
EventTrackingEnabledattribute class also has an overloaded default constructor. If you construct it with no parameters, the default constructor usestruefor the attribute value. As a result, the following two statements are equivalent:[EventTrackingEnabled][EventTrackingEnabled(true)]COM+ Object Pooling
The
ObjectPoolingattribute is used to configure every aspect of your component's object pooling. TheObjectPoolingattribute 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, andCreationTimeoutproperties are public properties of theObjectPoolingattribute 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
ObjectPoolingattribute has a Boolean property called theEnabledproperty. If you do not specify a value for it (trueorfalse), the attribute's constructor sets it totrue. In fact, the attribute's constructor has a few overloaded versions--a default constructor that sets theEnabledproperty totrueand 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 fromServicedComponent, which couples the client to the type used and renders many benefits of interface-based programming useless. Even worse, the client only has to callDisposeObject( )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
ServicedComponentimplement a special interface (defined in theSystemnamespace) calledIDisposable, defined as:public interface IDisposable{void Dispose( );}
ServicedComponentimplementation ofDispose( )returns the pooled object to the pool.Having the
Dispose( )method on a separate interface allows the client to query for the presence ofIDisposableand 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
IDisposabletechnique 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,IDisposableprovides a type-safe, component-oriented way of having the client dispose of the object without being too coupled to itstype.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
JustInTimeActivationattribute:[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
JustInTimeActivationattribute, JITA support is disabled when you register your component with COM+ (unlike the COM+ default of enabling JITA). TheJustInTimeActivationclass 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
DeactivateOnReturnproperty of theContextUtilclass. 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 returnContextUtil.DeactivateOnReturn = true;}//other methodsprotected 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
DeactivateOnReturnproperty tofalse.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
IObjectControlfor that purpose. The .NET base classServicedComponentalready implementsIObjectControl, 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 theFinalize( )method (the destructor in C#), andFinalize( )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
ServicedComponentimplementation ofIObjectControl.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
IObjectConstructinterface 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 theIObjectConstructinterface as a virtual method (it has only one method). Your derived serviced component can override theConstruct( )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
ConstructionEnabledattribute:[ConstructionEnabled(Enabled = true,Default = "My String")]public class MyComponent :ServicedComponent{public override void Construct(string constructString){...}}The
ConstructionEnabledattribute has two public properties.Enabledenables construction string support for your serviced component in the Component Services Explorer (once the component is registered) andDefaultprovides 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
ConstructionEnabledattribute has two overloaded constructors. One constructor accepts a Boolean value for theEnabledproperty; the default constructor sets the value of theEnabledproperty totrue. You can also set the value of theEnabledproperty 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
Transactionattribute. TheTransactionattribute's constructor accepts an enum parameter of typeTransactionOption, defined as:public enum TransactionOption{Disabled,NotSupported,Supported,Required,RequiresNew}For example, to configure your serviced component to require a transaction, use the
TransactionOption.Requiredvalue:[Transaction(TransactionOption.Required)]public class MyComponent :ServicedComponent{...}The five enum values of
TransactionOptionmap to the five COM+ transaction support options discussed in Chapter 4.When you use the
Transactionattribute to mark your serviced component to use transactions, you implicitly set it to use JITA and require activity-based synchronization as well.The
Transactionattribute has an overloaded default constructor, which sets the transaction support toTransactionOption.Required. As a result, the following two statements are equivalent:[Transaction][Transaction(TransactionOption.Required)]Voting on the Transaction
Not surprisingly, you use the
ContextUtilclass to vote on the transaction's outcome.ContextUtilhas a static property of the enum typeTransactionVotecalledMyTransactionVote.TransactionVoteis 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 returnsfinally{ContextUtil.DeactivateOnReturn = true;}}//helper methodsprotected 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
HRESULTfrom theDoWork( )helper method to decide on the transaction's outcome. A serviced component, like any other managed component, does not useHRESULTreturn codes for error handling; it uses exceptions instead. In Example 10-5 the component catches any exception that was thrown in thetryblock by theDoWork( )method and votes to abort in thecatchblock.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 totrueas 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 wellContextUtil.DeactivateOnReturn = true;ContextUtil.MyTransactionVote = TransactionVote.Abort;GetState(objectIdentifier);DoWork( );SaveState(objectIdentifier);ContextUtil.MyTransactionVote = TransactionVote.Commit;}//helper methodsprotected 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
AutoCompletemethod attribute. During the registration process, the method is configured to use COM+ auto-deactivation whenAutoCompleteis 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 theAutoCompleteattribute do not need to vote explicitly on their transaction outcome. Example 10-7 shows a transactional serviced component using theAutoCompletemethod 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 methodsprotected 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
trueif the method did not throw an exception and the consistency bit tofalseif it did. As a result, the transaction is committed if no exception is thrown and aborted otherwise.Nontransactional JITA objects can also use the
AutoCompleteattribute to deactivate themselves automatically on method return.The
AutoCompleteattribute has an overloaded default constructor that usestruefor the attribute construction. Consequently, the following two statements are equivalent:[AutoComplete][AutoComplete(true)]The
AutoCompleteattribute 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 useTransactionContextas well. To use theTransactionContextobject, add to the project references the COM+ services type library. TheTransactionContextclass is in theCOMSVCSLibnamespace.The
TransactionContextclass is especially useful in situations in which the class is a managed .NET component that derives from a class other thanServicedComponent. Remember that a .NET component can only derive from one concrete class and since the class already derives from a concrete class other thanServicedComponent, it cannot use theTransactionattribute. Nevertheless, theTransactionContextclass gives this client an ability to initiate and manage a transaction.Example 10-8 demonstrates usage of the
TransactionContextclass, 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
WebMethodattribute on the public methods exposed as web services. Example 10-9 shows theMyWebServiceweb service that provides theMyMessageweb 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
WebServicebase class, defined in theSystem.Web.Servicesnamespace (see Example 10-9). TheWebServicebase 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 fromWebServiceis that it is not derived fromServicedComponent, and .NET does not allow multiple inheritance of implementation.To overcome this hurdle, the
WebMethodattribute has a public property calledTransactionOption, of the enum typeEnterprise.Services.TransactionOptiondiscussed previously.The default constructor of the
WebMethodattribute sets this property toTransactionOption.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.Supporteddoes 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
TransactionOptionare confusing. To avoid making them the source of errors and misunderstandings, useTransactionOption.RequiresNewwhen you want transaction support for your web method; useTransactionOption.Disabledwhen 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
AutoCompleteattribute). Of course, the web service can still useContextUtilto 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.Pagebase class, not fromServicedComponent, 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
TransactionModeof typeTransactionOption. You can assign a value of typeTransactionOptiontoTransactionMode, to configure transaction support for your web form. You can assignTransactionModeprogrammatically 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 toRequiresNew, 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
ContextUtilmethods. 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
Synchronizationattribute to the class definition. TheSynchronizationattribute's constructor accepts an enum parameter of typeSynchronizationOption, defined as:public enum SynchronizationOption{Disabled,NotSupported,Supported,Required,RequiresNew}For example, use the
SynchronizationOption.Requiredvalue to configure your serviced component to require activity-based synchronization:[Synchronization(SynchronizationOption.Required)]public class MyComponent :ServicedComponent{...}The five enum values of
SynchronizationOptionmap to the five COM+ synchronization support options discussed in Chapter 5.The
Synchronizationattribute has an overloaded default constructor, which sets synchronization support toSynchronizationOption.Required. As a result, the following two statements are equivalent:[Synchronization][Synchronization(SynchronizationOption.Required)]
TIP: The
System.Runtime.Remoting.Contextnamespace contains a context attribute calledSynchronizationthat can be applied to context-bound .NET classes. This attribute accepts synchronization flags similar toSynchronizationOption, and initially looks like another version of theSynchronizationclass attribute. However, theSynchronizationattribute in theContextnamespace provides synchronization based on physical threads, unlike theSynchronizationattribute in theEnterpriseServicesnamespace, 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
COMAdminnamespace. 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 indexcatalog = (ICOMAdminCatalog)new COMAdminCatalog( );applicationCollection = (ICatalogCollection)catalog.GetCollection("Applications");//Read the information from the catalogapplicationCollection.Populate( );applicationCount = applicationCollection.Count;for(i = 0;i< applicationCount;i++){//Get the current applicationapplication= (ICatalogObject)applicationCollection.get_Item(i);int index = i+1;String traceMessage = index.ToString()+". "+application.Name.ToString( );Trace.WriteLine(traceMessage);}
TIP: The
System.EnterpriseServices.Adminnamespace 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 useSystem.EnterpriseServices.Adminnamespace directly. The resulting code, when programming directly using theSystem.EnterpriseServices.Adminnamespace, 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
ApplicationAccessControlis used to configure all the settings on the hosting COM+ application's Security tab.You can use
ApplicationAccessControlto turn application-level authentication on or off:[assembly: ApplicationAccessControl(true)]The
ApplicationAccessControlattribute has a default constructor, which sets authorization totrueif 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
ApplicationAccessControlattribute at all, then when you register your assembly, the COM+ default takes effect and application-level authorization is turned off.The
ApplicationAccessControlattribute has three public properties you can use to set the access checks, authentication, and impersonation level. TheAccessChecksLevelproperty accepts an enum parameter of typeAccessChecksLevelOption, defined as:public enum AccessChecksLevelOption{Application,ApplicationComponent}
AccessChecksLevelis 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 theApplicationAccessControlattribute's constructors set the access level toAccessChecksLevelOption.ApplicationComponent, the same as the COM+ default.The
Authenticationproperty accepts an enum parameter of typeAuthenticationOption, defined as:public enum AuthenticationOption{None,Connect,Call,Packet,Integrity,Privacy,Default}The values of
AuthenticationOptionmap to the six authentication options discussed in Chapter 7. If you do not specify an authentication level or if you use theDefaultvalue, theApplicationAccessControlattribute's constructors set the authentication level toAuthenticationOption.Packet, the same as the COM+ default.The
Impersonationproperty accepts an enum parameter of typeImpersonationLevelOption, defined as:public enum ImpersonationLevelOption{Anonymous,Identify,Impersonate,Delegate,Default}The values of
ImpersonationLevelOptionmap to the four impersonation options discussed in Chapter 7. If you do not specify an impersonation level or if you use theDefaultvalue, then theApplicationAccessControlattribute's constructors set the impersonation level toImpersonationLevelOption.Impersonate, the same as the COM+ default.Example 10-11 demonstrates using the
ApplicationAccessControlattribute 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 toIdentify.
Example 10-11: Configuring a server application security [assembly: ApplicationActivation(ActivationOption.Server)][assembly: ApplicationAccessControl(true,//Authentication is onAccessChecksLevel=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 asAuthenticationOption.Packet. Example 10-12 demonstrates how to use theApplicationAccessControlto configure the security setting ofa library application.
Example 10-12: Configuring a library application security [assembly: ApplicationActivation(ActivationOption.Library)][assembly: ApplicationAccessControl(true,//AuthenticationAccessChecksLevel=AccessChecksLevelOption.ApplicationComponent,//use AuthenticationOption.None to turn off authentication,//and any other value to turn it onAuthentication=AuthenticationOption.Packet)]Component-Level Access Checks
The component attribute
ComponentAccessControlis 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. TheComponentAccessControlattribute'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
ComponentAccessControlattribute has an overloaded default constructor that usestruefor 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
SecurityRoleattribute 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
SecurityRoleattribute has two public properties you can set. The first isDescription. Any text assigned to theDescriptionproperty 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
SetEveryoneAccessBoolean property. If you setSetEveryoneAccesstotrue, 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 tofalse, then no user is added during registration and you have to explicitly add users during deployment using the Component Services Explorer. TheSecurityRoleattribute sets the value ofSetEveryoneAccessby default totrue. 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
SetEveryoneAccessproperty tofalse:#if DEBUG[assembly: SecurityRole("Manager")]#else[assembly: SecurityRole("Manager",SetEveryoneAccess = false)]#endifAssigning Roles to Component, Interface, and Method
The
SecurityRoleattribute 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")]publicinterface 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 classSecurityCallContextthat gives you access to the security parameters of the current call.SecurityCallContextencapsulates the COM+ call-object's implementation ofISecurityCallContext, discussed in Chapter 7. The classSecurityCallContexthas a public static property calledCurrentCall.CurrentCallis a read-only property of typeSecurityCallContext(it returns an instance of the same type). You use theSecurityCallContextobject returned fromCurrentCallto 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 sufficientcredentials to transfer this sum"));}DoTransfer(sum,accountSrc,accountDest);//Helper method}//Other methods}You should use the Boolean property
IsSecurityEnabledofSecurityCallContextto verify that security is enabled before accessing theIsCallerInRole( )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 theEndInvoke( )method to allow the client to poll for the method completion. Additionally, .NET provides a helper class calledAsyncCallbackto 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
ApplicationQueuingassembly attribute is used to configure queuing support for the hosting COM+ application. TheApplicationQueuingattribute has two public properties that you can set. The BooleanEnabledproperty corresponds to the Queued checkbox on the application's queuing tab. When set totrue, 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 ofApplicationQueuingis the BooleanQueueListenerEnabledproperty. It corresponds to the Listen checkbox on the application's queuing tab. When set totrue, 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
ApplicationQueuingattribute has an overloaded default constructor that sets theEnabledattribute totrueand theQueueListenerEnabledattribute tofalse. 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
InterfaceQueuingattribute.InterfaceQueuinghas one public Boolean property calledEnabledthat corresponds to the Queued checkbox on the interface's Queuing tab.[InterfaceQueuing(Enabled = true)]public interface IMyInterface{void MyMethod( );}The
InterfaceQueuingattribute has an overloaded default constructor that sets theEnabledproperty totrueand 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
outorrefparameters. If you configure your interface as a queued interface using theInterfaceQueuingattribute 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
queuemoniker. A C++ or a Visual Basic 6.0 program uses theCoGetObject( )orGetObject( )calls. A .NET managed client can use the static methodBindToMoniker( )of theMarshalclass, defined as:public static object BindToMoniker(string monikerName);
BindToMoniker( )accepts a moniker string as a parameter and returns the corresponding object. TheMarshalclass is defined in theSystem.Runtime.InteropServicesnamespace.The
BindToMoniker( )method of theMarshalclass makes writing managed clients for a queued component as easy as if it were a COM client:using System.Runtime.InteropServices;//for the Marshal classIMyInterface obj;obj =(IMyInterface)Marshal.BindToMoniker("queue:/new:MyNamespace.MyComponent");obj.MyMethod( );//call is recordedIn 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 theServicedComponentclass, passing in the recorder object:using System.Runtime.InteropServices;//for the Marshal classIMyInterface obj;obj =(IMyInterface)Marshal.BindToMoniker("queue:/new:MyNamespace.MyComponent");obj.MyMethod( );//call is recorded//Expedite dispatching the recorded calls by disposing of the recorderServicedComponent sc = obj as ServicedComponent;If(sc !=null)ServicedComponent.DisposeObject(sc);You can use the
IDisposableinterface instead of callingDisposeObject().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
ExceptionClassattribute. Example 10-15 demonstrates using theExceptionClassattribute.
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
MyComponentwith 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
IPlaybackControlinterface. To addIPlaybackControlto your class definition you need to add a reference in your project to the COM+ Services type library.IPlaybackControlis defined in theCOMSVCSLibnamespace.The MessageMover class
As explained in Chapter 8, COM+ provides you with the
IMessageMoverinterface, 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 theMessageMoverclass, defined in theCOMSVCSLibnamespace. 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 movedmessageMover = (IMessageMover) new MessageMover( );//Move all the messages from the last retry queue to the application's queuemessageMover.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
EventClassattribute 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
EventClassattribute has two public Boolean properties you can set, calledAllowInprocSubscribersandFireInParallel. 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
EventClassattribute has an overloaded default constructor. If you do not specify a value for theAllowInprocSubscribersandFireInParallelproperties, it sets them totrueandfalse, 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
© 2001, O'Reilly & Associates, Inc.
webmaster@oreilly.com