COM Interop and Office Automation

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. COM (the Component Object Model) is a Windows technology, and the development machine behind this page was a Linux container, so the page is explicit about what ran. Examples that can run anywhere (reflection over the COM attributes that ComClass generates, the failure of COM calls on Linux, the Open XML SDK) were run. Examples that automate Excel and Word, and the COM server build, were built with warnings treated as errors and are labelled "built, not run": the Office examples compile against the real Microsoft interop packages, but no Office installation was available. Every example starts with Option Strict On and has the root namespace ShelfApp, unless it says otherwise.

COM lets a program use objects that live in another binary, possibly in another process, through interfaces identified by GUIDs. Visual Basic was designed around it: Visual Basic 6.0 programs were COM programs, and Office’s macro language, VBA, is a COM client. On .NET the same ideas apply with two wrappers. A runtime callable wrapper (RCW) makes a COM object look like a .NET object, so that Visual Basic code can consume Excel, Word or a vendor’s ActiveX library. A COM callable wrapper (CCW) makes a .NET object look like a COM object, so that VBA, VB6, scripts or C++ programs can use a Visual Basic class. The native-code counterpart, which needs no registration and no type information, is on Native Interop: Declare and P/Invoke.

Older books say that COM interop "does not work" with .NET Core and .NET 5 and later. That is wrong, and so is the opposite assumption that everything from .NET Framework carries over: COM clients and COM servers both work on .NET on Windows, with different tools. Neither works on Linux or macOS.

Two directions

Question Consuming COM (client) Exposing .NET to COM (server)

Wrapper

runtime callable wrapper (RCW), created by the runtime when a COM object is first used

COM callable wrapper (CCW), created when native code asks for the object

What you need

an interop assembly (types that describe the COM library) or late binding

<ComVisible(True)>, a <Guid> for the class, public types with a public parameterless constructor, and a COM host

Typical use

automating Excel, Word, Outlook; ActiveX controls; Windows components such as the Scripting Runtime

a plug-in for a VB6 or VBA application, a library for a scripting host

Visual Basic specifics

optional parameters, named arguments, indexed properties and late binding are language features

<ComClass> generates the interfaces for you

Registration

none; the COM library is registered by its installer

regsvr32 Name.comhost.dll, or a registration-free manifest

Task .NET Framework .NET 10 on Windows

Reference a COM library

Add Reference > COM, which writes a COMReference item, builds with MSBuild from Visual Studio

a NuGet interop package, or COMReference built with the .NET Framework version of MSBuild; the dotnet command line refuses it (MSB4803, shown below)

Embed interop types (no-PIA)

EmbedInteropTypes

same, on COMReference

Expose a class

Regasm /codebase or the "Register for COM interop" setting

EnableComHosting and regsvr32 on the generated comhost.dll

Type library for the server

tlbexp or Regasm /tlb

none is generated; write an IDL file and embed the compiled library

CLSID for a class

derived from the type when Guid is missing

Guid is required on every class that COM can create

Automatic class interface

ClassInterfaceType.AutoDual is available

use ClassInterfaceType.None with an explicit interface

Self-contained deployment of the server

not applicable, the Framework is installed system-wide

not supported; framework-dependent only

COM is Windows-only

On Linux and macOS the COM-related Visual Basic and BCL calls compile, because the types exist, but the platform analyzer warns about them (CA1416) and they do nothing useful at run time. This program disables that warning for one region and calls them anyway, to show what a COM call does on a non-Windows system:

Option Strict On

Imports System

#Disable Warning CA1416 ' the analyzer would flag every COM call on a non-Windows build
Namespace C01
    Friend Module Program
        Friend Sub Main()
            Console.WriteLine("Running on Windows: " & OperatingSystem.IsWindows().ToString())

            Dim byProgId As Type = Type.GetTypeFromProgID("Excel.Application")
            Console.WriteLine("GetTypeFromProgID:  " & If(byProgId Is Nothing, "Nothing", byProgId.FullName))

            Dim byClsid As Type = Type.GetTypeFromCLSID(New Guid("00024500-0000-0000-C000-000000000046"))
            Console.WriteLine("GetTypeFromCLSID:   " & If(byClsid Is Nothing, "Nothing", byClsid.FullName))

            Try
                Dim excel As Object = CreateObject("Excel.Application")
                Console.WriteLine("CreateObject:       " & TypeName(excel))
            Catch ex As Exception
                Console.WriteLine("CreateObject:       " & ex.GetType().FullName & ": " & ex.Message)
            End Try

            Try
                Dim report As Object = GetObject("C:\Shelf\report.xlsx")
                Console.WriteLine("GetObject:          " & TypeName(report))
            Catch ex As Exception
                Console.WriteLine("GetObject:          " & ex.GetType().FullName & ": " & ex.Message)
            End Try
        End Sub
    End Module
End Namespace
Running on Windows: False
GetTypeFromProgID:  Nothing
GetTypeFromCLSID:   Nothing
CreateObject:       System.Exception: Cannot create ActiveX component.
GetObject:          System.Exception: Cannot create ActiveX component.

