Exposing a C# component as a COM Server and other GAC related issues

(Sorry about the initial formatting in Firefox. WordPress didn’t like the xml style comments that get inserted into C# so I’ve removed them.)
Exposing a C# component as a COM Server and other GAC related issues
There are actually quite a few good sites that document how this works, but during the process of setting up a .NET component as a COM server, I ran into a number of issues that I hadn’t expected and thought I would post some information and hopefully save other folks the headaches. And I guess this is partly due to the fact that I was mostly interested in setting up my objects for vbscript/PHP/python access and in order for that to happen, the COM object would need to support late binding. Most of the documentation you will find online only will note how to create a COM server that supports early binding through the use of the generated type library. I’m hoping that this post will fill in some of those gaps.

Exposing the COM object:

So lets start with the basics. You have a .NET component and you want to expose it as a COM object — what exactly do you need to do to make this work. Well, first, you need to create the interface, event and vtable. So here’s the steps and example code:

  1. import the System.Runtime.InteropServices;
  2. Next, we need to setup the COM interfaces. The first option defines the interface and sets up the vtable to expose the objects of the component. Next, you setup the Events and finally, attach the final automatic commands to the main class. Also, of note. You need to create unique GUID statements for each of these three elements. So example, the code would look like:

using System;
using System.Runtime.InteropServices;
namespace MARCEngine5
{

[Guid(“337B0A89-A6A6-4122-8932-D2809352BF6D”)]
public interface MARC21_Interface
{
[DispId(1)]
int MarcFile(string sSource, string sDest);

[DispId(2)]
int MarcFileEx(string sSource, string sDest, int lFlag);

[DispId(3)]
int GetError{get;set;}

[DispId(4)]
int MMaker(string sSource, string sDest);

[DispId(5)]
int MMakerEx(string sSource, string sDest, int lFlag);

[DispId(6)]
int MARC2MARC21XML(string sSource, string sDest, bool bNamespace);

[DispId(7)]
int XML2XML(string sSource, string sDest, string sXSLTPath);

[DispId(8)]
int ReadMARC21XML(string sSource, string sDest, string sXSLT, int lFlag);

[DispId(9)]
int XML2MARC(string sSource, string sDest, string sXSLT, string sMARCXSLT, int lFlag) ;

[DispId(10)]
int MARC2XML(string sSource, string sDest, string sXSLT);

[DispId(11)]
int XMLFile(string sSource, string sDest);

[DispId(12)]
int XMLtoXML(string sSource, string sDest, string sXSLTPath);

[DispId(13)]
int MARCXMLtoMARC(string sSource, string sDest, string XSLT);

[DispId(14)]
int XMLtoMARC(string sSource, string sDest, string sXSLT, string sMARCXSLT);

}

[Guid(“92F0772D-0E9B-41a3-B1B0-216DEAF813A6”),
InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface MARC21_Events

{}

[Guid(“D4F347E9-AEFB-461f-AAC8-04315C79327A”),
ClassInterface(ClassInterfaceType.AutoDual),
ComSourceInterfaces(typeof(MARC21_Events))]
public class MARC21: MARC21_Interface

{

3) Next, the assembly will need to have a set GUID for the assembly itself. In the AssemblyInfo.cs file, you need to set the GUIDAttribute element with a unique GUID that will identify the component when registered. Oh, and the GUIDAttribute is in the System.Runtime.InteropServices namespace, so that will need to be imported into this file. Example:

[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile(@”..\..\MARCEngine.snk”)]
[assembly: AssemblyKeyName(“”)]
[assembly: GuidAttribute(“39BD230C-F771-4f31-97CD-822E93208197”)]

4) Something you might have noted in the above code snippet is that the class as been given a strong name. This is being set in the AssemblyKeyFile. Why is this there? Well, in order to setup your component as a COM server, you will need to move your component into the GAC (Global Assembly Cache). However, only strong-named components can be moved into the GAC, so you need to sign the component. How do you sign a component….Microsoft provides a tool in the SDK called sn.exe. You need to use this tool to create a keyfile that will assign a strong name to a defined component. However, something that the documentation doesn’t tell you is that once you sign a component, a number of things need to happen.

  • Once a component has been signed, all components that it references must be signed. This means that if you have created another component an have referenced it within this object (for example, you are creating the COM component as a wrapper around a number of .NET assemblies), all referenced components must be signed as well
  • Once a component has been signed, all files that access this newly signed referenced component must be recompiled. A component compiled against a component without a strong name cannot then use the component within a re-compile once the component has been given a strong name.

5) Once you’ve setup the above, you need to compile and create a type library. The easiest way to do this is to set an option in the project’s build options. Under build, there is an option marked as “Build for COM Interopâ€?. Set this to true (see below). This option will essentially performs the task of running the regasm command, both registering the component and generating the type library.

interop.PNG

6) Ok — you’ve compiled your code and everything is working on your machine. How do I move this to someone else’s. Easy, right? Ha! You would wish. Basically, when installing your COM component onto someone else’s machine, you need to do the following:

  1. Register your assembly (Regasm)
  2. Move your assembly to the GAC (gacutil)
  3. Move all referenced assemblies to the GAC (gacutil)

Now, the three steps above are basically what setting the Register for COM Interop option did for you when you built the component. However, now you have to do this for your clients and the utilities that I’ve noted, the Regasm and gacutil, these are utilities that likely will not be on your client’s machine. The gacutil generally ships with the SDK — the Regasm ships with Framework 1.1+. So you will likely need to fine another method to register your component

7) Fortunately, the .NET framework provides a method that can be used to register assemblies and move them into the GAC. These methods are found in the System.EnterpriseServices.Internal namespace. So, what I’ve found (and what I’m doing with MarcEdit) is that building a bootloader to handle this process for you is generally the simpliest way of setting up your assembly for COM. The code for this is very simple. You utilized the Publish Object in the System.EnterpriseServices.Internal namespace, calling functions: RegisterAssembly and GACInstall. Likewise, then you uninstall your application, you will want to call: UnRegisterAssembly and GACRemove to clean your assemblies from the GAC and remove the necessary registery entries. So an example of this type of code would be:

Publish p = new Publish();

if (Array.BinarySearch(args,”-u”)>-1)
{
//We are uninstalling….
if (System.IO.File.Exists(AppPath() + @”MARCEngine.dll”))
{
p.UnRegisterAssembly(AppPath() + “mengine60.dll”);
p.UnRegisterAssembly(AppPath() + “marc82utf8.dll”);
p.UnRegisterAssembly(AppPath() + “meUTF2MARC.dll”);
p.UnRegisterAssembly(AppPath() + “MARCEngine.dll”);
p.GacRemove(AppPath() + “MARCEngine.dll”);
p.GacRemove(AppPath() + “mengine60.dll”);
p.GacRemove(AppPath() + “marc82utf8.dll”);
p.GacRemove(AppPath() + “meUTF2MARC.dll”);
}
}else{
//We are uninstalling….
if (System.IO.File.Exists(AppPath() + @”MARCEngine.dll”))
{
p.RegisterAssembly(AppPath() + “mengine60.dll”);
p.RegisterAssembly(AppPath() + “marc82utf8.dll”);
p.RegisterAssembly(AppPath() + “meUTF2MARC.dll”);
p.RegisterAssembly(AppPath() + “MARCEngine.dll”);
p.GacInstall(AppPath() + “MARCEngine.dll”);
p.GacInstall(AppPath() + “mengine60.dll”);
p.GacInstall(AppPath() + “marc82utf8.dll”);
p.GacInstall(AppPath() + “meUTF2MARC.dll”);
}
}

And that’s pretty much it. Simple right? J There are a number of very good links that provide a better explaination to some or parts of this process, starting with Microsoft’s documentation. Some of these links are:

If anyone has any other questions about how this works, feel free to give me a holler or leave a comment.

–Terry


Posted

in

,

by

Tags:

Comments

4 responses to “Exposing a C# component as a COM Server and other GAC related issues”

  1. Alex Avatar
    Alex

    Hi Terry,

    I am using the GacInstall to publish my assemblies, however once installed into the gac, I must delete my ‘temporary’ copy of the assemblies.

    And then, if I ever wanted to uninstall the assemblies from the gac I do not have the files at the original path. This is causing a problem since I cannot seem to get the gacremove method to uninstall the assemblies unless I keep the original files…

    Do you know how I can accomplish the removal of the files from the gac without needing the original files at the ‘temporary’ location?

    I thought windows would automatically look at the GAC first when searching for a dll, does this not apply for removing assemblies?

    do you have any insight on this?

    Thanks

  2. Administrator Avatar
    Administrator

    Actually, it depends on your assemblies. If you are using strong named assemblies, windows will look for the assembly in the:
    1) GAC
    2) Application config file
    3) Application directory
    4) subfolder in the application directory with the same short name as the assembly.

    If not signed, the order is:
    1) Application config file
    2) Application directory
    3) subfolder in the application directory with the same short name as the assembly.

    In terms of your specific question — I’m not sure to be honest. My understanding is that the GacInstall/GacRemove require a full path to the file — so the “temp” files are required. This is likely why entries installed into the GAC by MSI installers cannot be manually deleted — since MSI manages the “filepaths” for uninstallation purposes. Though that is just a best guess.

    –TR

  3. Brian Avatar
    Brian

    Good info. I have a question on trapping for success/failure for GACInstall and GACRemove. In your example you show the function calls, but are there any return codes for those functions? I didn’t see anything explicit on MS’s site save for security exceptions.

    Ideally specific return codes would be nice to have, but I’d be happy with just a true or false that the file made it into the GAC or not.

    Thanks.

    Brian

  4. Administrator Avatar
    Administrator

    No return value, but it throws a SecurityException when registration can’t take place. So you can obviously trap the exception using the try…catch syntax:

    Publish p = new Publish();

    //We are installing
    if (System.IO.File.Exists(AppPath() + @�MARCEngine.dll�))
    {
    try {
    p.RegisterAssembly(AppPath() + “MARCEngine.dllâ€?);
    p.GacInstall(AppPath() + “MARCEngine.dllâ€?);
    } catch (System.SecurityException e) {
    System.Windows.Forms.MessageBox.Show(e.ToString());
    }
    }

    –TR