COM Interop

Yeah, I’ve Done That:

Calling .NET Code From ASP

Updated: 12/16/09

    • Visual Studio 2008 SP1

    • Windows 7

    • ASP

    • IIS

COM has to be one of the most finicky, frail, and brittle function calling mechanisms ever invented. Register this; find the programmatic ID; do you have all your GUIDs? However, it is on many millions of computers and even with the advances made in the Microsoft area around component development it is still the only way to get some tasks done. To wit, while you may have to work on an ASP site, you can still utilize the underpinnings of .NET and make your site changes and enhancements far more robust and much more maintainable.

COM is a function calling mechanism. Nothing more; nothing less. In its attempt to be a calling mechanism for everyone and to allow versioning of components and to achieve language neutrality it has become a cumbersome technology to administer. If you have not had the joy of spending a night attempting to get one function call to work via COM on a production system then you just don’t know COM. With that in mind, let’s delve into the implementation of COM and create a .NET class that can be called from an ASP page using VBScript.

A Sample Library

We’ll create a .NET library written in C# with the goal of calling the methods on a class named HelloCom from VBScript code on an ASP page. So fire up Visual Studio and create a C# class library project named ComLibrary. As always, I like to create a Visual Studio “blank solution” named ComLibrarySolution to house the ComLibrary project.

Visual Studio creates a file called AssemblyInfo.cs when it creates a project. Open that file and change the ComVisible attribute from false to true:

[assembly: ComVisible(true)]

It is not strictly necessary to enable this attribute for the initial class we are going to create because all public members of a class are already visible to COM clients, but it will come in handy later so go ahead and set the ComVisible attribute to true. Also take note of the GUID that Visual Studio has conveniently placed in that file for you as well.

Now add a class to the project and name it HelloCom. In order for COM to be able to create an instance of this class, the class must have a default (parameterless) constructor, so add one to the class.

This class is going to have two functions: a read-only property named ComputerName which will return the name of the computer and a method named Echo which will take in a string, append a “2” to it, and return the resultant string to the caller. These methods are quite straightforward, so here’s the entire class:

using System;

namespace ComLibrary

{

public class HelloCom

{

string _echoSuffix;

/// <summary>

/// COM requires a default constructor.

/// </summary>

public HelloCom()

{

_echoSuffix = "2";

}

/// <summary>

/// Gets the name of the computer.

/// </summary>

/// <value>

/// A string containing the name of the computer.

/// </value>

public string ComputerName

{

get

{

return Environment.MachineName;

}

}

/// <summary>

/// “Echo” the string passed in, appending a “2” to it.

/// </summary>

/// <param name="s">

/// The string to be returned.

/// </param>

/// <returns>

/// The sting that was passed in, with a “2” appended to it.

/// </returns>

public string Echo(string s)

{

if (s == null) s = string.Empty;

return s + _echoSuffix;

}

}

}

Register With COM

In order to call this class from a COM client, you must register the generated assembly with COM. There is a command-line tool distributed with the .NET Framework for doing this. It is called the assembly registration tool, and can be found here:

%SystemRoot%\Microsoft.NET\Framework\v2.0.50727\RegAsm.exe

To have this directory automatically added to your PATH so that you can run this utility just by typing its name, bring up a Visual Studio command prompt window. Type regasm in the window and you will see a list of the switches displayed.

To register the above C# library with COM, set your directory to the Debug directory for the solution, for example:

cd/d H:\vsprojects\ComLibrarySolution\ComLibrary\bin\Debug

Let’s try some of the utility’s switches to understand its behavior. Go ahead and register the assembly ComLibrary.dll using this command:

regasm ComLibrary.dll /tlb:ComLibrary.tlb /registered /verbose

You will see output similar to the following:

Microsoft (R) .NET Framework Assembly Registration Utility 2.0.50727.4927

Copyright (C) Microsoft Corporation 1998-2004. All rights reserved.

Types registered successfully

Type 'C' exported.

Assembly exported to 'H:\vsprojects\ComLibrarySolution\ComLibrary\bin\Debug\ComLibrary.tlb', and the type library was registered successfully

This command has done two things:

    1. Created a type library named ComLibrary.tlb for the class.

    2. Added entries to the Windows registry to allow the COM run-time to find this class.

Programmatic ID & Class ID

One primary way that COM finds the information necessary to create an instance of a class is by using a string called a programmatic ID or ProgID. In our sample library, the ProgID for our class is ComLibrary.HelloCom because the namespace we created the class in is ComLibrary and the name of the class is HelloCom.

Lets look at the registry keys that have been added. Fire up the Windows registry editor (regedit.exe) and navigate to the following key:

HKEY_LOCAL_MACHINE\SOFTWARE\Classes\ComLibrary.HelloCom

The HKLM\SOFTWARE\Classes key is the node in the registry that COM looks for ProgIDs. Looking at this key you will see it has one sub value, the (Default) key, which simply echos the ProgID name. It also has one sub-key named CLSID which also has one default value which in this case is set to:

{48EACCF2-D486-35BC-ABD9-F40E9E6EBB80}

This GUID was created by regasm. Every time you run regasm a different GUID will be generated and assigned to the class (we’ll look at how to specify your own GUID below). GUIDs hit it big-time with the first release of COM. A GUID is the internal identifier that COM uses to find a class, and these are known in COM parlance as a class ID or CLSID for short. You can find a COM-enabled class by knowing its ProgID or its CLSID. Since a ProgID is much easier to work with, but since COM really works with the CLSID under the covers, the HKLM\SOFTWARE\Classes node in the registry simply translates ProgIDs to CLSIDs, much like DNS translates host names to IP addresses.

So now let’s find where COM gets all the information about our class. Find this key:

HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Wow6432Node\CLSID

\{48EACCF2-D486-35BC-ABD9-F40E9E6EBB80}

HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Classes\CLSID

\{48EACCF2-D486-35BC-ABD9-F40E9E6EBB80}

Tip: When I’m scouring through the registry to debug issues with a COM object not working properly, I’ll go to the Classes node for my ProgID and highlight the CLSID node in the registry editor. Now double-click the (Default)Edit String window is displayed with the GUID highlighted. Press Ctrl+C, click the Cancel button, press Ctrl+F, Press Ctrl+V, then press Enter. The registry editor should now bring you to the key we just navigated to manually above.

The figure shows this key displayed in the registry editor. The top key has a default value which simply contains the ProgID of the class, as does the ProgId sub-key.

The most important sub-key—which allows the COM run-time to find the class—is the InprocServer32 sub-key. The information found in this sub-key is used to create an instance of the class. This sub-key has the following values:

    • (Default)—The name of the host DLL. For .NET classes, this will always be mscoree.dll.

    • Assembly—The name of the .NET assembly which contains the class.

    • Class—The fully-qualified name of the class.

    • RuntimeVersion—The version of the .NET run-time that the class requires.

    • ThreadingModel—The threading model to be used.

The InprocServer32 key will have a sub-key named the same as the version number assigned to the assembly. In our case this is 1.0.0.0, which is the version number specified in the AssemblyVersion attribute found in the AssemblyInfo.cs file. Notice that the version number sub-key has the same entries as the InprocServer32 key has, except for the ThreadingModel value.

The Implemented Categories sub-key specifies the component category that the class falls under. The GUID shown is the name of a sub-key under this key:

HKEY_CLASSES_ROOT\Component Categories

\{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}

This sub-key simply specifies that this is a .NET class.

Test Harness Project

Now let’s create a test harness project to make sure that we can call our class using the COM run-time. Add a new project of type VB.NET Test Project to your solution and call it ComLibrary.Test. (If your edition of Visual Studio does not have this project type then you will have to create a console application to use as your test harness project.)

Right-click the ComLibrary project node and select Add then New Test. Select the Unit Test template and name the test HelloComTests.cs. The first thing I do is rip out all the auto-generated in the class except for the TestMethod1 method. To this method add the following code:

Dim comObject As Object = CreateObject("ComLibrary.HelloCom")

Dim computer As String = comObject.ComputerName

Console.WriteLine("Computer Name: {0}", computer)

You will also have to add Option Strict Off to the top of the file to allow the VB compiler to late-bind to the ComputerName property of the COM object. If you do not have this reference then you will get the following error when trying to run the unit test:

Cannot create ActiveX component.

Add a project reference to the ComLibrary project. Right-click the ComLibrary.Test node and select Properties. Then go to the References tab and add the project reference.

We are now ready to run the unit test we just created. In the HelloComTests.vb file, right-click somewhere within the confines of the TestMethod1 method, and select Run Tests. Look at the test run results window and you’ll see your computer name displayed.

So let’s think about what we just did. We created a run-of-the-mill .NET class written in C#. The only COM-specific change we made to that class was to enable the ComVisisble attribute, and that isn’t even necessary at this point since the C# class and its methods we wish to call are public. Then, using regasm, we created a type library file and added appropriate entries to the registry so that the class can be called from the COM infrastructure. We then called this class from a VB.NET test harness. We are one step away from calling this .NET class written in C# from the VBScript code on our ASP page.

Before jumping into the VBScript, let’s examine the registry key that directs a scripting engine to the type library for our class. Look back in the AssemblyInfo.cs file that VIsual Studio generated for us. That file contains a GUID (attached to the Guid attribute) that Visual Studio created for us. This GUID is used to define a key like this:

HKEY_CLASSES_ROOT\TypeLib

\{7E48B9D4-B26A-4340-85B5-B442679AD903}

A scripting engine might know the GUID for a type library that it is going to reference. For example, an ASP website’s Global.asa file might have the following type library reference in it:

<!--METADATA TYPE="TypeLib"

NAME="Microsoft ActiveX Data Objects 2.1 Library"

UUID="{00000201-0000-0010-8000-00AA006D2EA4}"

VERSION="2.1"-->

This reference will cause the scripting engine to look up this GUID under the TypeLib key where it can find out where the type library file resides.

The regasm tool took the GUID out of the assembly file’s metadata section and created this key for us as well as populated the key with the appropriate information. Navigate to the that key now. Notice that there is also a version number sub-key under this key. In our case, the version number is 1.0.0, so our type library information would be under this key:

HKEY_CLASSES_ROOT\TypeLib

\{7E48B9D4-B26A-4340-85B5-B442679AD903}

\1.0

\0

\win32

Notice how there is a entry for the “major + minor” version number and then there is a separate sub-key for each release number. The Win32 key points to the location of the type library file for the version of interest, in my case this is:

\\s2\doten\vsprojects\ComLibrarySolution\ComLibrary

\bin\Debug\ComLibrary.tlb

Unfortunately, this scheme does not have a friendly name for the type library like the ProgID does for the class information. It would be nice if there were a “friendly type name”-to-GUID translator, but there is no such mechanism. It’s GUIDs all the way for the type library information.

Sign the Assembly

In order to use COM to call our library, the library assembly must be signed. If you do not have a key-pair file create one with this command:

sn -k ComLibraryKeyPair.snk

This will create a key-pair file that you can add to the Visual Studio solution and then specify that the ComLibrary should be signed with this file on the Signing tab of the Properties pages.

The regasm codebase switch must also be used when registering the library for use on an ASP page. So first un-register the library:

regasm ComLibrary.dll /unregister /tlb /verbose

and then register it again, but with the codebase switch:

regasm ComLibrary.dll /tlb:ComLibrary.tlb /registered /verbose /codebase

Calling COM From VBScript

To create an ASP page that calls our library, you will need to configure IIS to host an ASP website (refer to my article Debugging ASP With Visual Studio for one way to set up this environment). With the required environment in place, create an ASP page which we’ll call test-com.asp. Add this code to the page:

<%

option explicit

on error goto 0

%>

<html>

<head>

<title>COM Test Page</title>

</head>

<body>

<h1>COM Test Page</h1>

<%

dim comLib

set comLib = Server.CreateObject("")

Response.Write "Computer Name: " & comLib.ComputerName & "<br>"

%>

</body>

</html>

Now browse to this test page. If you get this error message:

Server object error 'ASP 0177 : 80070002'

Server.CreateObject Failed

/pm/admin/test-com.asp, line 16

then it means that your ASP website does not have a reference to the library (error code 80070002 is “file not found”). So add a reference to the library DLL file to your website, then browse to the page again.

Calling .NET Framework Classes

Not only can you call your own classes from VBScript, you can also call the .NET Framework classes from VBScript. You may be stuck in the world of ASP and VBScript, but you still have the .NET Framework at your disposal.

dim shell, env

set shell = Server.CreateObject("WScript.Shell")

set env = shell.Environment("Process")

dim systemRoot : systemRoot = env("systemroot")

dim fileSys

set fileSys = Server.CreateObject("Scripting.FileSystemObject")

dim filename : filename = fileSys.GetTempName()

TBS.

References

Patrick Steele over at 15 Seconds wrote an article called COM Interop Exposed which gives a good account of the history of COM and and its relation to VB6. He explains how VB6 exposes a class as a COM coclass and how VB6 generates interfaces. More importantly, he describes how to expose a .NET class to COM, how COM early- and late-bingind work and why. And in Part 2, he describes COM events (the COM connection point protocol).

Calling .NET From COM

Essential COM by Don Box, Addison-Wesley, 1998, ISBN 0-201-63446-5.

Also refer to the Microsoft COM page and the COM, COM+, and DTC page at MSDN.