Type.GetTypeFromProgID and GetTypeFromCLSID return Nothing instead of throwing, and CreateObject and GetObject raise a plain System.Exception whose text is the old Visual Basic error 429 message, "Cannot create ActiveX component." Real code guards COM paths with OperatingSystem.IsWindows() and annotates them with <SupportedOSPlatform("windows")>, as the Office examples below do.

Consuming COM

Interop assemblies and COMReference

A COM library describes its types in a type library. To use it from .NET you need an interop assembly, a .NET assembly with an equivalent for every interface, class and enumeration. There are three ways to get one:

  • A primary interop assembly from the vendor, usually on NuGet. For Office, Microsoft publishes Microsoft.Office.Interop.Excel (16.0.18925.20022 was current on the day this page was written) and Microsoft.Office.Interop.Word (15.0.4797.1004). They are ordinary packages; the DLL is copied next to your program, and the examples below were built against them with a normal PackageReference:

    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net10.0</TargetFramework>
        <RootNamespace>ShelfApp</RootNamespace>
        <OptionStrict>On</OptionStrict>
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
      </PropertyGroup>
      <ItemGroup>
        <PackageReference Include="Microsoft.Office.Interop.Excel" Version="16.0.18925.20022" />
        <PackageReference Include="Microsoft.Office.Interop.Word" Version="15.0.4797.1004" />
      </ItemGroup>
    </Project>
  • A COMReference item, which asks MSBuild to run tlbimp against the registered type library and, with EmbedInteropTypes, to copy the few types you use into your own assembly so that nothing but your program is deployed:

    <ItemGroup>
      <COMReference Include="Microsoft.Office.Interop.Excel">
        <WrapperTool>tlbimp</WrapperTool>
        <VersionMajor>1</VersionMajor>
        <VersionMinor>9</VersionMinor>
        <Guid>00020813-0000-0000-c000-000000000046</Guid>
        <Lcid>0</Lcid>
        <Isolated>false</Isolated>
        <EmbedInteropTypes>true</EmbedInteropTypes>
      </COMReference>
    </ItemGroup>

    The dotnet build command cannot process it. A project like the one above, built with the SDK’s command line in the build container, stops with:

    Microsoft.Common.CurrentVersion.targets(3084,5): error MSB4803: The task "ResolveComReference" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.

    The message was printed for a net10.0-windows project. It names the edition of MSBuild, not the operating system: the task exists only in the .NET Framework version of MSBuild, which is the one that Visual Studio runs.

  • A hand-made or pregenerated interop assembly, created once with tlbimp on a Windows machine and referenced as a normal file or package.

Early binding: automating Excel

With an interop assembly the compiler knows every member, so Option Strict On is satisfied and IntelliSense works. The program below creates a workbook, writes a header row and a few cells, saves it as .xlsx, and releases everything. It is built, not run.

Option Strict On

Imports System
Imports System.IO
Imports System.Runtime.InteropServices
Imports System.Runtime.Versioning
Imports Excel = Microsoft.Office.Interop.Excel

Namespace O1
    <SupportedOSPlatform("windows")>
    Friend Module Program
        ' Every COM wrapper obtained through a property or method call must be released, in reverse order
        Private Sub Release(Of T As Class)(ByRef comObject As T)
            If comObject IsNot Nothing AndAlso Marshal.IsComObject(comObject) Then
                Marshal.FinalReleaseComObject(comObject)
            End If
            comObject = Nothing
        End Sub

        <STAThread>
        Friend Sub Main()
            Dim path As String = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "shelf-books.xlsx")
            Dim app As Excel.Application = Nothing
            Dim books As Excel.Workbooks = Nothing
            Dim book As Excel.Workbook = Nothing
            Dim sheets As Excel.Sheets = Nothing
            Dim sheet As Excel.Worksheet = Nothing
            Dim header As Excel.Range = Nothing
            Dim cell As Excel.Range = Nothing
            Try
                app = New Excel.Application()
                app.Visible = False
                app.DisplayAlerts = False
                books = app.Workbooks
                book = books.Add()
                sheets = book.Worksheets
                sheet = CType(sheets(1), Excel.Worksheet)
                sheet.Name = "Books"

                header = sheet.Range("A1", "C1")
                header.Value2 = New Object() {"ISBN", "Title", "Copies"}
                header.Font.Bold = True

                cell = sheet.Range("A2")
                cell.Value2 = "978-0-13-468599-1"
                Release(cell)
                cell = sheet.Range("C2")
                cell.Value2 = 3

                book.SaveAs(Filename:=path, FileFormat:=Excel.XlFileFormat.xlOpenXMLWorkbook)
                Console.WriteLine("Saved " & path & " (" & New FileInfo(path).Length.ToString() & " bytes)")
            Finally
                Release(cell)
                Release(header)
                Release(sheet)
                Release(sheets)
                If book IsNot Nothing Then book.Close(SaveChanges:=False)
                Release(book)
                Release(books)
                If app IsNot Nothing Then app.Quit()
                Release(app)
            End Try
        End Sub
    End Module
End Namespace

What Visual Basic does for you here, and what it does not:

  • Optional parameters and named arguments. book.SaveAs(Filename:=path, FileFormat:=...) omits the dozen other optional parameters of SaveAs. In older C# code this needed a column of Type.Missing.

  • Indexed properties. sheets(1) calls the default indexed property of Sheets. It returns Object, so Option Strict On requires CType(sheets(1), Excel.Worksheet), and sheet.Range("A1").Value2 is an Object as well. A program that leaves the conversions out:

    Option Strict On
    Imports Excel = Microsoft.Office.Interop.Excel
    Module Program
        Sub Main()
            Dim app As New Excel.Application()
            app.Quit()
            Dim book As Excel.Workbook = app.Workbooks.Add()
            Dim sheet As Excel.Worksheet = book.Worksheets(1)
            Dim name As String = sheet.Range("A1").Value2
            book.Close()
        End Sub
    End Module

    gets these messages from the compiler:

    Program.vb(8,40): error BC30512: Option Strict On disallows implicit conversions from 'Object' to 'Worksheet'.
    Program.vb(9,30): error BC30512: Option Strict On disallows implicit conversions from 'Object' to 'String'.
  • No Set. Object assignment is a plain =; Set is Visual Basic 6.0 syntax.

  • ByRef Object parameters. Many Office methods take ref object. Passing a String variable where the parameter is ByRef Object is rejected because the callee could store another type back into the variable; pass an Object variable or a literal. Word’s SaveAs2 shows it:

    Program.vb(25,39): error BC32029: Option Strict On disallows narrowing from type 'Object' to type 'String' in copying the value of 'ByRef' parameter 'FileName' back to the matching argument.

    The Word example below declares Dim target As Object = path for that reason.

  • A thread in a single-threaded apartment. Office objects expect to be called from an STA thread; <STAThread> on Main selects it (see Apartments and threads).

Word

A Word document follows the same pattern. Built, not run:

Option Strict On

Imports System
Imports System.Runtime.InteropServices
Imports System.Runtime.Versioning
Imports Word = Microsoft.Office.Interop.Word

Namespace O3
    <SupportedOSPlatform("windows")>
    Friend Module Program
        <STAThread>
        Friend Sub Main()
            Dim path As String = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "shelf-letter.docx")
            Dim app As Word.Application = Nothing
            Dim docs As Word.Documents = Nothing
            Dim doc As Word.Document = Nothing
            Dim content As Word.Range = Nothing
            Try
                app = New Word.Application()
                app.Visible = False
                docs = app.Documents
                doc = docs.Add()
                content = doc.Content
                content.Text = "Dear reader, your loan of 978-0-13-468599-1 is due."
                ' SaveAs2 takes "ref object": a String variable cannot be passed, an Object variable can
                Dim target As Object = path
                doc.SaveAs2(FileName:=target, FileFormat:=Word.WdSaveFormat.wdFormatXMLDocument)
                Console.WriteLine("Saved " & path)
            Finally
                If content IsNot Nothing Then Marshal.FinalReleaseComObject(content)
                If doc IsNot Nothing Then
                    doc.Close(SaveChanges:=False)
                    Marshal.FinalReleaseComObject(doc)
                End If
                If docs IsNot Nothing Then Marshal.FinalReleaseComObject(docs)
                If app IsNot Nothing Then
                    app.Quit(SaveChanges:=False)
                    Marshal.FinalReleaseComObject(app)
                End If
            End Try
        End Sub
    End Module
End Namespace

Late binding

When the type library is not available at build time, a program can still talk to a COM object through IDispatch, looking up members by name at run time. CreateObject("Excel.Application") returns Object. Under Option Strict On the natural syntax is refused:

Option Strict On
Module Program
    Sub Main()
        Dim excel As Object = CreateObject("Excel.Application")
        excel.Visible = True
        excel.Quit()
    End Sub
End Module
Program.vb(5,9): error BC30574: Option Strict On disallows late binding.
Program.vb(6,9): error BC30574: Option Strict On disallows late binding.

There are two ways out. The first is to turn Option Strict off for one file, which Early and Late Binding shows and discusses. The second keeps Option Strict On and makes the dynamic calls explicit, with CallByName from the Visual Basic runtime and Type.InvokeMember from the BCL. Built, not run:

Option Strict On

Imports System
Imports System.Reflection
Imports System.Runtime.InteropServices
Imports System.Runtime.Versioning

Namespace O2
    <SupportedOSPlatform("windows")>
    Friend Module Program
        <STAThread>
        Friend Sub Main()
            ' Option Strict On forbids "excel.Visible = True" on an Object, but two explicit APIs are allowed:
            ' CallByName from the VB runtime ...
            Dim excel As Object = CreateObject("Excel.Application")
            Dim books As Object = Nothing
            Dim book As Object = Nothing
            Dim sheet As Object = Nothing
            Try
                CallByName(excel, "Visible", CallType.Let, False)
                books = CallByName(excel, "Workbooks", CallType.Get)
                book = CallByName(books, "Add", CallType.Method)
                sheet = CallByName(book, "ActiveSheet", CallType.Get)
                Dim cell As Object = CallByName(sheet, "Range", CallType.Get, "A1")
                Try
                    CallByName(cell, "Value2", CallType.Let, "ISBN")
                    Console.WriteLine("A1 = " & Convert.ToString(CallByName(cell, "Value2", CallType.Get)))
                Finally
                    Marshal.FinalReleaseComObject(cell)
                End Try

                ' ... and Type.InvokeMember from the BCL, which is the same IDispatch call written out
                Dim t As Type = excel.GetType()
                Console.WriteLine("Version " & Convert.ToString(t.InvokeMember("Version", BindingFlags.GetProperty, Nothing, excel, Nothing)))
            Finally
                If book IsNot Nothing Then CallByName(book, "Close", CallType.Method, False)
                If sheet IsNot Nothing Then Marshal.FinalReleaseComObject(sheet)
                If book IsNot Nothing Then Marshal.FinalReleaseComObject(book)
                If books IsNot Nothing Then Marshal.FinalReleaseComObject(books)
                CallByName(excel, "Quit", CallType.Method)
                Marshal.FinalReleaseComObject(excel)
            End Try
        End Sub
    End Module
End Namespace

Late binding is slower, has no compile-time checking and no IntelliSense, but it needs no interop assembly and works with whichever version of Office is installed. It remains the pragmatic choice for a small script-like task. GetObject attaches to a running instance or opens a file through its registered handler; Marshal.GetActiveObject, which .NET Framework programs used for the same purpose, does not exist on .NET:

Program.vb(6,27): error BC30456: 'GetActiveObject' is not a member of 'Marshal'.

Releasing COM objects

Every COM object reached through a property or method call is represented by a wrapper that holds one COM reference. The garbage collector releases it eventually, but "eventually" can be minutes, and until then the Excel process stays in Task Manager with a hidden workbook. So automation code releases explicitly:

  • Marshal.ReleaseComObject(x) decrements the wrapper’s count and returns the new count; Marshal.FinalReleaseComObject(x) sets it to zero. When a wrapper is used by only one variable in your program, prefer FinalReleaseComObject.

  • Do not chain. app.Workbooks.Add().Worksheets(1) creates wrappers for Workbooks and for the workbook that no variable holds, so they cannot be released. The example assigns every intermediate object to its own variable and releases them in reverse order in Finally.

  • Release(Of T) in the Excel example is a helper; after it runs the variable is Nothing, so a second call is harmless.

  • Release after Quit, not before: book.Close(SaveChanges:=False) and app.Quit() are calls on live wrappers.

  • A For Each over a COM collection also creates an enumerator and a wrapper per item; index with For i = 1 To count and release each element when the collection is large.

  • Forcing GC.Collect() followed by GC.WaitForPendingFinalizers() is a last resort for code you cannot restructure; it does not replace explicit releases.

Apartments and threads

COM objects live in an apartment. Office applications are single-threaded apartment (STA) servers; a call from another kind of thread is marshalled through a proxy and is slower and, for some members, fails. In Visual Basic a console program on .NET starts in the multithreaded apartment unless the entry point is marked <STAThread>, as the examples above are. Rules that follow:

  • Create and use a COM object on the same thread. Do not create it in Main and call it from Task.Run, a thread-pool continuation after Await, or a Parallel.For body.

  • If you need background work, start a dedicated thread, call SetApartmentState(ApartmentState.STA) before Start, and keep all the COM work on it. (Setting the apartment state is only meaningful on Windows.)

  • Windows Forms and WPF UI threads are STA already, so COM calls from event handlers are fine, but they block the UI for the duration of the call.

When Office says no

Automation fails in ways that are not specific to Visual Basic. The usual COMException HRESULTs are 0x80040154 (the class is not registered: Office is not installed, or only the wrong bitness is), 0x80010001 (the call was rejected because Excel is busy, for example in a modal dialog or in-cell edit mode; retry after a pause), 0x800A03EC (a generic Excel error such as an invalid range or a protected sheet) and 0x800706BA (the RPC server is unavailable because the process has exited). These constants come from the Windows and Office documentation; they were not reproduced on this page’s Linux machine.

Office automation is not a server technology

Microsoft’s support article "Considerations for server-side Automation of Office" is blunt: Office applications were designed and tested as end-user products on a workstation, assume an interactive desktop and a user profile, and do not provide the reentrancy and security that unattended server components need. Microsoft does not recommend, and does not support, automation of Office from unattended, non-interactive components such as ASP.NET and Windows services, because Office may behave unstably or deadlock. Two further consequences are practical: every Office instance is a heavy process, and a server that handles many requests will start many of them. Automation is for desktop programs (a Windows Forms or WPF tool that exports the current catalogue to the user’s own Excel) and for a developer’s one-off script.

For a server, read and write the file formats directly. The .xlsx and .docx formats are zip packages of XML, and libraries exist that handle them without Office:

Library What it is Where on this site

Open XML SDK

Microsoft’s strongly typed .NET library for the Office Open XML formats (DocumentFormat.OpenXml on NuGet); works from Visual Basic and runs on Linux

the example below

Apache POI

the Java library for Word and Excel, with a detailed guide on this site; useful for the concepts of the formats and for Java back ends that exchange files with a .NET front end

Office Files with Apache POI

Those pages are written for Java, but the concepts carry across languages. The file formats page explains the OOXML zip package (parts, relationships, content types) and the older OLE2 format that the Open XML SDK also has to respect; the exporting reports from a server page describes the report pipeline (query, mapping, workbook, stream) and the download headers that any web server needs; the importing data from Excel page covers uploads, type coercion and per-row validation; and Security and untrusted files lists the risks of opening files that users upload, which apply to every library. The introduction and architecture page gives the overview.

This program writes the same two-row workbook as the Excel automation example, with no Excel, no COM and no Windows, then reads it back. It uses DocumentFormat.OpenXml 3.5.1 and ran on Linux:

Option Strict On

Imports System
Imports System.IO
Imports System.Linq
Imports DocumentFormat.OpenXml
Imports DocumentFormat.OpenXml.Packaging
Imports DocumentFormat.OpenXml.Spreadsheet

Namespace X1
    Friend Module Program
        Private Function TextCell(reference As String, text As String) As Cell
            Return New Cell With {.CellReference = reference, .DataType = CellValues.InlineString,
                                  .InlineString = New InlineString(New Text(text))}
        End Function

        Private Function NumberCell(reference As String, value As Integer) As Cell
            Return New Cell With {.CellReference = reference, .DataType = CellValues.Number,
                                  .CellValue = New CellValue(value.ToString(Globalization.CultureInfo.InvariantCulture))}
        End Function

        Friend Sub Main()
            Dim path As String = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "shelf-books-openxml.xlsx")

            ' write: no Excel, no COM, no Windows
            Using document As SpreadsheetDocument = SpreadsheetDocument.Create(path, SpreadsheetDocumentType.Workbook)
                Dim workbookPart As WorkbookPart = document.AddWorkbookPart()
                workbookPart.Workbook = New Workbook()
                Dim sheetPart As WorksheetPart = workbookPart.AddNewPart(Of WorksheetPart)()
                Dim data As New SheetData()
                data.Append(New Row(TextCell("A1", "ISBN"), TextCell("B1", "Title"), TextCell("C1", "Copies")) With {.RowIndex = 1UI})
                data.Append(New Row(TextCell("A2", "978-0-13-468599-1"), TextCell("B2", "The Pragmatic Programmer"), NumberCell("C2", 3)) With {.RowIndex = 2UI})
                sheetPart.Worksheet = New Worksheet(data)
                Dim sheets As Sheets = workbookPart.Workbook.AppendChild(New Sheets())
                sheets.Append(New Sheet With {.Id = workbookPart.GetIdOfPart(sheetPart), .SheetId = 1UI, .Name = "Books"})
                workbookPart.Workbook.Save()
            End Using
            Console.WriteLine("Wrote " & New FileInfo(path).Length.ToString() & " bytes")

            ' read it back
            Using document As SpreadsheetDocument = SpreadsheetDocument.Open(path, False)
                Dim sheet As Sheet = document.WorkbookPart.Workbook.Descendants(Of Sheet)().First()
                Dim part As WorksheetPart = CType(document.WorkbookPart.GetPartById(sheet.Id.Value), WorksheetPart)
                Console.WriteLine("Sheet '" & sheet.Name.Value & "':")
                For Each row As Row In part.Worksheet.Descendants(Of Row)()
                    Dim values As New System.Collections.Generic.List(Of String)
                    For Each c As Cell In row.Elements(Of Cell)()
                        values.Add(If(c.DataType IsNot Nothing AndAlso c.DataType.Value = CellValues.InlineString, c.InlineString.Text.Text, c.CellValue.Text))
                    Next
                    Console.WriteLine("  " & String.Join(" | ", values))
                Next
            End Using
            File.Delete(path)
        End Sub
    End Module
End Namespace
Wrote 1760 bytes
Sheet 'Books':
  ISBN | Title | Copies
  978-0-13-468599-1 | The Pragmatic Programmer | 3

Source: Open XML SDK

(The program uses inline strings to keep it short. A real writer would use a shared string table and a style sheet, as the Apache POI pages explain for the same format.)

Exposing a Visual Basic class to COM

The reverse direction publishes a .NET class so that VBA, VB6, a scripting host or a C++ program can create it by ProgID. A COM client sees only interfaces. There are two decisions.

ClassInterfaceType.None and an explicit interface

When a class is exposed, COM needs an interface to call. If you do nothing, the runtime can generate a "class interface" automatically. ClassInterfaceType.AutoDual is the generated dual interface that older tutorials recommend; it is fragile because adding a member changes the layout of the interface that existing clients compiled against. The recommended design is ClassInterfaceType.None with a hand-written interface that you version deliberately:

Option Strict On

Imports System.Runtime.InteropServices
Imports Microsoft.VisualBasic

' A hand-written COM class: explicit interface, no automatic class interface
<ComVisible(True), Guid("5D2A8E0C-3C1E-4C8E-9B53-7B2F3C0A2001"), InterfaceType(ComInterfaceType.InterfaceIsDual)>
Public Interface ICatalogDesk
    Function CountBooks() As Integer
    Function Describe(isbn As String) As String
End Interface

<ComVisible(True), Guid("5D2A8E0C-3C1E-4C8E-9B53-7B2F3C0A2002"), ProgId("Shelf.CatalogDesk"), ClassInterface(ClassInterfaceType.None)>
Public Class CatalogDesk
    Implements ICatalogDesk

    Public Function CountBooks() As Integer Implements ICatalogDesk.CountBooks
        Return 2
    End Function

    Public Function Describe(isbn As String) As String Implements ICatalogDesk.Describe
        Return "Book " & isbn
    End Function
End Class

' ComClass lets the compiler write the interfaces; ComVisible is still needed for the COM host
<ComVisible(True), ComClass(LoanDesk.ClassId, LoanDesk.InterfaceId, LoanDesk.EventsId)>
Public Class LoanDesk
    Public Const ClassId As String = "5D2A8E0C-3C1E-4C8E-9B53-7B2F3C0A1001"
    Public Const InterfaceId As String = "5D2A8E0C-3C1E-4C8E-9B53-7B2F3C0A1002"
    Public Const EventsId As String = "5D2A8E0C-3C1E-4C8E-9B53-7B2F3C0A1003"

    Public Sub New()
        MyBase.New()
    End Sub

    Public Function OpenLoans() As Integer
        Return 2
    End Function
End Class

Rules the example follows:

  • <ComVisible(True)> on the interface and the class; types are visible by default only in some project templates, so state it.

  • <Guid> on the interface (the IID) and on the class (the CLSID). On .NET (but not on .NET Framework) a class without a Guid is not activatable.

  • <ProgId("Shelf.CatalogDesk")> gives the human-readable name; without it the ProgID is the class’s full name.

  • <InterfaceType(ComInterfaceType.InterfaceIsDual)> makes the interface available to both vtable and IDispatch clients; InterfaceIsIUnknown is vtable only.

  • A public, parameterless constructor, and public methods with COM-compatible types.

ComClass

Visual Basic has a shortcut that C# lacks: the Microsoft.VisualBasic.ComClassAttribute. Given a class with three GUIDs, the compiler writes the interface for the public members, the events interface for the events, and the attributes. The program below ran on Linux and uses reflection to list what the compiler produced, next to a hand-written pair:

Option Strict On

Imports System
Imports System.Linq
Imports System.Reflection
Imports System.Runtime.InteropServices
Imports Microsoft.VisualBasic

Namespace C02
    ' ComClass makes the compiler write the COM interfaces and attributes for you
    <ComClass(LoanDesk.ClassId, LoanDesk.InterfaceId, LoanDesk.EventsId)>
    Public Class LoanDesk
        Public Const ClassId As String = "5D2A8E0C-3C1E-4C8E-9B53-7B2F3C0A1001"
        Public Const InterfaceId As String = "5D2A8E0C-3C1E-4C8E-9B53-7B2F3C0A1002"
        Public Const EventsId As String = "5D2A8E0C-3C1E-4C8E-9B53-7B2F3C0A1003"

        Public Event LoanOpened(isbn As String)

        Public Sub New()
            MyBase.New()
        End Sub

        Public Function OpenLoans() As Integer
            Return 2
        End Function

        Public Sub Open(isbn As String)
            RaiseEvent LoanOpened(isbn)
        End Sub
    End Class

    ' The hand-written alternative: an explicit interface and ClassInterfaceType.None
    <ComVisible(True), Guid("5D2A8E0C-3C1E-4C8E-9B53-7B2F3C0A2001"), InterfaceType(ComInterfaceType.InterfaceIsDual)>
    Public Interface ICatalogDesk
        Function CountBooks() As Integer
    End Interface

    <ComVisible(True), Guid("5D2A8E0C-3C1E-4C8E-9B53-7B2F3C0A2002"), ProgId("Shelf.CatalogDesk"), ClassInterface(ClassInterfaceType.None)>
    Public Class CatalogDesk
        Implements ICatalogDesk

        Public Function CountBooks() As Integer Implements ICatalogDesk.CountBooks
            Return 2
        End Function
    End Class

    Friend Module Program
        Private Function Show(a As CustomAttributeTypedArgument) As String
            If a.ArgumentType.IsEnum Then
                Return [Enum].ToObject(a.ArgumentType, a.Value).ToString()
            End If
            Return a.Value.ToString()
        End Function

        Private Sub Describe(t As Type, indent As String)
            Console.WriteLine(indent & t.Name)
            For Each a As CustomAttributeData In t.CustomAttributes
                Console.WriteLine(indent & "  [" & a.AttributeType.Name.Replace("Attribute", "") & "(" &
                    String.Join(", ", a.ConstructorArguments.Select(AddressOf Show)) & ")]")
            Next
            For Each nested As Type In t.GetNestedTypes(BindingFlags.Public Or BindingFlags.NonPublic).OrderBy(Function(x) x.Name)
                Describe(nested, indent & "    ")
            Next
        End Sub

        Friend Sub Main()
            Dim asm As Assembly = GetType(LoanDesk).Assembly
            For Each t As Type In asm.GetTypes().Where(Function(x) x.Namespace = "ShelfApp.C02" AndAlso x.IsPublic AndAlso Not x.IsNested).OrderBy(Function(x) x.Name)
                Describe(t, "")
            Next
        End Sub
    End Module
End Namespace
CatalogDesk
  [ComVisible(True)]
  [Guid(5D2A8E0C-3C1E-4C8E-9B53-7B2F3C0A2002)]
  [ProgId(Shelf.CatalogDesk)]
  [ClassInterface(None)]
ICatalogDesk
  [ComVisible(True)]
  [Guid(5D2A8E0C-3C1E-4C8E-9B53-7B2F3C0A2001)]
  [InterfaceType(InterfaceIsDual)]
LoanDesk
  [Guid(5D2A8E0C-3C1E-4C8E-9B53-7B2F3C0A1001)]
  [ClassInterface(None)]
  [ComSourceInterfaces(ShelfApp.C02.LoanDesk+__LoanDesk)]
  [ComClass(5D2A8E0C-3C1E-4C8E-9B53-7B2F3C0A1001, 5D2A8E0C-3C1E-4C8E-9B53-7B2F3C0A1002, 5D2A8E0C-3C1E-4C8E-9B53-7B2F3C0A1003)]
    __LoanDesk
      [Guid(5D2A8E0C-3C1E-4C8E-9B53-7B2F3C0A1003)]
      [InterfaceType(2)]
      [ComVisible(True)]
    _LoanDesk
      [Guid(5D2A8E0C-3C1E-4C8E-9B53-7B2F3C0A1002)]
      [ComVisible(True)]
    LoanOpenedEventHandler

What to read from the output:

  • LoanDesk has ClassInterface(None), ComSourceInterfaces pointing at the nested __LoanDesk interface and the ComClass attribute itself. The nested _LoanDesk interface (the GUID ending in 1002) holds the public methods and __LoanDesk (the GUID ending in 1003) holds the events; its InterfaceType(2) is ComInterfaceType.InterfaceIsIDispatch, which is the outgoing event interface.

  • The class itself has no ComVisible attribute. That matters for the .NET COM host (below): the SDK registers only types that say ComVisible(True), so a ComClass needs the attribute added by hand.

  • Only Public members are exposed. The Learn walkthrough gives every ComClass an explicit public parameterless constructor, as the example does; a ComClass with only a parameterized constructor compiled without any diagnostic, so check this yourself.

Misuse is diagnosed (each was built as its own program):

Code Message

BC32509

'Microsoft.VisualBasic.ComClassAttribute' cannot be applied to 'Hidden' because it is not declared 'Public'.

BC31527

'Microsoft.VisualBasic.ComClassAttribute' cannot be applied to a class that is generic or contained inside a generic type.

BC32500

'ComClassAttribute' cannot be applied because the format of the GUID 'not-a-guid' is not correct.

BC40011 (warning)

'Microsoft.VisualBasic.ComClassAttribute' is specified for class 'Hidden' but 'Hidden' has no public members that can be exposed to COM; therefore, no COM interfaces are generated.

The COM host

On .NET (and not on .NET Framework) a COM server is a small native DLL, the COM host, that loads the runtime and your assembly. The SDK builds it when the project says so:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <RootNamespace>Shelf.Com</RootNamespace>
    <AssemblyName>Shelf.Com</AssemblyName>
    <OptionStrict>On</OptionStrict>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <EnableComHosting>true</EnableComHosting>
    <EnableRegFreeCom>true</EnableRegFreeCom>
  </PropertyGroup>
</Project>

The CatalogDesk.vb file above is the whole project. Building it from the Linux container needs a runtime identifier, because the COM host exists only for Windows; without one the SDK stops with NETSDK1091: Unable to find a .NET Core COM host. The .NET Core COM host is only available on .NET Core 3.0 or higher when targeting Windows. With dotnet build -r win-x64 --no-self-contained it built with no warnings and produced:

Shelf.Com.X.manifest
Shelf.Com.comhost.dll
Shelf.Com.deps.json
Shelf.Com.dll
Shelf.Com.pdb
Shelf.Com.runtimeconfig.json

Shelf.Com.comhost.dll is the registrable server. Shelf.Com.X.manifest (from EnableRegFreeCom) is the side-by-side manifest for registration-free use. The build also writes an intermediate map from CLSIDs to types, which shows exactly which classes the host will activate:

{"{5d2a8e0c-3c1e-4c8e-9b53-7b2f3c0a2002}":{"type":"Shelf.Com.CatalogDesk","assembly":"Shelf.Com, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null","progid":"Shelf.CatalogDesk"},"{5d2a8e0c-3c1e-4c8e-9b53-7b2f3c0a1001}":{"type":"Shelf.Com.LoanDesk","assembly":"Shelf.Com, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null","progid":"Shelf.Com.LoanDesk"}}

and the manifest lists the same two classes:

<?xml version="1.0" encoding="utf-8" standalone="yes"?><assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"><assemblyIdentity type="win32" name="Shelf.Com.X" version="1.0.0.0" /><file name="Shelf.Com.comhost.dll"><comClass clsid="{5d2a8e0c-3c1e-4c8e-9b53-7b2f3c0a2002}" threadingModel="Both" progid="Shelf.CatalogDesk" /><comClass clsid="{5d2a8e0c-3c1e-4c8e-9b53-7b2f3c0a1001}" threadingModel="Both" progid="Shelf.Com.LoanDesk" /></file></assembly>

Two facts show in those listings. The ProgID of LoanDesk, which declared none, is its full name Shelf.Com.LoanDesk; CatalogDesk has the one it declared. And LoanDesk is listed only because the example adds <ComVisible(True)> to the ComClass: a first build without it listed CatalogDesk alone.

Registration and use, all on Windows, were not run:

