A COM-visible type has only parameterized constructors

ID

vbnet.portability.com_with_param_constructors

Severity

critical

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Interop

Language

VB.NET

Description

Reports a class whose declaration carries <ComVisible(True)> or <ComVisibleAttribute(True)>, which declares a public constructor taking at least one parameter and no public parameterless constructor. A type with both forms is not reported, and neither is a type that declares no constructor at all, since the compiler then supplies the public parameterless one. A Private or Protected parameterless constructor does not satisfy the requirement and the type is still reported. Types marked <ComVisible(False)> and types with no attribute are not examined, so a type made COM-visible only through an assembly-level <Assembly: ComVisible(True)> is outside this rule’s reach.

Rationale

COM creates objects through IClassFactory::CreateInstance, which takes no arguments. Every COM client - CreateObject("Acme.SessionBridge") from VBScript or VB6, New against a referenced type library, CoCreateInstance from C++ - goes through that one path, so a COM-visible class is creatable only if it has a public parameterless constructor. Declaring any constructor in VB suppresses the implicit one, so a class that declares only New(endpoint As String) has no parameterless constructor at all and cannot be instantiated from COM by any means. Nothing detects this before a client tries: the class compiles, regasm registers it, and the type library is generated. The failure lands on the client as a creation error - CreateObject returns 0x80040110 (CLASS_E_NOAGGREGATION) or a plain "cannot create ActiveX component" - which names the class but says nothing about the missing constructor, and it is raised in a script or a legacy application rather than in the code that has the defect. Because the class is usable from .NET and its own tests, the defect survives every check on the producing side and is discovered only by whoever integrates it.

The following code illustrates the pattern detected by this rule:

Namespace Acme.Interop

    ' FLAGGED: A COM-visible type has only parameterized constructors
    <ComVisible(True)>
    <Guid("8c2b7d40-6f19-4a5e-9d33-1c0e5b7a2f88")>
    Public Class SessionBridge

        Public Sub New(endpoint As String)
            Me.Endpoint = endpoint
        End Sub

Remediation

Add a public parameterless constructor and let the client set the state afterwards through properties - that is the COM idiom, and the parameterized constructors can stay for .NET callers. Where the object genuinely cannot exist without its arguments, keep it uncreatable and export a COM-visible factory class that does have a parameterless constructor and whose instance methods return configured instances; the client creates the factory and asks it for objects. Where the type does not need to be creatable from COM at all, say so by marking it <ComVisible(False)>, so the intent is recorded rather than left to be discovered at integration time. Validate in the property setters or in an explicit initialisation method, so a COM-created object still cannot be used in a half-configured state.

' Before: CreateObject("Acme.SessionBridge") cannot succeed
<ComVisible(True)>
Public Class SessionBridge
    Public Sub New(endpoint As String)
        Me.Endpoint = endpoint
    End Sub

    Public Property Endpoint As String
End Class

' After: creatable from COM, still convenient from .NET
<ComVisible(True)>
Public Class SessionBridge
    Public Sub New()
    End Sub

    Public Sub New(endpoint As String)
        Me.Endpoint = endpoint
    End Sub

    Public Property Endpoint As String
End Class

Configuration

This detector does not need any configuration.