Desktop Developer

Build Serviced Components the Right Way

Take advantage of these practical guidelines for implementing serviced components properly and reduce coding and maintenance headaches now?and in the future.

Technology Toolbox: VB.NET

Editor's Note: This article is excerpted from Chapter 31, "Serviced Components," of Francesco Balena and Giuseppe Dimauro's book, Practical Guidelines and Best Practices for Microsoft Visual Basic and Microsoft Visual C# Developers, with permission from Microsoft Press [2005, ISBN: 0735621721, www.microsoft.com/learning/books/]. It has been edited for length and format to fit the magazine.

Serviced components are Microsoft .NET Framework objects that run under Component Services and can leverage the full range of COM+ services, including Just In Time Activation (JITA), automatic transactions, synchronization, object pooling, role-based security (RBS), and programmatic security. You can run a serviced component as a library component (in the client's process) or a server component (in a different process, possibly running on a different computer), even though a few COM+ services are available only in server libraries.

If you aren't familiar with serviced components and their benefits, you can read the whole story on MSDN (see Additional Resources). If you have worked with serviced components already, we're sure you'll find some interesting tips in this article. Note that a couple code examples in this article assume that you have imported a pair of namespaces with the Imports (Visual Basic) statement: System.Data.SwlClient and System.EnterpriseServices.

It's important that you consider carefully the pros and cons of using transactional serviced components to implement COM+ transactions as opposed to using standard ADO.NET transactions. Implementing transactions in serviced components (COM+ transactions) offers several advantages, including the support for distributed databases, a higher degree of independence from the database, and a cleaner object-oriented design (for example, you can use attributes to select the transaction isolation level). However, COM+ transactions use the Microsoft Distributed Transaction Coordinator (MS DTC). DTC-based transactions can be from 10 to 50 percent slower than ADO.NET transactions. When working with a single database server, you might decide to use ADO.NET transactions from inside a standard .NET class rather than encapsulating the database code in a serviced component.

Another factor to consider when deciding which type of transaction to adopt is that serviced components running under Microsoft Windows 2000 can use only the Serializable isolation level; therefore, an ADO.NET transaction can give you more flexibility. Under Microsoft Windows Server 2003, you can use the Transaction attribute to select the actual isolation level (we'll return to this later).

In some cases, you can reduce the overhead of COM+ transactions from inside serviced components by turning off automatic enlistment of an ADO.NET connection. If you're using the Microsoft SQL Server .NET data provider, you can disable automatic enlistment by setting the Enlist attribute to false in the connection string:

Data Source=.;Integrated 
   Security=SSPI;Initial 
   Catalog=Pubs;Enlist=false

You should also avoid shared members in types that inherit from System.EnterpriseServices.ServicedComponent. This is because .NET serviced components support remote method invocation of their instance methods only. Also, COM+ interception doesn't work with shared members; thus, the transactional context doesn't flow correctly through shared method calls.

Guidelines Help You Choose
You should keep a handful of rules in mind when deciding whether to implement a library or a server COM+ component. First, server components can run remotely, on a computer other than the client's machine, and therefore can improve the application's scalability. Also, server components can easily impersonate an identity other than the client's identity. Next, server components live inside separate applications that can be restarted automatically under certain conditions. If you don't need the security, scalability, and fault-tolerance features of server COM+ components, you should use client components to achieve better performance.

All the arguments passed to and returned from a method defined in a server component must either be marked as serializable or derive from MarshalByRefObject. Also, server components can run inside a Windows service. Both library and server components must reside in strong-named assemblies. Furthermore, server components must be registered in the GAC.

You decide between a server or library COM+ component by marking the assembly with a suitable ApplicationActivation attribute. Do this by assigning a value to the ApplicationName, ApplicationID, ApplicationActivation, and Description attributes for assemblies that contain serviced components. The ApplicationName attribute is the name that identifies the application in the Component Services administration snap-in; the Description attribute is used to describe the application itself. The ApplicationID attribute assigns an explicit ID to the application. (If omitted, this ID is generated automatically when the component is registered.) The ApplicationName and ApplicationID attributes affect what you see in the General tab of the application's Properties window.

Note that an explicit ApplicationID value is especially useful for having multiple assemblies share the same COM+ application (and therefore the same server-side process), which in turn optimizes cross-component communication and marshaling. However, keep in mind that this attribute prevents you from using COM+ 1.5 partitions; thus, you must omit this attribute if you plan to use partitions:

<Assembly: ApplicationName( _
   "BankMoneyMover")> _
   <Assembly: Description( _
   "Components for moving money " & _
   "between accounts")> _
   <Assembly: ApplicationID( _
   "F088FCFF-6FF0-496B-9121-" & _
   DC9EB9DAEFFA")> 

'This is a library COM+ component. 
<Assembly: ApplicationActivation( _
   ActivationOption.Library)> 

'Assemblies containing serviced 
'components must have a strong name. 
<Assembly: AssemblyKeyFile( _
   "c:\codearchitects.snk")> 

Configuration attributes are important, especially in the developing and test phases, because they help to register the serviced component correctly on the first launch and therefore support XCOPY deployment (this is known as dynamic or lazy registration). However, most attributes related to serviced components are used only if the COM+ application doesn't exist yet. On the customer's site, a user without administrative privileges might first launch the application, and the installation would fail. For this reason, you should always rely on the regsvcs tool to register the component.

Utilize Attributes
The only attributes that are always read from the metadata in the component and that supersede the attributes in the COM+ catalog are JustInTimeActivation, AutoComplete, and ObjectPooling, plus the SecurityRole attribute when used at the method level. The ObjectPooling attribute in source code can enable or disable object pooling, but COM+ always uses the pool size defined in the COM+ catalog.

You need to apply the ClassInterface attribute to all serviced components to make them expose a dual interface if you don't need to apply RBS at the method level. If you omit this attribute, the Component Services MMC snap-in doesn't list the component's individual methods. Also, late-bound calls from unmanaged clients would ignore the AutoComplete attribute, and you'd be forced to commit or abort the transaction explicitly by using code. However, you can't apply this attribute when you need to apply method-level RBS:

<ClassInterface( _
   ClassInterfaceType.AutoDual)> _ 
Public Class MoneyMover 
   Inherits ServicedComponent 
   ... 
End Class 

You also need to mark serviced components with the JustInTimeActivation attribute. A JIT-activated component is more scalable and resource-savvy because the COM+ infrastructure instantiates it only when one of its methods is invoked and destroys it when the method completes, assuming that the method signals that the task has been completed or is marked with an AutoComplete attribute. Also, pooled objects should always be marked with the JustInTimeActivation attribute. On the other hand, you should only use the JustInTimeActivation attribute for components that are designed to be used in a stateless fashion. You can't just add this attribute to a nontransactional component that you have already tested because the attribute changes the component's lifetime and, consequently, the way clients should use the component.

Another potential problem of JIT-activated components: The server must keep alive a wrapper of the component. Such a wrapper might take a significant amount of memory on the server; therefore, unnecessarily using this attribute can affect the application's performance and scalability negatively.

The JustInTimeActivation attribute is primarily useful for preventing clients from using a stateless component in an incorrect way. If you're using a nontransactional component, you can usually get better performance by omitting this attribute and letting the client release the object explicitly. If the client is itself stateless—for example, it's an ASP.NET Web Forms application—using this attribute is superfluous and you can probably avoid it by using nontransactional and nonpoolable components.

A transactional type (a class flagged with the Transaction attribute) is also implicitly a JIT-activated component. However, you should apply an explicit JustInTimeActivation attribute even to transactional types to make the code more readable and avoid problems if you later decide to remove the Transaction attribute:

<Transaction( _
   TransactionOption.Required), _
   JustInTimeActivation()> _
   Public Class MoneyMover _
   Inherits ServicedComponent 
   ... 
   End Class 

You control the outcome of a transaction by applying the AutoComplete attribute, rather than by invoking the SetComplete or SetAbort methods. Explicitly throw exceptions if the transaction should be aborted and avoid catching exceptions (unless you rethrow them) when calling other components or methods exposed by .NET Framework objects. You should do it this way because a serviced component should throw an exception whenever something goes wrong. Using the AutoComplete attribute helps you enforce this rule and makes your code more concise and easier to debug:

<AutoComplete()> _ 
   Public Sub TransferMoney(ByVal _
   accountID As Integer, ByVal _
   amount As Decimal) 
   ... 
End Sub 

Use the Default Serializable Value
As a rule, you should use the default Serializable value for the Transaction attribute because sticking to the default isolation level makes the component easier to reuse and avoids several potential problems. However, keep in mind that you can usually achieve better performance by using a different isolation level. (You can set a nondefault value only in Microsoft Windows XP and Windows Server 2003 platforms.)

The isolation level of a transaction is determined by the root component?that is, the first transactional component in the call chain. If this root component calls a child component whose isolation level is equal to or higher than the root's isolation level, everything works smoothly; otherwise, the cross-component call will fail with an E_ISOLATIONLEVELMISMATCH error.

You can also set the transaction support and the isolation level from the Component Services MMC snap-in (see Figure 1):

<Transaction( _
   TransactionOption.Required, _
   Isolation:= _
   TransactionIsolationLevel. _
   Serializable)> _ 
   Public Class MoneyMover 
   Inherits ServicedComponent 
   ... 
End Class

You should consider using the TransactionIsolationLevel.Any value as the isolation level for nonroot components. This special value forces the component to use the isolation level set by the component that is the root of the current transaction and offers a simple mechanism for making the component reusable in different situations:

<Transaction( _
   TransactionOption.Supported, _
   Isolation:= _
   TransactionIsolationLevel.Any)> _ 
   Public Class MoneyMover 
   Inherits ServicedComponent 
   ... 
End Class 

If a type exposes methods that require different isolation levels, consider creating a facade component that uses two (or more) types marked with different Transaction attributes.

For example, assume you have one method that updates a database and requires the Serializable level, whereas another method performs a read operation for which a ReadCommitted level would be enough. If a component exposes both these methods, the best you can do is mark the component with a Transaction attribute that specifies a Serializable isolation level and accept the unnecessary overhead that results when the latter method is invoked. You can avoid this overhead by creating two additional classes: one that performs all the write operations at the Serializable level, and one that performs all the read operations at the ReadCommitted level. The original component would have no Transaction attribute and would be responsible only for dispatching calls to one of the two types, depending on whether it's a write or a read operation.

You can override the CanBePooled method to ensure that the object is returned to the pool as soon as it has completed its job. Remember that pooled objects should be marked with the JustInTimeActivation attribute. Object pooling enables you to use resources effectively if clients create many objects and these objects take a significant time to initialize. Also, pooling gives you the ability to configure the maximum number of objects that can be running at any given time. You need to employ the technique described in this guideline because serviced components aren't poolable by default. There are caveats, though. You might decide not to use object pooling for objects that initialize quickly. Also, you shouldn't use object pooling to implement a singleton model by forcing the maximum size of the pool to one.

Make a Component Poolable
You can make a component poolable by marking it with an ObjectPooling attribute. You can also specify the minimum and/or the maximum number of objects in the pool, although you should keep in mind the values entered in the Component Services MMC snap-in have higher priority than those specified in the ObjectPooling attribute. Also, the higher value you assign to the MinPoolSize property, the longer the first instantiation of the component will take:

<ObjectPooling(True, MinPoolSize:=4, _
   MaxPoolSize:=20), _
   JustInTimeActivation()> _ 
   Public Class MoneyMover 
   Inherits ServicedComponent 

   Protected Overrides Function _
      CanBePooled() As Boolean 
      Return True 
End Function 
End Class 

You can add an assembly-level ApplicationAccessControl attribute to enable COM+ role-based security and to enforce checks at the process and component level. In .NET Framework version 1.1, the COM+ security is enabled by default if the ApplicationAccessControl attribute is omitted; in version 1.0, COM+ security was disabled by default. Adding this attribute explicitly is recommended to improve readability:

<Assembly: ApplicationAccessControl( _
   True, AccessChecksLevel:= _
   AccessChecksLevelOption. _
   ApplicationComponent)>

In server applications, you should set the authentication level to Privacy, unless it is safe to use less severe settings. The authentication level of a library component is inherited from the client process. The Authentication property of the ApplicationAccessControl attribute lets you decide how a server component authenticates data coming from the caller. Setting the authentication level to Privacy ensures that COM+ authenticates the caller's credentials and encrypts each data packet, thus providing the most secure type of authentication but affecting performance negatively.

If data sniffing isn't an issue, you might decide to use a more efficient setting, such as Connect (authenticates credentials only when the connection is established), Call (authenticates credentials on each call), Packet (authenticates credentials and ensures that all packets are received), or Integrity (authenticates credentials and ensures that no data packet has been modified):

<Assembly: ApplicationAccessControl( _
   True, AccessChecksLevel:= _
   AccessChecksLevelOption. _
   ApplicationComponent, _
   Authentication:= _
   AuthenticationOption.Privacy)

Remember that the actual authentication level used by a COM+ component depends also on the authentication level set by the client. When the component's and the client's authorization levels differ, COM+ uses the higher level of the two. If the client is an ASP.NET application, you can configure its authentication level by means of the comAuthenticationLevel attribute in the <processModel> element in machine.config.

In server applications, set the impersonation level to Identify, unless you need to enable impersonation or delegation. The impersonation level of a library COM+ component is inherited from the client process and can't be changed. The ImpersonationLevel property of the ApplicationAccessControl attribute lets you decide whether another component called by the current COM+ component can discover the identity of the caller and can impersonate the caller when calling services running on different computers.

The available settings are Anonymous (the component is unaware of the caller's identity and can't access local or remote resources on the caller's behalf); Identify (the component can determine the caller's identity); Impersonate (the component impersonates the caller when accessing local resources, or even resources on a different computer if the caller resides on the same machine as the component); and Delegate. In the Delegate setting, the component impersonates the caller when accessing resources on the local computer as well as any remote server. This setting requires the Kerberos authentication services and that the Active Directory directory service be configured on both the client and the server machines.

Determine the Caller's Identity
When using the Identity setting, the called COM+ component can use the SecurityCallContext object to determine the caller's identity (the DirectCaller property) and whether the caller is in a given role (the IsCallerInRole method), but the component can't impersonate the caller when accessing a database or other resources, either on the same machine or on a remote server. Because the trusted subsystem is recommended in multitier architectures, impersonation and delegation are usually neither necessary nor desirable:

<Assembly: ApplicationAccessControl( _
   True, AccessChecksLevel:= _
   AccessChecksLevelOption. _
   ApplicationComponent, _ 
   Authentication:= _
   AuthenticationOption.Privacy, _
   ImpersonationLevel:= _
   ImpersonationLevelOption.Identify)>

You can add a class-level ComponentAccessControl attribute to enable security checks for that component, but this attribute is ignored if access checks are enabled only at the application level:

<ComponentAccessControl(True)> _ 
   Public Class MoneyMover 
Inherits ServicedComponent 
   ... 
End Class 

Next, add one or more assembly-level SecurityRole attributes that define all the user roles recognized by the application. Always include a SecurityRole attribute that adds the Everyone user to the Marshaler role if you plan to enable method-level security.

You can apply the SecurityRole attribute at the assembly, class, and method levels. When applied at the assembly level, the SecurityRole attribute defines which users can activate any component in the application, provided that you've applied an ApplicationAccessControl(true) attribute. When applied at the class level, it defines which users can call any method in that class, provided that you have applied a ComponentAccessControl(true) attribute to the class. You apply the SecurityRole attribute to individual methods only when you enable security at the method level.

The registration process adds all the roles that you specify in SecurityRole attributes in the COM+ catalog. By default, these roles contain no users, but you can add the Everyone user to a role by passing true in the second argument (which corresponds to the SetEveryoneAccess property). Users other than Everyone can be added to a role only by means of the COM+ explorer or by using an administrative script:

' Accountants can launch this
' application; all users are in 
' this role. 
<Assembly: SecurityRole( _
   "Accountants", True)> 

' Create the Managers role, but don't
' add any users to it. 
<Assembly: SecurityRole("Managers")> 

' Prepare the application for security 
' at the method level. 
<Assembly: SecurityRole("Marshaler", _
   True)> 

<SecurityRole("Readers", True, _
   Description:= _
   "Users who can read")> _ 
   Public Class MoneyMover 
   Inherits ServicedComponent 
   ... 
End Class 

You must take a handful of steps to enable RBS at the method level. First, ensure that you marked the assembly with an ApplicationAccessControl(true) attribute and the serviced component class with a ComponentAccessControl(true) attribute whose AccessChecksLevel property is set to ApplicationComponent.

Next, add an assembly-level SecurityRole attribute that adds the Everyone user to the Marshaler role, then mark the serviced component class with the SecureMethod attribute, and define the methods to be secured in a separate interface and have the serviced component class implement the interface.

Apply the SecurityRole Attribute
Then apply the SecurityRole attribute to methods in the class, specifying which role can call which method, or use the MMC snap-in to perform this step from the user interface (see Listing 1).

Use the SecurityCallContext object to perform programmatic security and always explicitly test that the IsSecurityEnabled property is true. Don't use the ContextUtil class to implement programmatic security. The SecurityCallContext object exposes a more complete set of security-related methods than the ContextUtil class. However, IsCallerInRole and other methods return true even if RBS is disabled; thus, you should always explicitly check that RBS is enabled by testing the IsSecurityEnabled property:

Dim scc As SecurityCallContext = _
   SecurityCallContext.CurrentCall 
If Not scc.IsSecurityEnabled Then 
   Throw New Exception("This method " _
   & "requires role-based security") 
ElseIf scc.IsCallerInRole( _
   "Managers") Then 
... 
End If 

Be sure that you run a server COM+ component under the identity of a least-privileged specific account. Never run the component under the interactive user's identity, and avoid using predefined system accounts. Running the component under the identity of the interactive user is OK only during the development phase because this setting enables you to display message boxes and other diagnostic messages. In real applications, you should always define an ad-hoc account that has only the privileges that the application strictly requires, such as access to certain directories and registry keys. This technique gives you more granular security than the one offered by predefined system accounts such as Local Service, Network Service, and Local System.

The client application should dispose of all serviced components that aren't JIT-activated as soon as it is finished with them. The recommended way to do so is by calling the component's Dispose method, rather than by means of the ServicedComponent.DisposeObject static method:

' *** OK 
ServicedComponent.DisposeObject(obj) 

' *** Better 
obj.Dispose() 

For performance reasons, a serviced component should never implement the Finalize method because such a method would be called through reflection. Instead, override the protected Dispose(disposed) method and place all finalization code there.

Also, never mark a public method of a serviced component with a WebMethod attribute to make it callable through the Web service infrastructure. Both serviced components and Web services offer remote clients the ability to call methods in a .NET component. However, these two remoting technologies might conflict with each other because of the way they enable the transaction and context flow from the client and the component. As a consequence, you should never mix the two techniques in the same component.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube