Click here to Skip to main content
Click here to Skip to main content

How I Came to Love COM Interoperability

, 26 May 2014
Rate this:
Please Sign up or sign in to vote.
This article provides a practical approach for exposing a .NET assembly to COM.

Introduction

Well, maybe the title of this article is slightly exaggerated, but this is the story about how I – despite strong reluctance to do so – successfully managed to expose a .NET library of mine to COM. Furthermore, during this process, my original .NET library became even better and I actually ended up kind of liking the additional COM API.

Obviously, as most other .NET developers, I have had to deal with the tremendous amount of unmanaged code out there. Even if I would rather avoid it, from time to time, I have had to use the .NET interoperability services to consume some ActiveX component or other type of unmanaged legacy code. And I have learned to live with it. But why would someone ever think of exposing a nice and clean .NET library for a development platform (COM) that was deprecated decades ago?

Anyway, recently I found myself in a situation where I was left no choice. I had made this terrific .NET library with loads of nice functionality when the client required that the same functionality was made available through COM.

Background

There are lots of explanations out there on the Internet on how to expose .NET assemblies to COM - for example this article on CodeProject. The reason why I bother to write this article anyway is that none of the resources that I have found on the Internet describes the exact approach that I eventually chose for my project. Also, I did not find any resources giving an overview of all the small challenges I met in the process. And as we all know, the devil is in the detail.

The Example Code

The example code for this article is based on the legendary “Hello World!” example. Compared to the normally very simple structure of such examples, my code might seem unnecessarily complicated to overcome the simple task of displaying the famous text message, but still it is relatively simple and elegantly illustrates all of the challenges that I met when working with the real code base.

The central class in the example library is a Greeter class that has a dependency to an IMessageWriter instance that is injected into the Greeter class through the constructor (yes, this is dependency injection in action):

public class Greeter
{
    ...

    private IMessageWriter messageWriter;

    public Greeter(IMessageWriter messageWriter) : this()
    {
        this.messageWriter = messageWriter;
    }

    ...

    public void Greet(GreetingType greetingType)
    {
        this.messageWriter.Write(greetingType.Greeting);
        ...
    }
}

The IMessageWriter instance is used in the Greet method to write the message. The GreetingType decides the exact phrasing of the greeting (much more about this later). The IMessageWriter interface contains a single Write method:

public interface IMessageWriter
{
    void Write(string message);
} 

The example library comes with a single concrete implementation of the IMessageWriter interface – a ConsoleMessageWriter that writes a text message to the console:

public class ConsoleMessageWriter : IMessageWriter
{
    public void Write(string message)
    {
        Console.Write(message);
    }
} 

From a console application, the following code creates a silly greeting:

internal class Program
{
    private static void Main()
    {
        var greeter = new Greeter(new ConsoleMessageWriter());
        greeter.Greet(GreetingType.Silly);
    }
}

The Overall Approach

Now, let's dig into the matter. Probably because of my initial reluctance to deal with the COM interoperability at all, I decided to make a clear rule for myself – a self-imposed dogma so to speak. I would under no circumstances “pollute” my original .NET library with any COM-related stuff such as COM-specific interfaces or any of the ComVisible-, Guid- or ClassInterface attributes. I would allow no references to the System.Runtime.InteropServices namespace whatsoever. Also, I would not accept major degradations of my original library. So I ended up with a project structure like this:

All the COM-specific stuff is encapsulated in the ClassLibrary.Interop assembly while my original ClassLibrary assembly remains a clean .NET library.

In the ClassLibrary.Interop assembly, I explicitly define all the COM interfaces and decorate them with the Guid attribute:

namespace ClassLibrary.Interop
{
    using System;
    using System.Runtime.InteropServices;

    [Guid("2c0fe71f-fb44-4fae-ab8e-779053f86737")]
    public interface IGreeter
    {
         ... 
    }
}

Furthermore, I create new classes inheriting from the original ones and implementing the corresponding explicitly defined COM interface. I decorate the classes with the Guid attribute and the ClassInterface attribute with the ClassInterfaceType.None parameter. The ClassInterfaceType.None parameter prevents the class interface from being automatically generated when the class metadata is exported to a COM type library. So in the below example, only the members of the IGreeter interface will be exposed:

namespace ClassLibrary.Interop
{
    using System;
    using System.Runtime.InteropServices;

    [Guid("8ea7f6d2-d86f-4070-aaee-b43bbed3706e")]
    [ClassInterface(ClassInterfaceType.None)]
    public class Greeter : ClassLibrary.Greeter, IGreeter
    {
        ...
    }
} 

I don’t bother decorating the individual classes with the ComVisible attribute because the whole point is that in the ClassLibrary.Interop assembly I only deal with .NET types that I want to expose for COM, so instead I declare this once and for all in the AssemblyInfo file:

[assembly: ComVisible(true)]

Dealing with the Challenges

As mentioned earlier, I met a few challenges on the way – mostly because of .NET/C# features that are not supported in COM. In the following, I will describe the individual challenges and the solutions to them.

Constructors with Parameters

COM does not support constructors with parameters. COM requires default (parameterless) constructors.

As shown earlier, the Greeter class uses dependency injection and requires an instance of an IMessageWriter interface provided through its constructor:

public class Greeter
{
    ...

    private IMessageWriter messageWriter;

    public Greeter(IMessageWriter messageWriter) : this()
    {
        this.messageWriter = messageWriter;
    }

    ...
}

So what I did was that I created an additional protected parameterless default constructor and a protected MessageWriter property. The fact that these two additional members are protected is an important point because then I can use them from my Greeter extension class in the ClassLibrary.Interop assembly to provide COM interoperability while still hiding these members from “normal” use of the Greeter class within the .NET Framework – thus forcing the consumer to use the public constructor:

public class Greeter
{
    private readonly List<string> greetings;
    private IMessageWriter messageWriter;

    public Greeter(IMessageWriter messageWriter) : this()
    {
        this.messageWriter = messageWriter;
    }

    protected Greeter()
    {
        this.greetings = new List<string>();
    }
 
    ...

    protected IMessageWriter MessageWriter
    {
        get { return this.messageWriter; }
        set { this.messageWriter = value; }
    }

    ...
}

Then I can introduce an Initialize method in the COM interface of the Greeter class and use this method to set the MessageWriter property.

[Guid("8ea7f6d2-d86f-4070-aaee-b43bbed3706e")]
[ClassInterface(ClassInterfaceType.None)]
public class Greeter : ClassLibrary.Greeter, IGreeter
{
    ...

    public void Initialize(IMessageWriter messageWriter)
    {
        this.MessageWriter = messageWriter;
    }
}

So now, from a COM consumer I will have to first create the Greeter object using the default constructor and then call the Initialize method.

Overloaded Methods

Overloaded methods are not supported in COM. In the Greeter class, I do have two Greet methods with different signatures – one always making a neutral greeting and one where I can provide a specific greeting type as a parameter:

public class Greeter
{
    ...

    public void Greet()
    {
        this.Greet(GreetingType.Neutral);
    }

    public void Greet(GreetingType greetingType)
    {
        this.messageWriter.Write(greetingType.Greeting);
        this.greetings.Add(greetingType.Greeting);
    }
} 

The only way to deal with this problem is to introduce different names in the COM interface:

[Guid("8ea7f6d2-d86f-4070-aaee-b43bbed3706e")]
[ClassInterface(ClassInterfaceType.None)
public class Greeter : ClassLibrary.Greeter, IGreeter
{
    ...

    public void Greet(GreetingType greetingType)
    {
        base.Greet(greetingType);
    }

    public void GreetNeutral()
    {
        base.Greet(GreetingType.Neutral);
    }

    ...
}

Generics

.NET generics is gibberish for COM. So if you have made any generic classes or methods or if you use any of the built-in generic types, then you have to be a bit creative. In the Greeter class, I am using the generic ReadOnlyCollection<> to keep the greeting history:

public class Greeter
{
    ...

    public ReadOnlyCollection<string> Greetings
    {
        get { return new ReadOnlyCollection<string>(this.greetings); }
    }
   
    ...
} 

The solution to this problem is pretty straight forward. Simply let the Greeter extension in the ClassLibrary.Interop assembly return an arrays of strings instead:

[Guid("8ea7f6d2-d86f-4070-aaee-b43bbed3706e")]
[ClassInterface(ClassInterfaceType.None)
public class Greeter : ClassLibrary.Greeter, IGreeter
{
    public new string[] Greetings
    {
        get { return base.Greetings.ToArray(); }
    }

    ...
}

Inheritance

One challenge that I met is in a different category than the others. This challenge was not due to the missing COM support for certain .NET/C# features. Rather, it was due to my self-imposed dogma about keeping my original .NET library free of COM-related stuff. As I wanted to extend the original .NET types with COM interoperability using inheritance, only inheritable types could be extended. .NET types like struct and enum are not inheritable.

So I had to change a couple of structs to classes in my original library, which didn’t really bother me too much.

The enums, however, were a bit trickier. What I did was to introduce my own Enumeration class instead of using enums. This was one of the changes that I actually consider a major improvement to my original code. I have always found it annoying that enums could not be extended with for example a display name (for example including spaces). By introducing an Enumeration class, exactly this can be done:

public abstract class Enumeration : IComparable<Enumeration>
{
    private readonly string displayName;
    private readonly int value;

    protected Enumeration(int value, string displayName)
    {
        this.value = value;
        this.displayName = displayName;
    }

    public string DisplayName
    {
        get { return this.displayName; }
    }

    public int Value
    {
        get { return this.value; }
    }

    ...
}

The whole discussion about using enumeration classes instead of enums is worth a whole article by itself, but another advantage worth mentioning is that this approach can reduce the number of switch statements that inevitably follows from the usage of enums. Look how elegantly the greeting text, in the form of the Greeting property, has become a detail of a greeting type:

public abstract class GreetingType : Enumeration
{
    ...

    protected GreetingType(int value, string displayName)
        : base(value, displayName)
    {
    }

    public abstract string Greeting { get; }

} 

Now the individual greeting types can be defined, e.g. a neutral greeting type:

public class GreetingTypeNeutral : GreetingType
{
    public GreetingTypeNeutral()
        : base(0, "Neutral")
    {
    }

    public override string Greeting
    {
        get { return "Hello World!"; }
    }
}

Or a silly greeting type:

public class GreetingTypeSilly : GreetingType
{
    public GreetingTypeSilly()
        : base(1, "Silly")
    {
    }

    public override string Greeting
    {
        get { return "Howdy World!"; }
    }
}

Static Methods

The GreetingType enumeration class brings us to the last of the challenges. In the GreetingType enumeration class, I define 3 static methods – one for each of the greeting types.

public abstract class GreetingType : Enumeration
{
    public static readonly GreetingType Casual = new GreetingTypeCasual();
    public static readonly GreetingType Neutral = new GreetingTypeNeutral();
    public static readonly GreetingType Silly = new GreetingTypeSilly();

    ...
}

But unfortunately static methods are not supported in COM. So, for the COM interface, I have to expose the 3 greeting type classes instead – here illustrated by the GreetingTypeCasual class:

[Guid("b60afdd6-7f93-48eb-baf1-15196c8f779b")]
[ClassInterface(ClassInterfaceType.None)]
public class GreetingTypeCasual : ClassLibrary.GreetingTypeCasual, IGreetingType
{
}

This is why I had to make the original greeting types public. If I wasn't going to expose my assembly to COM, I would have made the GreetingTypeNeutral (and the other greeting types) internal - or even private classes within the GreetingType class.

COM Registration

When all challenges are overcome and the ClassLibrary.Interop assembly is ready, it must be properly registered.

In my ClassLibrary.Interop project, I have checked the “Register for Com interop” option under the projects Build properties. This will do the trick on your own machine.

If you want to deploy the COM version of the library to other machines, you have to use the assembly registration tool RegAsm. If you call it from a Windows batch file placed in the same folder as the assembly itself, you can for example use the following syntax:

c:\Windows\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe /tlb /codebase %~dp0ClassLibrary.Interop.dll

This approach requires that the assembly is signed with a strong name (even if not put in the GAC).

My guess is that most COM consumers run in 32 bit. If you want to register for 64 bit consumers, you should call the 64 bit version of RegAsm found in c:\Windows\Microsoft.NET\Framework64.

VBA Sample

And finally, here is some sample code using the COM API from a Visual Basic for Applications (VBA) macro:

Public Sub testGreeter()
    Dim ConsoleMessageWriter As New ConsoleMessageWriter
    Dim Greeter As New Greeter
    Dim Greetings() As String
    
    Greeter.Initialize ConsoleMessageWriter
    
    Greeter.GreetNeutral
    Greetings = Greeter.Greetings
    MsgBox Greetings(0), vbOKOnly, "Neutral greeting"
    
    Greeter.Greet New GreetingTypeCasual
    Greetings = Greeter.Greetings
    MsgBox Greetings(1), vbOKOnly, "Casual greeting"  
End Sub

Summary

This article describes an approach for exposing a .NET assembly to COM by handling all the COM-specifics in a dedicated ClassLibrary.Interop assembly without having to compromise the original ClassLibrary assembly.

Exposing .NET assembly functionality to COM does not necessarily need to be a hassle. Yes, there are indeed some challenges to overcome and, for sure, my personal preference will always be to use the .NET assembly directly. However, I do see some advantages in providing a dedicated COM API acting as a sort of “higher level” scripting API for other than hardcore .NET programmers. I kind of like the way that the explicitly defined COM interfaces in the ClassLibrary.Interop assembly acts as a facade to the full functionality, and how for example abstract base classes and interfaces are hidden to the COM API user.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

lars.michael.dk
Architect
Denmark Denmark
I am a software architect/developer/programmer.
 
I have a rather pragmatic approach towards programming, but I have realized that it takes a lot of discipline to be agile. I try to practice good craftsmanship and making it work.
Follow on   Twitter   LinkedIn

Comments and Discussions

 
QuestionGreat article, example code assumes VS2012 Pinmembertealok1-Jun-14 8:39 
AnswerRe: Great article, example code assumes VS2012 Pinmemberlars.michael.dk2-Jun-14 4:31 
QuestionArgh! Where were you a week ago when I needed this?! =P Pinmemberal13n31-May-14 0:40 
AnswerRe: Argh! Where were you a week ago when I needed this?! =P Pinmemberlars.michael.dk31-May-14 3:56 
GeneralMy vote of 5 PinmemberMember 1016216030-May-14 8:30 
GeneralRe: My vote of 5 Pinmemberlars.michael.dk31-May-14 4:08 
GeneralNice! Pinmembermohd_muneer29-May-14 21:06 
GeneralRe: Nice! Pinmemberlars.michael.dk31-May-14 4:09 
QuestionGood work! PinpremiumVolynsky Alex26-May-14 8:44 
AnswerRe: Good work! Pinmemberlars.michael.dk31-May-14 4:11 
QuestionRe: Good work! PinpremiumVolynsky Alex31-May-14 6:14 
QuestionRegistering to Network Drive PinmemberSampath Sridhar22-May-14 21:27 
GeneralVery good article PinmemberKiloBravoLima22-May-14 7:58 
GeneralRe: Very good article Pinmemberlars.michael.dk1-Jun-14 22:55 
GeneralMy vote of 5 PinpremiumPragmateek21-May-14 22:03 
GeneralRe: My vote of 5 Pinmemberlars.michael.dk21-May-14 22:26 
GeneralRe: My vote of 5 PinpremiumPragmateek21-May-14 22:32 
QuestionNaming of com wrapper PinmemberMember 422717821-May-14 21:19 
AnswerRe: Naming of com wrapper Pinmemberlars.michael.dk21-May-14 22:28 
SuggestionRegistration free COM PinprofessionalCatchExAs20-May-14 12:18 
QuestionFrom one pragmatist to another ... PinmemberGarth J Lancaster20-May-14 11:21 
AnswerRe: From one pragmatist to another ... PinpremiumPragmateek21-May-14 22:13 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140721.1 | Last Updated 26 May 2014
Article Copyright 2014 by lars.michael.dk
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid