Integrating Web Services and COM Components

 

Keith Pijanowski
Microsoft Corporation

May 2004

Applies to:
   Microsoft® .NET Framework
   Microsoft Visual Basic® 6.0
   Microsoft Visual Studio® .NET
   C#

Summary: Eliminate organizational dependence on the SOAP Toolkit to expose COM components as Web services. See how to consume Web services written on any platform from legacy environments such as COM components and classic ASP pages. (24 printed pages)

Download the associated WebServCOMIntegration.exe code sample.

Contents

Overview
Getting Started
Exposing COM Components as .NET Web Services
Benefits of .NET Web Services Over the SOAP Toolkit
Infusing Web Services Into a COM Architecture
Conclusion
Related Articles and Resources

Overview

COM is not dead and support for Web services grows stronger every day. Unfortunately, COM as a technology was designed and built before Web services were conceived; consequently, there is no built-in support for calling a Web service from a COM object or exposing a COM class as a Web service. This leaves application architects that must extend their existing COM systems facing two critical scenarios:

  1. How to expose the functionality of existing COM components as a Web service.
  2. How to infuse an existing COM architecture with functionality that is available via both public and internal-facing Web services.

One option is to rewrite your COM component using a .NET-compatible language. This would allow you to easily expose your .NET class as Web services and consume existing Web services, since support for creating and consuming Web services is an inherent feature of the .NET Framework. If the COM component in question is not dependent on other COM components, then you may want to consider a rewrite, especially if your Web service requires maximum performance.

The other extreme is to pursue a completely native solution; in other words, a solution that utilizes the same COM-aware language and development platform that you are using today. The SOAP Toolkit 3.0 allows you to bolt Web service capabilities onto your COM server components. Using the SOAP Toolkit, COM components can be exposed as Web services without any modifications to the underlying binaries. There are several compelling reasons to use the SOAP Toolkit: you can use the existing skills of your development team; you do not have to modify your components in any way; and you do not have to worry about deploying the .NET Framework and its minimum requirements. Unfortunately, the SOAP Toolkit has reached its final revision with version 3.0 and will no longer be supported after April 2005. This means the SOAP Toolkit is not a good long-term solution, as it will not benefit from the evolving Web service standards such as WS-Security, WS-Transactions, and WS-Reliable Messaging that provide integrated security, transactional capabilities, and reliable communications, respectively. Furthermore, Web services created with the SOAP Toolkit 3.0 by default are not compliant with the Web Services Interoperability Organization's Basic Profile. (The role of this organization and how it works with standards bodies and industry will be discussed later.)

You may have even played around with COM 1.5's Web service switch that can take an entire COM component and produce a Web service that exposes every public entity within your component. Chances are your component was designed and built without any consideration for being consumed as a Web service. Perhaps your component requires state, has a chatty interface, or requires data types that are not conducive to cross-system communications. For these and many other reasons the COM 1.5 switch may not be ideal.

This article pursues an option that represents the middle ground between a complete rewrite to the .NET Framework and using the SOAP Toolkit to expose your COM components. If you are ready to take a baby step towards adopting the .NET Framework and the Common Language Runtime (CLR), then it is possible to preserve your COM investment by using the .NET Framework's COM interop capabilities and strategically incorporating thin .NET interop assemblies (explained later in this article) into existing COM architectures.

In addition to allowing managed code to consume COM components, COM Interop allows .NET classes to emulate a COM interface so that these classes can be consumed from a COM environment, such as Visual Basic 6.0 or classic ASP. Using this technique it is possible to impersonate and replace an existing COM component without recompiling the COM components that consume the replaced component. This article will also describe this technique.

Getting Started

We are going to use a function that determines the number of factors for a specified number. Remember from your high school mathematics class that a number is a factor of another number if it can be evenly divided into it. For example, 1, 2, and 4 are factors of 4; however, 3 is not a factor of 4. The following code is the Visual Basic 6.0 implementation of a Factorize function that returns the total number of factors for the specified parameter.

' Determine the number of factors for n.
Public Function Factorize(n As Long) As Long
    Dim lCount As Long
    lCount = 0

    ' Look for factors including 1 and the number itself.
    Dim i As Long
    For i = 1 To n
        If (n Mod i) = 0 Then
            lCount = lCount + 1
        End If
    Next
    
    Factorize = lCount
End Function

We are going to have a lot of fun with this function. We are going to package this function within a Visual Basic 6.0 project and expose a Visual Basic 6.0 COM implementation of this function as a .NET Web service using the .NET Framework's COM Interoperability capabilities. We will also implement this same function in C# as a fully managed assembly, expose it as an XML Web service and use COM Interop once again to allow this .NET Web service to be consumed from a COM-based client. Below is the C# implementation of our Factorize function:

   // Determine the number of factors for n.
   public int Factorize(int n)
   {
      int nCount = 0;
      // Look for factors other than 1 and the number itself.
      for (int i=1; i<=n; i++)
      {
         if (0 == (n % i))
         {
            nCount++;
         }
      }
      return nCount;
   }

