Classic COM to .NET Interoperability
Building a Classic COM Server using C# and .NET

Gopalan Suresh Raj

Note
To work with any of these samples, you will need the following:
.........................................Microsoft .NET SDK
.........................................Microsoft Visual Studio.NET Beta 2 or higher

 

To ease development, I recommend using the Visual Studio.NET IDE. However, you are free to develop your application in the favorite editor of your choice, using the command-line to execute the various commands to build and deploy it.

The various steps that are involved in creating a COM Server using C# and the .NET Framework are as follows (I'm going to assume you're using the VS.NET IDE):

  1. Create a Visual C# - Class Library project

  2. Generate a Key-Value pair to use when deploying your Shared Assembly

  3. Configure your Project Property Pages with the right information

  4. Develop the QuoteServer.cs library

  5. Modify the generated AssemblyInfo.cs to add the right assembly information

  6. Build the Project Files

  7. Deploy the component as a Shared Assembly

 

1. Create a Visual C# - Class Library project

Create a new Visual C# Class Library project.

Wait for the picture to load

 

2. Generate a Key-Value pair to use when deploying your Shared Assembly

Shared Assemblies are those that can be used by any client application, such as a system DLL that every process in the system can use. Unlike private-assemblies, shared assemblies must be published or registered in the system's Global Assembly Cache (GAC). As soon as they are registered in the GAC, they act as system components. An essential requirement for GAC registration is that the component must possess originator and version information. In addition to other metadata information, these two items allow multiple versions of the same component to be registered and executed on the same machine. Unlike Classic COM, we don't have to store any information in the system registry for clients to use these shared assemblies.

There are three general steps to registering shared assemblies in the GAC:

  1. The Shared Name (sb.exe) utility should be used to obtain the public/private key pair. This utility generates a random key pair value, and stores it in an output file - for example, QuoteServer.key.

  2. Build the assembly with an assembly version number and the key information in the QuoteServer.key

  3. Using the .NET Global Assembly Cache (gacutil.exe) utility, register the assembly in the GAC.

The assembly now becomes a shared assembly and can be used by any client in the system.

Therefore, as a first step, use the Shared Name Utility to obtain a public/private key pair and store it in a file (QuoteServer.key, in this case) as shown below.

Command Prompt
C:\MyProjects\Cornucopia\ClassicCOM\QuoteServer>sn -k QuoteServer.key

Microsoft (R) .NET Framework Strong Name Utility Version 1.0.2914.16
Copyright (C) Microsoft Corp. 1998-2001. All rights reserved.

Key pair written to QuoteServer.key

C:\MyProjects\Cornucopia\ClassicCOM\QuoteServer>

The -k option generates the random key pair and saves the key information in the QuoteServer.key file. We use this file as input when we build our Shared Assemblies.

 

3.  Configure your Project Property Pages with the right information

Configure the Project Properties with the right information. Specifically, move to the General tab, and in the Wrapper Assembly Key File area, enter the key file to use. In this case, it is QuoteServer.key.

Wait for the picture to load

Move to the Configuration Properties area, and into the Build properties, and in the "Register for COM Interop" area, choose the "True" list item. This makes sure that as part of building the assembly, the IDE will also register the .NET Component and prepare it for COM Interoperability.

Wait for the picture to load

 

4. Develop the QuoteServer.cs library

To illustrate a COM Client communicating with managed code, create a simple C# Class library that defines a single class called QuoteServer which supports a method called getQuote(). Also make sure the QuoteServer implements an interface called IQuoteServer. The logic behind the class is very simple; however, notice the use of the InterfaceTypeAttribute on line 33, and the ClassInterface attribute on line 60. As specified by both the InterfaceTypeAttribute  and the ClassInterface attributes, the [default] has been configured as [dual]. When you expect your .NET component to be used by classic COM clients, you can make direct use of these attributes in your managed code. This allows you to gain more control over the generated COM type information. Notice that you are using various attributes to control the generated GUID of the types as well as the underlying definition if the IQuoteServer interface and the getQuote() method.

QuoteServer.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
115:
116:
117:
118:
119:
120:
121:
122:
123:
124:
125:
126:
//////////////////////////////////////////////////////
/// The following example illustrates a Classic COM Type
/// developed using C# and the .NET Framework.
///
/// author: Gopalan Suresh Raj
/// Copyright (c), 2001. All Rights Reserved.
/// URL: http://gsraj.tripod.com/
/// email: gopalan@gmx.net

///
/// Here's how to build the assemply manually
/// <compile>
/// csc /t:library /out:QuoteServer.dll QuoteServer.cs
/// </compile>
///
/// Here's how to generate the Type Library and
/// Register the .NET Types manually
/// <register>
/// regasm QuoteServer.dll /tlb:QuoteServer.tlb OR
/// tlbexp QuoteServer.dll /out:QuoteServer.tlb
/// </register>
//////////////////////////////////////////////////////

namespace SimpleStocks {

  using System;
  using System.Runtime.InteropServices;

  //////////////////////////////////////////////////////
  /// <summary>IQuoteServer Interface</summary>
  /// <remarks>
  /// This interface allows for getting Stock Quotes
  /// </remarks>
  //////////////////////////////////////////////////////

  [ InterfaceTypeAttribute( ComInterfaceType.InterfaceIsDual ) ]
  public interface IQuoteServer {

    //////////////////////////////////////////////////////
    /// <summary>The getQuote method</summary>
    /// <remarks>
    /// Given a Stock Symbol, this method returns
    /// the Stock Quote for that symbol.
    /// </remarks>
    /// <param name="symbol">
    /// The String representing the Symbol for which the
    /// Stock Quote is requested.
    /// </param>
    /// <returns>
    /// The Price of the Stock corresponding to the symbol
    /// </returns>
    //////////////////////////////////////////////////////

    [ DispId( 1000 ) ] float getQuote( String symbol );
  }

  //////////////////////////////////////////////////////
  /// <summary>QuoteServer Class</summary>
  /// <remarks>
  ///  This Class implements the IQuoteServer interface
  ///  that allows for getting Stock Quotes
  /// </remarks>
  //////////////////////////////////////////////////////

  [ ClassInterface( ClassInterfaceType.AutoDual ) ]
  public class QuoteServer : IQuoteServer {

    /// <summary>
    /// Public Default Constructor
    /// </summary>

    public QuoteServer () {
    }

    //////////////////////////////////////////////////////
    /// <summary>getQuote Method</summary>
    /// <remarks>
    /// Given a Stock Symbol, this method returns
    /// the Stock Quote for that symbol.
    /// </remarks>
    /// <param name="symbol">
    /// The String representing the Symbol for which the
    /// Stock Quote is requested.
    /// </param>
    /// <returns>
    /// The Price of the Stock corresponding to the symbol
    /// </returns>
    /// <exception cref="ArgumentNullException"></exception>
    //////////////////////////////////////////////////////

    public float getQuote ( String symbol ) {
      if (null == symbol) {
        // Intercepted as a COM Error Object
        throw new ArgumentNullException(symbol);
      }

      float price = 0;
      char[] symbolArray = symbol.ToCharArray ();
      for (int index = 0; index < symbolArray.Length; index++) {
        price += (int) symbolArray [index];
      }
      price /= 5;
      return price;
    }

    //////////////////////////////////////////////////////
    /// <summary>addExtraRegistrationLogic method</summary>
    /// <remarks>
    /// This attribute configures this method to be invoked during
    /// registration of this assembly
    /// </remarks>
    //////////////////////////////////////////////////////

    [ ComRegisterFunctionAttribute ]
    public static void addExtraRegistrationLogic(string registrationLogic) {
      // perform any extra logic when registration occurs.
      Console.WriteLine( "Invoked by the system since Registration is occuring ..." );
    }

    //////////////////////////////////////////////////////
    /// <summary>removeExtraRegistrationLogic method</summary>
    /// <remarks>
    /// This attribute configures this method to be invoked during
    /// unregistration of this assembly
    /// </remarks>
    //////////////////////////////////////////////////////

    [ ComUnregisterFunctionAttribute ]
    public static void removeExtraRegistrationLogic(string registrationLogic) {
      // perform any extra logic when unregistration occurs.
      Console.WriteLine( "Invoked by the system since Unregistration is occuring ..." );
    }

  }
}

Next you need to examine the use of the COMRegisterFunctionAttribute and COMUnregisterFunctionAttribute  types. Classic COM servers export a couple of functions - DllRegisterServer and DllUnregisterServer - which are invoked by various registration utilities to insert or remove the required COM registration information. Even though .NET binaries do not export such functions, by declaring static methods with these attributes, you can simulate the same behaviour. While the name of these methods is irrelevant, it must take a single string argument that holds the current location of the registry being updated. Also, if you configure a method that takes a COMRegisterFunctionAttribute attribute, you must also configure a method that takes a COMUnregisterFunctionAttribute  attribute. You can thus simulate a self-registering COM Server.

5. Modify the generated AssemblyInfo.cs to add the right assembly information

You provide the compiler with your assembly information in an assembly file called AssemblyInfo.cs. The assembly information file is compiled with the  rest of the project's source files. The information is in the form of assembly attributes - directives to the compiler on the information to embed in the assembly.

AssemblyInfo.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
using System.Reflection;
using System.Runtime.CompilerServices;

//
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
//
[assembly: AssemblyTitle("QuoteServer")]
[assembly: AssemblyDescription("Obtains Stock Quotes given a Stock Symbol")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("iCommWare Corporation")]
[assembly: AssemblyProduct("")]
[assembly: AssemblyCopyright("(c) 2001, Gopalan Suresh Raj. All Rights Reserved.")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("en-US")]

//
// Version information for an assembly consists of the following four values:
//
//      Major Version
//      Minor Version
//      Build Number
//      Revision
//
// You can specify all the values or you can default the Revision and Build Numbers
// by using the '*' as shown below:

[assembly: AssemblyVersion("1.0.0.0")]

//
// In order to sign your assembly you must specify a key to use. Refer to the
// Microsoft .NET Framework documentation for more information on assembly signing.
//
// Use the attributes below to control which key is used for signing.
//
// Notes:
//   (*) If no key is specified, the assembly is not signed.
//   (*) KeyName refers to a key that has been installed in the Crypto Service
//       Provider (CSP) on your machine. KeyFile refers to a file which contains
//       a key.
//   (*) If the KeyFile and the KeyName values are both specified, the
//       following processing occurs:
//       (1) If the KeyName can be found in the CSP, that key is used.
//       (2) If the KeyName does not exist and the KeyFile does exist, the key
//           in the KeyFile is installed into the CSP and used.
//   (*) In order to create a KeyFile, you can use the sn.exe (Strong Name) utility.
//       When specifying the KeyFile, the location of the KeyFile should be
//       relative to the project output directory which is
//       %Project Directory%\obj\<configuration>. For example, if your KeyFile is
//       located in the project directory, you would specify the AssemblyKeyFile
//       attribute as [assembly: AssemblyKeyFile("..\\..\\mykey.snk")]
//   (*) Delay Signing is an advanced option - see the Microsoft .NET Framework
//       documentation for more information on this.
//
[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile("QuoteServer.key")]
[assembly: AssemblyKeyName("")]

In particular, pay attention to the fact that we specify a version number for this library using the AssemblyVersion attribute and also specify the assembly key file using the AssemblyKeyFile attribute.

6. Build the Project Files

Build the files that make up the project. Also generate the type library and register the component as a COM Server for COM interoperability. If you're using VS.NET this is all done automatically for you.

------ Rebuild All started: Project: QuoteServer, Configuration: Debug .NET ------

Preparing resources...
Updating references...
Performing main compilation...

Build complete -- 0 errors, 0 warnings
Building satellite assemblies...
Registering project output for COM Interop...



---------------------- Done ----------------------

Rebuild All: 1 succeeded, 0 failed, 0 skipped

If you had used the Visual Studio.NET IDE, you can move on to Step 7.

If however, you are not using the VS.NET IDE, you'd have to build the library and then register the component using either the Register Assembly (regasm.exe) utility or the Type Library Exporter (tlbexp.exe) utility manually. The Server files can be built using the command:

csc /t:library /out:QuoteServer.dll QuoteServer.cs AssemblyInfo.cs

Then you can register manually using either

regasm QuoteServer.dll /tlb:QuoteServer.tlb

OR

tlbexp QuoteServer.dll /out:QuoteServer.tlb

 

Metadata allows different components, tools, and runtimes to support interoperability as it provides a common format for specifying types. Just as you can inspect the metadata of any .NET assembly, you can also ask an object at runtime for its type, methods, properties, events, etc. Tools can do the same thing. Microsoft ships with a bunch of such tools that assist interoperability.

You can use the .NET assembly registration (regasm.exe) utility to register a .NET component into the system registry so that COM clients can make use of it. Similarly, the Type Library Exporter (tlbexp.exe) utility  generates a type library (.tlb) file when you pass it a .NET assembly. Once you have generated a type library from a given .NET assembly, you can import the type library into VC++ or VB and use the .NET assembly in exactly the same way as if you were using a COM component. To simplify, the type library exporter makes a .NET assembly look like a COM component.

7. Deploy the component as a Shared Assembly

After you've built the assembly, you can use the .NET Global Assembly Cache (GAC) utility to register this assembly into the GAC as shown below.

Command Prompt
C:\MyProjects\Cornucopia\ClassicCOM\QuoteServer\bin\Debug>gacutil /i QuoteServer.dll

Microsoft (R) .NET Global Assembly Cache Utility. Version 1.0.2914.16
Copyright (C) Microsoft Corp. 1998-2001. All rights reserved.

Assembly successfully added to the cache

C:\MyProjects\Cornucopia\ClassicCOM\QuoteServer\bin\Debug>

Successful registration against the cache turns this component into a shared assembly. A version of this component is copied into the GAC so that even if you delete this file locally, you will still be able to run your client program.

 

Classic COM to .NET Interoperability
Developing a Classic COM Server Component using C# and .NET - The QuoteServer Example
Developing a Classic COM Client Application using Visual Basic ver 6.0 to access the QuoteServer - The VB6Client Example
Developing a Managed C++ Client Application which binds to the Server using Early Binding - The QuoteClient Example
Developing a Managed C++ Client Application which binds to the Server using Late Binding - The cppLateBinding Example

 

Download the entire source code as a zip file.

 

click here to go to
My Advanced C#/.NET Tutorial Page...

About the Author...
Gopalan Suresh Raj is a Software Architect, Developer and an active Author. He has co-authored a number of books including "Professional JMS", "Enterprise Java Computing-Applications and Architecture" and "The Awesome Power of JavaBeans". His expertise spans enterprise component architectures and distributed object computing. Visit him at his Web Cornucopia site (http://gsraj.tripod.com/) or mail him at gopalan@gmx.net.

 


Go to the Component Engineering Cornucopia page

This site was developed and is maintained by Gopalan Suresh Raj

This page has been visited times since December 27, 2001.

Last Updated : Dec 27, '01

If you have any questions, comments, or problems regarding this site, please write to me I would love to hear from you.


Copyright (c) 1997-2001, Gopalan Suresh Raj - All rights reserved. Terms of use.

All products and companies mentioned at this site are trademarks of their respective owners.