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 |
|
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 |
|
Registration |
none; the COM library is registered by its installer |
|
Source: Introduction to COM Interop
| Task | .NET Framework | .NET 10 on Windows |
|---|---|---|
Reference a COM library |
Add Reference > COM, which writes a |
a NuGet interop package, or |
Embed interop types (no-PIA) |
|
same, on |
Expose a class |
|
|
Type library for the server |
|
none is generated; write an IDL file and embed the compiled library |
CLSID for a class |
derived from the type when |
|
Automatic class interface |
|
use |
Self-contained deployment of the server |
not applicable, the Framework is installed system-wide |
not supported; framework-dependent only |
Source: Expose .NET components to COM
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.
Source: Interaction.CreateObject
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) andMicrosoft.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 normalPackageReference:<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
COMReferenceitem, which asks MSBuild to runtlbimpagainst the registered type library and, withEmbedInteropTypes, 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 buildcommand 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-windowsproject. 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
tlbimpon 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 ofSaveAs. In older C# code this needed a column ofType.Missing. -
Indexed properties.
sheets(1)calls the default indexed property ofSheets. It returnsObject, soOption Strict OnrequiresCType(sheets(1), Excel.Worksheet), andsheet.Range("A1").Value2is anObjectas 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 Modulegets 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=;Setis Visual Basic 6.0 syntax. -
ByRef Objectparameters. Many Office methods takeref object. Passing aStringvariable where the parameter isByRef Objectis rejected because the callee could store another type back into the variable; pass anObjectvariable or a literal. Word’sSaveAs2shows 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 = pathfor that reason. -
A thread in a single-threaded apartment. Office objects expect to be called from an STA thread;
<STAThread>onMainselects it (see Apartments and threads).
Source: Early and late binding
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'.
Source: Interaction.CallByName
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, preferFinalReleaseComObject. -
Do not chain.
app.Workbooks.Add().Worksheets(1)creates wrappers forWorkbooksand 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 inFinally. -
Release(Of T)in the Excel example is a helper; after it runs the variable isNothing, so a second call is harmless. -
Release after
Quit, not before:book.Close(SaveChanges:=False)andapp.Quit()are calls on live wrappers. -
A
For Eachover a COM collection also creates an enumerator and a wrapper per item; index withFor i = 1 To countand release each element when the collection is large. -
Forcing
GC.Collect()followed byGC.WaitForPendingFinalizers()is a last resort for code you cannot restructure; it does not replace explicit releases.
Source: Marshal.FinalReleaseComObject
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
Mainand call it fromTask.Run, a thread-pool continuation afterAwait, or aParallel.Forbody. -
If you need background work, start a dedicated thread, call
SetApartmentState(ApartmentState.STA)beforeStart, 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 ( |
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 |
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 aGuidis 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 andIDispatchclients;InterfaceIsIUnknownis vtable only. -
A public, parameterless constructor, and public methods with COM-compatible types.
Source: ClassInterfaceAttribute
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
Source: ComClassAttribute
What to read from the output:
-
LoanDeskhasClassInterface(None),ComSourceInterfacespointing at the nested__LoanDeskinterface and theComClassattribute itself. The nested_LoanDeskinterface (the GUID ending in1002) holds the public methods and__LoanDesk(the GUID ending in1003) holds the events; itsInterfaceType(2)isComInterfaceType.InterfaceIsIDispatch, which is the outgoing event interface. -
The class itself has no
ComVisibleattribute. That matters for the .NET COM host (below): the SDK registers only types that sayComVisible(True), so aComClassneeds the attribute added by hand. -
Only
Publicmembers are exposed. The Learn walkthrough gives everyComClassan explicit public parameterless constructor, as the example does; aComClasswith 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 |
|
BC31527 |
|
BC32500 |
|
BC40011 (warning) |
|
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-x86or-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.tlbwith aComHostTypeLibraryitem; late-boundCreateObjectworks 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 underdotnet buildjust likeCOMReference: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: |
supported on COM types since C# 4.0 |
Late binding |
|
|
Passing a value for a |
automatic temporary |
automatic temporary for COM calls since C# 4.0 |
|
gone |
not applicable |
Exposing a class |
|
only the explicit attributes |
Embedded interop types |
same |
same |
Compiler messages seen while writing this page
| Code | Where | Message |
|---|---|---|
BC30574 |
late-bound call under |
|
BC30512 |
indexed property or |
|
BC32029 |
String variable passed to a |
|
BC30456 |
|
|
BC32509, BC31527, BC32500, BC40011 |
|
see the table above |
MSB4803 |
|
the task is not supported on the .NET Core version of MSBuild |
NETSDK1091 |
|
Unable to find a .NET Core COM host |
CA1416 |
|
|
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 |
Registering with |
Regasm belongs to .NET Framework; use |
|
use |
Adding a COM reference in the designer and building with the command line |
|
|
not in .NET; use |
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 returnNothingor throw. -
Put
<STAThread>onMain, 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.FinalReleaseComObjectinFinally. -
Close the document and call
Quitbefore releasing the application object. -
Pass
Objectvariables, notStringvariables, toByRef Objectparameters. -
Do not use Office automation on a server. Use the Open XML SDK or a similar library.
-
Give every class you expose a
Guid, useClassInterfaceType.None, and add<ComVisible(True)>to aComClass. -
Match the bitness of the COM host to the client, and install the .NET runtime on the target machine.
-
COMReferenceandRegisterForComInteropneed the .NET Framework MSBuild; they fail underdotnet build(MSB4803).
References
-
COM Interop (Visual Basic), Introduction to COM Interop and Walkthrough: Creating COM Objects with Visual Basic
-
Runtime callable wrapper, COM callable wrapper and Type equivalence and embedded interop types
-
Interaction.CreateObject, Interaction.GetObject and Interaction.CallByName
-
ComClassAttribute, ComVisibleAttribute, GuidAttribute and ClassInterfaceAttribute
-
Open XML SDK and DocumentFormat.OpenXml and Microsoft.Office.Interop.Excel on NuGet
-
Native Interop: Declare and P/Invoke, Early and Late Binding and The VB Family: VB6, VBA and VBScript in this section
-
Office Files (Word and Excel with Apache POI) and its file formats, export, import, security and architecture pages