Exposing COM Components as .NET Web Services

This section provides step-by-step guidance on calling COM components from .NET and then exposing those components as .NET Web services. The first thing I want to cover is the mechanics involved when exposing the functionality of a COM component as a Web service. This will require an understanding of the .NET Framework's COM Interop capabilities for server-side components. Once COM Interop is understood we will create an interop assembly and then build a Visual Studio .NET Web service project.

The procedures I am going to describe in this section are an alternative to using the Soap Toolkit to expose server-side COM components as Web services.

The Mechanics of .NET Calling COM

Figure 1 below illustrates the mechanics of consuming a COM component from a .NET Web service.

Figure 1. .NET Web service using an Interop Assembly to call a COM component

There are two important features of the architecture depicted in the component diagram in Figure 1. First, our COM component does not need to be altered in any way to participate in this architecture. Its interface is unmodified and it is registered on the Web server on which our .NET Web service resides. Second, the code we write to implement a .NET Web service consumes this COM component as if it were a fully managed .NET class. These two features are made possible by the COM Interop technology that is part of the .NET Framework's base class libraries. COM Interop allows us to easily generate an interop assembly for every COM component we wish to consume from .NET. An interop assembly is a thin .NET assembly which is primarily metadata used by the .NET Framework to marshal calls across the .NET/COM boundary. During development Visual Studio .NET uses this metadata to show the developer the interface of the underlying COM component via the object browser or IntelliSense®. At runtime the metadata within an interop assembly is used by the CLR to create a proxy to the COM environment known as a Runtime Callable Wrapper (RCW). It is this RCW that marshals calls for us across the .NET/COM boundary.

Figure 2 is essentially the same diagram as Figure 1; however, Figure 2 shows the assemblies and components that are used in the code download. In this figure, WSNumbers.dll is the .NET Web service assembly, NumLib.dll is a COM component, and Interop.NumLib.dll is an interop assembly used by the CLR to proxy calls from WSNumbers.dll to NumLib.dll.

Figure 2. WSNumbers Web service using an Interop assembly to call into the NumLib.dll COM component

Generating Interop Assemblies

The easiest way to create an interop assembly is to use the COM tab in the Add Reference dialog in Visual Studio .NET. However, if you are working in a team environment, you do not want every developer to create an interop assembly for a given COM component. Instead you will want to create a single interop assembly for each COM component you need to consume from .NET. Once created, this single interop assembly can be placed under source code control and shared among all team members. The Type Library Importer command line tool generates these interop assemblies. The example shown below will generate an interop assembly named "Interop.NumLib.dll" from the NumLib.dll COM component. Interop.NumLib.dll can than be referenced like any other private .NET assembly.

tlbimp  NumLib.dll  /out:Interop.NumLib.dll /namespace:NumLib /asmversion:1.0.0.1 /verbose

As you can see, the command above allows for more control over the process of generating an interop assembly than the Add Reference | COM tab of Visual Studio .NET. The switches used above allow for the specification of the name of the interop assembly (Interop.NumLib.dll), the namespace in which all the runtime callable wrappers will be placed (NumLib), and the version number in major.minor.build.revision format of the interop assembly (1.0.0.1). There are a lot of other switches that can be used with this command-line tool. For additional options regarding the creation of interop assemblies, see Type Library Importer in the .NET Developer's Guide. For recommended best practices on managing interop assemblies in a team environment, check out Chapter 4, "Managing Dependencies," of Team Development with Visual Studio .NET and Visual SourceSafe. This chapter contains best practices for referencing COM components and managing outer system assemblies (assemblies that are not rebuilt by your build process).

As a final note on interop assemblies, if the COM component you need to interoperate with is a third-party component that you purchased, then you should ask your vendor for an interop assembly. Interop assemblies supported by third parties are known as Primary Interop Assemblies.

Creating Visual Studio .NET Web Service Projects

Now that we have a way to call into our COM component from managed code we need a Web service that will host our interop assembly and invoke the functions we wish to expose via Web methods. Unlike previous development environments like Visual Basic 6.0 and Visual C++® 6.0, Visual Studio .NET and the .NET Framework were built with strong support for XML Web services. If you are new to Visual Studio .NET and the .NET Framework, then creating a new Web service that references the interop assembly created above is as simple as following the procedural steps described below:

  1. Open Visual Studio .NET 2003 and on the File menu, choose New Project ...
  2. In the new project dialog, choose the language of your choice. (The code download uses C#.)
  3. In the new project dialog, choose ASP.NET Web Service as the project type.
  4. Specify "WSNumbers" as the project name.
  5. Rename the "Service1" Web service to "Primes". (Rename both the class name and the file name to "Primes" to avoid confusion.)
  6. Add a reference to the interop assembly created above. Do not use the COM tab. The interop assembly created in the previous section is a private .NET assembly, so you will need to browse for it from the .NET tab.

The procedural steps above give us a .NET Web service project with an empty class named Primes that inherits from System.Web.Services.WebService. This class represents our Web service and is the class in which we will write functions (or Web methods) that consume our COM component via the interop assembly created in the previous section and referenced in step 6 above.

Once we have our Web service project set up, all we need to do is add the code below to create a Web method that utilizes the Visual Basic 6.0 Factorize function.

   [WebMethod]
   public int Factorize(int nNum)
   {
      // Simple use of a RCW.
      // Note:  The default behavior for VB 6.0 is to pass arguments by 
      // reference.  Hence the need for the 'ref' keyword below.
      Primes p = new Primes();
      return p.Factorize(ref nNum);
   }

Words of Caution

There are a couple of issues to keep in mind when architecting systems that contain both COM components and .NET assemblies. The cost of COM interop and the differences in threading models between .NET and COM can potentially cause performance degradation. Both of these issues are discussed in the following two sections and potentially could be mitigated by using ASP.NET caching capabilities (also described in a later section).

The Cost of COM Interop

In the listing above that shows the Factorize function, it is important to note that the Primes class that is being instantiated in the Factorize function is not the Visual Basic 6.0 Primes class in our NumLib COM component. This class represents the Runtime Callable Wrapper for our Visual Basic 6.0 Primes class. Every call into this class whether it is a property set, property get, or function call will result in a trip across the .NET/COM boundary. This trip across the .NET/COM boundary is a fixed cost with respect to performance. The approximate overhead for a COM interop call is about 50 machine instructions (on an x86 processor) as compared to a call from one .NET class to another .NET class. Consequently, if we measure the cost of COM Interop as a percentage of the total cost of a function call, then functions which do a lot of work per call will pay a lower cost than functions which do very little work per function call. If your COM architecture made heavy use of properties that need to be set before a particular function is called or if the parameters to your COM functions are complicated COM objects, then you may want to consider creating a COM shim component that accepts raw data types and packages the data appropriately for a call to the underlying COM component. Raw data types travel across the COM/.NET boundary a lot more efficiently than COM objects which require their own RCW to make the trip.

Threading Models

The next issue that requires investigation involves the different threading models of the SOAP Toolkit (classic ASP), .NET Web services, and Visual Basic 6.0 COM components. Visual Basic 6.0 COM components are "apartment model," which means that they must be executed in a Single-Threaded Apartment (STA). Classic ASP pages run in a Single-Threaded Apartment; consequently, when a Visual Basic 6.0 class is instantiated from classic ASP, the threading models match and the object can be run on the same thread in which the request came in on. On the other hand, .NET Web services run in the Multi-Threaded apartment (MTA). This means that every time a request comes in for a .NET Web service, the .NET class that represents the requested Web service is created in the Multi-Threaded apartment. Now, when the RCW for an apartment model COM component is instantiated, it is created in a Single-Threaded Apartment, and every time the RCW is called from the Web service a thread hop occurs. In other words, the Web service's MTA thread has to communicate with the RCW's STA thread via a message pump.

If you are ASP.NET savvy, you know that ASP.NET Web Forms have a page-level directive (shown below) that allows the page to run in the STA.

<%@ Page Language="cs" AutoEventWireup="false" Codebehind="MyPage.aspx.cs" Inherits="MyNamespace.MyPage" ASPCompat="true" %>

This is useful when your Web Form's code behind file uses COM Interop to talk to a COM component that is apartment model. ASP.NET Web services have no equivalent directive or attribute at this time.

The bottom line is that due to this thread hop you may experience degraded performance as compared to the same COM component running under the SOAP Toolkit. If this degradation is a concern, make sure you read the section below on ASP.NET caching capabilities. Also keep in mind that oftentimes there is a big difference between "as fast as possible" and "fast enough". If your existing Web service is not dealing with incredibly high volumes of requests, then you should not lose sleep over the degradation described above. If you do need an extremely high-performance Web service, then you should consider rewriting your Web service completely in managed code.

Benefits of .NET Web Services Over the SOAP Toolkit

The benefits of hosting your COM components within ASP.NET far outnumber the scenarios you have to be cautious about. Many of the benefits listed below you simply get for free due to the fact that you are now working within an environment that fully supports Web services.

If the cost of COM Interop or inefficient threading models cause an unacceptable performance hit to your Web service, then consider using ASP.NET caching. ASP.NET provides capabilities for caching both SOAP messages and objects. These techniques are described below and could more than make up for the performance hit of COM Interop and incompatible threading models.

Interface and Object Control

The first benefit to note is that we have full control over how our COM object is instantiated and invoked. It will be your code that publishes the interface to your Web service (via a WSDL file) and creates and consumes the underlying COM component. The SOAP Toolkit makes it difficult to modify a Web service's WSDL file and it limits what can be done between the time that the request comes in off the wire and your objects are created. For example, if your COM component is not architected for a stateless environment, you may need to set one or more properties before calling the desired function. With the Web service that we just implemented, this is easy to account for. First add the necessary parameters to your Web method. Within your WebMethod, set the parameter values into the appropriate properties of your RCW, and then call the desired function on the RCW. Visual Studio .NET will automatically modify your WSDL file to account for the modified parameter signature.

One Tool One Platform

Another advantage is that you will no longer need to deal with plumbing code and the intricacies of plumbing components such as the SOAP Toolkit ISAPI filter and the ASP listener. All the features of the ISAPI filter and the ASP Listener are now a part of the same development platform in which you will implement your business logic.

Increasing Performance with the Caching Capabilities in ASP.NET

One way to give a Web service a performance boost is to use ASP.NET caching capabilities. ASP.NET provides two techniques for caching the results of a Web method in a Web server's memory. The code below shows how to use the WebMethod attribute to cache the SOAP response of a Web method in a Web server's memory. Every time this WebMethod is called, ASP.NET will first look in its cache for a SOAP response that matches the request. If the SOAP response is found, then ASP.NET streams the cached results back to the caller without invoking the WebMethod. If your WebMethod takes parameters, then the cache will be keyed according to the values of your parameters. In this example, the SOAP response is cached for 60 seconds. After 60 seconds the cache expires and the next request will cause your code to execute and a new SOAP response stream will be placed in the cache for the WebMethod.

[WebMethod(CacheDuration=60)]
public DataSet getDataCacheSOAP()
{
   DataSet ds;
   // Connect to a database and fill the DataSet.
   return ds;
}

The other option is to use the API Cache. The API Cache does not cache SOAP responses; rather, the underlying objects of your choice are put into the Cache so that they can be reused during subsequent requests. In the example below, the code first tries to find the desired DataSet in the Cache. If the DataSet in question is not found, then the data access code is executed and the DataSet is placed in the Cache so that subsequent callers will not need to invoke the data access code. The advantage with this technique is that the cached objects can be consumed from any WebMethod and not just the WebMethod that initially cached the object. Additionally, there are more options for configuring the lifetime of the cached object when the API Cache is used. These options include an absolute timeout and a sliding expiration. An absolute timeout provides a mechanism to have your cached object removed from the cache at a specified time of day. On the other hand, a sliding expiration tells ASP.NET to remove the object from the cache if it is not utilized within the specified amount of time. The example below inserts a DataSet into the cache with the absolute expiration set to the maximum value that can be stored in a DateTime variable; however, if the DataSet is not used for a period of 1000 seconds, then it will be removed from the Cache.

[WebMethod]
public DataSet getDataCacheObject()
{
// Try to get the DataSet from the cache.
   DataSet ds;
   ds = (DataSet) Cache.Get("MyDS");

   if (ds == null) 
   {
      // DataSet was not found in the Cache.
      // Connect to a database and fill the DataSet.

      // Place DataSet into the Cache.
      Cache.Insert("PubsDS", ds, null, DateTime.MaxValue,
TimeSpan.FromSeconds(1000), 
System.Web.Caching.CacheItemPriority.High, null);
   }
   return ds;
}

Web Service Enhancements 2.0

Web Services Enhancements 2.0 is available today as a technology preview and will soon be a supported product. With WSE 2.0, developers have a fully managed interface for creating and consuming Web services that are compliant with recently released Web service specifications, such as WS-Security, WS-Policy, WS-SecurityPolicy, WS-Trust, WS-SecureConversation and WS-Addressing. The SOAP Toolkit does not support these standards. The easiest way to create Web services that utilize these protocols is to migrate your Web service façades from the SOAP Toolkit to the .NET Framework and WSE.

Compliance with the WS-I Basic Profile

A final benefit which is not readily apparent but is very important when considering whether to migrate from the SOAP Toolkit to the .NET Framework is that Web services created with the .NET Framework will by default use "Document"-based SOAP messages with the "Literal" parameter formatting style otherwise known as Document/Literal. This is in direct contrast to the SOAP Toolkit which by default uses "RPC" SOAP messages with the Encoded parameter formatting style. This is important because to be compliant with the Web Services Interoperability Organization's Basic Profile, your Web services must support Document/Literal SOAP messages.

The WS-I is not a standards body; rather, it sits between standards bodies (such as the W3C, OASIS, and the IETF) and organizations building systems with Web services. Its mission is to produce best practices that insure interoperability. This organization's first deliverable was the "Basic Profile" which addresses issues with messaging (SOAP), description (WSDL), discovery (UDDI), XML schema, and XML 1.0. The "Basic Profile" is more than a written document; it also comes with test tools, use cases, usage scenarios, and a sample application. For more information on this organization, check out the WS-I Web site at www.ws-i.org, as well as Building Interoperable Web Services: WS-I Basic Profile 1.0.

As an aside, the SOAP Toolkit 3.0 is capable of supporting Document/Literal SOAP messages. However, to get this support, you need to manually edit your WSDL file after it is generated from the WSDL generator in the SOAP Toolkit.

Infusing Web Services Into a COM Architecture

It is time to change gears and look at how we can consume a Web service from a classic environment, i.e., an ASP page or a COM component. The techniques discussed in this section can be used to replace your dependency on the SOAP Toolkit's client-side tool, MSSOAP.SoapClient30, which is currently in use to allow COM clients to access Web services. MSSOAP.SoapClient30 takes a late-bound approach to calling Web services; consequently, there is no way for your compiler to tell if you are calling a Web service correctly. The technique shown below will produce a result that allows for early binding to a Web service.

Perhaps the system that you are responsible for supporting and extending makes use of classic ASP Pages that call into COM components, and your ASP pages cannot be migrated to the .NET Framework anytime soon. Furthermore let's say that your organization's current plans call for a point release in which a lot of the functionality and data being requested is already implemented as either an internal Web service, a Web service exposed by a business partner, or some other Web service published to a UDDI directory.

If this is your situation, it is more pragmatic to migrate to the .NET Framework one component at a time from the bottom up. In other words, convert your middle tier first and then convert your user interface.

The Mechanics of COM Calling .NET

Figure 3 below shows a COM-based user interface consuming a .NET class via a COM Callable Wrapper (CCW). The .NET class can then invoke a Web service on behalf of the COM environment using all the tools available within Visual Studio .NET and the .NET Framework.

Figure 3. COM Client using a CCW to call a .NET class that in turn calls a Web service

COM Callable Wrappers allow .NET classes to have a COM interface so that they can be called from both late-bound environments (classic ASP) and early bound environments (other COM components). COM Callable Wrappers are similar to Runtime Callable Wrappers in that they are generated at runtime from metadata within a .NET assembly. However, COM Callable Wrappers do not require a special interop assembly to house the additional metadata needed to make the hop from the COM environment to the .NET environment. All the information needed to create a CCW at runtime is contained in the assembly that will be called from COM.

.NET Attribution and COM Callable Wrappers

Figure 4 shows the type library of our Visual Basic 6.0 NumLib COM component. Our goal is to replace the NumLib COM component with the class in Code Block 1, in such a way that any COM consumer of this component (late bound or early bound) will not know that this replacement has taken place. In other words, the COM consumers will not have to be recompiled. In order to pull this off, all the information in the type library of Figure 4 must somehow make its way into the metadata of the assembly that will be replacing the NumLib COM component.

Figure 4. Type Library for the NumLib COM Component

// Generated .IDL file (by the OLE/COM Object Viewer)
// 
// typelib filename: NumLib.dll

[
  uuid(D496563C-8E5A-4920-B384-241C8B0F3896),
  version(5.1)
]
library NumLib
{
    // TLib :     // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
    importlib("stdole2.tlb");

    // Forward declare all types defined in this typelib
    interface _Primes;

    [
      odl,
      uuid(E5014B85-FCB2-4F0D-95EC-F740395A1952),
      version(1.1),
      hidden,
      dual,
      nonextensible,
      oleautomation
    ]
    interface _Primes : IDispatch {
        [id(0x60030004)]
        HRESULT IsPrime(
                        [in, out] long* n, 
                        [out, retval] VARIANT_BOOL* );
        [id(0x60030005)]
        HRESULT Factorize(
                        [in, out] long* n, 
                        [out, retval] long* );
        [id(0x60030006)]
        HRESULT getApartment([out, retval] BSTR* );
        [id(0x60030007)]
        HRESULT getPlatform([out, retval] BSTR* );
    };

    [
      uuid(67499A8D-A06C-464E-9755-27C394780FA2),
      version(1.1)
    ]
    coclass Primes {
        [default] interface _Primes;
    };

};

Code Block 1. Primes class that will replace the Numlib.Primes COM component

   public class Primes
   {
      public Primes()
      {
      }

      public bool IsPrime(ref int n)
      {
         // Call any web service here.
      }

      public int Factorize(ref int n)
      {
         // Call any web service here.
      }

      public string getApartment()
      {
         switch(Thread.CurrentThread.ApartmentState)
         {
            case ApartmentState.STA:
               return "Single Threaded Apartment";
            case ApartmentState.MTA:
               return "Multithreaded Apartment";
            case ApartmentState.Unknown:
               return "ApartmentState property as not been set";
            default:
               return "Error determining Apartment State";
         }      
      }

      public string getPlatform()
      {
         return "NET Framework " + System.Environment.Version.ToString();
      }

   }

Several attributes from System.Runtime.InteropServices namespace must be correctly incorporated into the code shown in Code Block 1 in order to include enough information into the metadata of your assembly to give the CLR enough information to present a COM interface. Once this is done your assembly can be registered in the registry. The necessary tasks are listed below:

  1. Strong name the assembly.
  2. Assign a Prog ID.
  3. Assign a Type Library ID.
  4. Create a default interface for the COM impersonating .NET class.
  5. Assign a Class ID to the COM impersonating .NET class.
  6. Assign a Dispatch ID to all public methods and functions.
  7. Register your assembly in the registry.

Strong Names

Strong names are unique identifiers that protect the identity and integrity of an assembly. RegAsm, a command line tool for registering .NET assemblies so that they may be consumed by COM components, requires assemblies to be strongly named. (This tool will be discussed in a later section.) To give an assembly a strong name, you must use the sn.exe command line tool. The command below will generate a key file that can be used to strong name an assembly.

   sn –k MyKeyFile.snk

Once you have a key file, you associate it with an assembly using the AssemblyKeyFile attribute as shown below:

   [assembly: AssemblyKeyFile("..\\..\\mykeyfile.snk")]

This attribute is an assembly-level attribute usually placed in your projects AssemblyInfo file with all other assembly-level attributes. Notice that the path of the specified key file is a relative path with respect to the location of the assembly. The example above assumes that the key file is located in the project folder, which is two folders above the directory used by the compiler. Once you have associated your assembly with a strong name key file and compiled your project, your assembly is strongly named.

Prog IDs

A COM component's ProgID is a two-part, dot-delimited name which usually indicates the component name and the class name. The ProgID attribute shown below can be used to assign the ProgID of our COM class to a .NET class that will eventually replace the COM class.

   [ProgId("NumLib.Primes")]

This attribute is a class-level attribute and its constructor takes a string which will serve as the ProgID. If a ProgID is not explicitly declared by using this attribute, then the RegAsm tool will assign a ProgID based on the namespace and class name of the .NET class.

Type Library IDs

Every type library in COM needs to be uniquely identified via a Globally Unique Identifier (GUID). Looking at the first section of the type library in Figure 4, you can see IDL syntax which specifies the follow GUID as the type library ID of NumLib.dll: D496563C-8E5A-4920-B384-241C8B0F3896. Using the Guid attribute scoped to the entire assembly, as shown below, will inform RegAsm to use the specified GUID as the Type Library ID.

[assembly:  Guid("D496563C-8E5A-4920-B384-241C8B0F3896")]

If a type library ID is not specified, then RegAsm will randomly generate a GUID and assign it as the type library ID. This could cause problems for early bound consumers of the COM component when you attempt to replace it with your .NET assembly.

Class IDs

Every .NET class that needs a COM interface needs a class ID. The Guid attribute used at the class level allows you to declaratively specify a class ID. To correctly replace the Primes class in the NumLib COM component, we cannot use any old GUID. We must use the GUID that is specified as the Class ID in the type library of Figure 4. The code snippet below shows the Guid attribute specify the Class ID for the NumLib.Primes component.

   [ProgId("NumLib.Primes")]
   [Guid("67499A8D-A06C-464E-9755-27C394780FA2")]
   public class Primes
   {
      ...
   }

Default Interfaces

If you are not familiar with the internal machinations of COM, then things are about to get a little weird. Interactions with COM components are done via interfaces that are immutable in COM. With respect to the goal we are pursuing here, the bottom line is that we need to specify an interface that matches the interface of our Primes COM class. Then, our .NET Primes class will need to implement this interface. Finally, .NET attribution will be used to make sure this interface is the default interface to our .NET Primes class when the CCW is generated at runtime.

Reviewing the NumLib.dll type library, we can see that the default interface for our Primes class is "_Primes" and it has an interface ID of E5014B85-FCB2-4F0D-95EC-F740395A1952. This means we need to add an interface description to the source code of our .NET project and once again the Guid attribute will be used to specify the correct GUID as the unique identifier of the _Primes interface. Once all of this is done we need to tell RegAsm not to automatically generate an interface for our Primes class; rather, we want RegAsm to use the first interface it finds implemented by the Primes class, i.e., _Primes. Passing the ClassInterfaceType.None parameter to the ClassInterface attribute does this.

The code below shows the _Primes interface with the correct interface ID. It also shows the Primes class implementing this interface. Finally, the ClassInterface attribute is used to ensure that RegAsm does not automatically generate a class interface.

   [Guid("E5014B85-FCB2-4F0D-95EC-F740395A1952")]
   public interface _Primes
   {
      bool IsPrime(ref int n);
      int Factorize(ref int n);
      string getApartment();
      string getPlatform();
   }

   [ProgId("NumLib.Primes")]
   [ClassInterface(ClassInterfaceType.None)]
   [Guid("67499A8D-A06C-464E-9755-27C394780FA2")]
   public class Primes : _Primes
   {
      ...
   }

It is very important to understand why we have to go through the trouble of specifying this interface and having our Primes class implement it. Remember we are replacing an existing COM component that has early bound capabilities. This component had a specific interface with a specific interface ID. The only way we can specify the exact values of the replaced COM component is to use the ClassInterface attribute to insure that a class interface is not automatically generated and to specify the class interface ourselves so that we can use .NET attribution to assign it the correct interface ID. If this interface is not set up correctly, then when early bound consumers try to instantiate the NumLib.Primes component, they will receive error 430, "Class does not support automation or does not support expected interface."

Dispatch IDs

Dispatch IDs uniquely identify methods, fields, and properties. (Believe it or not, dispatch IDs are not GUIDs—they are ordinary integers.) The only time it is necessary to specify a Dispatch ID is when you are replacing an existing component and it has clients that have cached the Dispatch IDs from the component during compilation. While specifying Dispatch IDs is not always necessary, you should always make sure that the physical placement of your functions, fields, and properties within the interface is in ascending order by Dispatch ID. Otherwise, early bound environments may call the wrong function, field, or property at runtime. The code below shows our _Primes interface with Dispatch IDs specified for the four methods in this interface. Note that this attribute only takes decimal-based integers and the Dispatch IDs shown in the NumLib type library are in hexadecimal making a conversion from Hex to decimal necessary.

   [Guid("E5014B85-FCB2-4F0D-95EC-F740395A1952")]
   public interface _Primes
   {
      [DispId(1610809348)]  // In HEX this is 0x60030004
      bool IsPrime(ref int n);
      [DispId(1610809349)]  // In HEX this is 0x60030005
      int Factorize(ref int n);
      [DispId(1610809350)]  // In HEX this is 0x60030006
      string getApartment();
      [DispId(1610809351)]  // In HEX this is 0x60030007
      string getPlatform();
   }

Assembly Registration

Code Block 2 shows the completed and fully attributed Primes class along with its default interface. Code Block 3 shows the attributes that were placed in the AssemblyInfo file to facilitate the correct registration of our assembly.

Code Block 2. Primes class ready for consumption by COM

using System;
using System.Runtime.InteropServices;

namespace SomeNamespace
{
   [Guid("E5014B85-FCB2-4F0D-95EC-F740395A1952")]
   public interface _Primes
   {
      [DispId(1610809348)]  // In HEX this is 0x60030004
      bool IsPrime(ref int n);
      [DispId(1610809349)]  // In HEX this is 0x60030005
      int Factorize(ref int n);
      [DispId(1610809350)]  // In HEX this is 0x60030006
      string getApartment();
      [DispId(1610809351)]  // In HEX this is 0x60030007
      string getPlatform();
   }

   [ProgId("NumLib.Primes")]
   [ClassInterface(ClassInterfaceType.None)]
   [Guid("67499A8D-A06C-464E-9755-27C394780FA2")]
   public class Primes : _Primes
   {
      public Primes()
      {
      }

      public bool IsPrime(ref int n)
      {
         // Call any web service here.
      }

      public int Factorize(ref int n)
      {
         // Call any web service here.
      }

      public string getApartment()
      {
         switch(Thread.CurrentThread.ApartmentState)
         {
            case ApartmentState.STA:
               return "Single Threaded Apartment";
            case ApartmentState.MTA:
               return "Multithreaded Apartment";
            case ApartmentState.Unknown:
               return "ApartmentState property as not been set";
            default:
               return "Error determining Apartment State";
         }      
      }

      public string getPlatform()
      {
         return "NET Framework " + System.Environment.Version.ToString();
      }

   }
}

Code Block 3. Assembly level attributes located in the AssemblyInfo.cs file

[assembly: AssemblyVersion("1.2.3.4")]
[assembly: TypeLibVersion(5,0)]
[assembly: AssemblyKeyFile("..\\..\\MyKeyFile.snk")]
[assembly:  Guid("D496563C-8E5A-4920-B384-241C8B0F3896")]

The final task that must be completed is registering the assembly in the registry. The command-line tool, RegAsm (short for register assembly), will take all the metadata that we have included in our assembly for the purposes of COM Interop (specifically, COM calling into our assembly) and set up the correct registry entries. Once this is done our assembly appears to be a COM component to other COM components. The command below will register the wsproxies.dll assembly. Figure 5 shows the InprocServer32 key for WSProxies.dll.

regasm wsproxies.dll /codebase /tlb:wsproxies.tlb

Figure 5. InProcServer32 key for the WSProxies.dll

There are three facts to note about the command above. First the /codebase option creates a "CodeBase" entry in the registry under the InprocServer32 key, which specifies the location of your assembly. This option is only necessary if your assembly is not installed in the Global Assembly Cache (GAC). If your assembly is not installed in the GAC and you forget to specify this option, your COM clients will receive the following runtime error: "File or assembly name WSProxies, or one of its dependencies, was not found."

Figure 6. Error resulting from assembly not installed in GAC and /codebase option unspecified

Second, all assemblies that you specify with the /codebase option must be strong-named assemblies. This is why we had to assign a key file to our assembly in step 1.

The third fact is the most important. If you are replacing a COM component that is used from an early bound environment you MUST use the /tlb option. This option generates a type library (wsproxies.tlb in the example above) and registers that type library in the registry. Without this switch there is not enough information in the registry for early binding. Specifically, interface information is omitted. (Remember the work we had to do in order to set up the _Primes interface as the default interface.) If there is a problem with your default interface, then you will receive error 430: "Class does not support automation or does not support expected interface."

Figure 7. Error resulting from problem with default interface

If you receive this error, then double-check the GUID on your interface ID and make sure that you use the /tlb switch while registering your assembly. If you call this assembly from a classic ASP environment, you may receive either a System.UnauthorizedAccessException or a System.IO.FileNotFoundException when calling one of the functions that calls a Web service. If this occurs, give both the IUSR_{machine} and the IWAM_{machine} accounts full access to c:\windows\temp.

RegAsm also has an unregister switch. The command below will unregister an assembly from the registry.

regasm wsproxies.dll /codebase /tlb:wsproxies.tlb /unregister

Performance Considerations

The cost of a trip across the COM/.NET border is roughly the same regardless of the direction being traveled. Consequently, when calling a .NET object from a COM client using the techniques described above, the cost is roughly 50 machine instructions per call (on an x86 processor).

With respect to threading, take a look at Figure 5 one more time. The threading model is set to "Both"; this means that our .NET assembly when called from COM will be assigned to the apartment of the caller and we can be assured that calls into our assembly will be as efficient as possible. All .NET assemblies registered with RegAsm will have a threading model of "Both". This means that we do not have to worry about thread hops caused by two dependent components that reside in different apartments. Even if the COM client is a Visual Basic 6.0 component that is trapped in an STA, we will not have a problem because the "Both" threading models allows for our .NET assembly to be placed in the caller's STA.

Conclusion

Full support for the SOAP Toolkit will end in April 2005. Web services that rely on the SOAP Toolkit to expose the functionality of COM components have two options available to them. The first option is a complete rewrite of all code to a .NET Framework-compatible language. If performance is a concern, then this option is recommended because SOAP Toolkit Web services rewritten in a .NET language enjoy a significant performance boost. The other option is to use the .NET Framework to build a thin Web service façade and use COM Interop to keep your underlying COM components intact. This option may produce degraded performance if your underlying COM components are apartment model and you are unable to take advantage of the ASP.NET caching capabilities. However, this option represents the least amount of development time. Regardless of the option you choose, the maintainability and extendibility of your system will be greatly enhanced.

With respect to COM-based client components that need to consume Web services, the .NET Framework COM Interop technologies can also be used to replace any dependencies on MSSOAPLib.SoapClient. Using this flavor of COM Interop, COM interfaces can be assigned to a .NET class allowing the .NET class to be consumed from any COM environment. These .NET classes can then act as Web service proxies on behalf of their COM clients.

Beyond (COM) Add Reference: Has Anyone Seen the Bridge?

Building Interoperable Web Services: WS-I Basic Profile 1.0

INFO: XML Web Services and Apartment Objects

Migrating Native Code to the .NET CLR

An Overview of Managed/Unmanaged Code Interoperability

Soap Toolkit Information on the MSDN Web Services Developer Center

Team Development with Visual Studio .NET and Visual SourceSafe

MSDN Web Services Developer Center

WS-I Announcement of General Availability of the Basic Profile 1.0

.NET Web Services: Architecture and Implementation

Real World XML Web Services: For VB and VB .Net Developers

Com and .NET Interoperability