rem register (elevated prompt); the host and the .dll, .deps.json and .runtimeconfig.json stay together
regsvr32 Shelf.Com.comhost.dll

rem unregister
regsvr32 /u Shelf.Com.comhost.dll

Source: regsvr32

Points to watch:

  • Bitness. The host is native, so a 32-bit client (32-bit Office, a VB6 program) needs a 32-bit host. By default the host has the bitness of the SDK that built it; build with the matching runtime identifier (-r win-x86 or -r win-x64). The Learn article on exposing .NET components to COM explains the default.

  • Framework-dependent only. The target machine needs the .NET runtime; self-contained COM servers are not supported.

  • No type library. Early-bound clients (VB6 references, VBA Dim d As Shelf.CatalogDesk) need a type library, and .NET does not generate one. Write an IDL file, compile it with the Windows SDK’s MIDL, and add the .tlb with a ComHostTypeLibrary item; late-bound CreateObject works without it.

  • Regasm and the registration check box do not apply. RegisterForComInterop, the property behind Visual Studio’s "Register for COM interop" check box, fails under dotnet build just like COMReference:

    Microsoft.Common.CurrentVersion.targets(5725,5): error MSB4803: The task "RegisterAssembly" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.
  • Assembly loading. By default the assembly loads into its own AssemblyLoadContext, so two COM servers in one process cannot share types.

Calling the class from VBA or VB6

From the client side this is ordinary late-bound COM: a VBA (Visual Basic for Applications, the macro language inside Office) or VB6 macro creates the object with CreateObject("Shelf.CatalogDesk") into an Object variable and calls CountBooks() on it. VBA is a different language from Visual Basic on .NET, and that client side was not run here.

The VBA/VB6 family and how its syntax differs from Visual Basic on .NET is described in The VB Family: VB6, VBA and VBScript.

Visual Basic versus C#

Topic Visual Basic C#

Optional parameters in COM calls

since Visual Basic 6.0, with named arguments

since C# 4.0

Indexed properties of COM objects

native: sheet.Range("A1"), sheets(1)

supported on COM types since C# 4.0

Late binding

Object with strict typing turned off for the file, or CallByName and InvokeMember

dynamic

Passing a value for a ref COM parameter

automatic temporary

automatic temporary for COM calls since C# 4.0

Set for objects

gone

not applicable

Exposing a class

<ComClass> generates interfaces; or explicit ComVisible, Guid, ClassInterface

only the explicit attributes

Embedded interop types

same COMReference items in either language

same

Compiler messages seen while writing this page

Code Where Message

BC30574

late-bound call under Option Strict On

Option Strict On disallows late binding.

BC30512

indexed property or Value2 assigned without a conversion

Option Strict On disallows implicit conversions from 'Object' to 'Worksheet'.

BC32029

String variable passed to a ByRef Object parameter

Option Strict On disallows narrowing from type 'Object' to type 'String' in copying the value of 'ByRef' parameter 'FileName' back to the matching argument.

BC30456

Marshal.GetActiveObject

'GetActiveObject' is not a member of 'Marshal'.

BC32509, BC31527, BC32500, BC40011

ComClass misuse

see the table above

MSB4803

COMReference or RegisterForComInterop with dotnet build

the task is not supported on the .NET Core version of MSBuild

NETSDK1091

EnableComHosting built without a Windows runtime identifier

Unable to find a .NET Core COM host

CA1416

CreateObject, GetObject, Type.GetTypeFromProgID, Marshal.FinalReleaseComObject, Marshal.IsComObject outside a Windows guard

This call site is reachable on all platforms. and the member is only supported on 'windows'

Where the books are outdated

Book habit Today on .NET

"COM interop does not work with .NET Core or .NET 5 and later"

it works on Windows, in both directions: clients with interop assemblies or late binding, servers with EnableComHosting

Registering with Regasm /codebase

Regasm belongs to .NET Framework; use regsvr32 on the generated comhost.dll, or a registration-free manifest

ClassInterfaceType.AutoDual for convenience

use ClassInterfaceType.None and an explicit, versioned interface

Adding a COM reference in the designer and building with the command line

COMReference needs the .NET Framework MSBuild; use a NuGet interop package or build with Visual Studio’s MSBuild

Marshal.GetActiveObject to attach to a running application

not in .NET; use GetObject

Running Excel or Word on the web server to produce reports

not supported by Microsoft; use the Open XML SDK or another library

Pitfalls checklist

  • Guard every COM call with OperatingSystem.IsWindows() and <SupportedOSPlatform("windows")>; on other systems the calls return Nothing or throw.

  • Put <STAThread> on Main, and keep one COM object on one thread.

  • Never chain member calls on COM objects; assign each wrapper to a variable and release it with Marshal.FinalReleaseComObject in Finally.

  • Close the document and call Quit before releasing the application object.

  • Pass Object variables, not String variables, to ByRef Object parameters.

  • Do not use Office automation on a server. Use the Open XML SDK or a similar library.

  • Give every class you expose a Guid, use ClassInterfaceType.None, and add <ComVisible(True)> to a ComClass.

  • Match the bitness of the COM host to the client, and install the .NET runtime on the target machine.

  • COMReference and RegisterForComInterop need the .NET Framework MSBuild; they fail under dotnet build (MSB4803).