How to Use a .NET 8 DLL from VBA with Type Safety - Expose via COM and Generate a TLB with dscom

· · C#, .NET 8, VBA, COM, Office, dscom

The short version

Here is the flow for using a .NET 8 DLL from VBA with type safety (early binding):

  1. build the .NET 8 class library with EnableComHosting=true
  2. define explicit COM-facing interfaces and classes
  3. mark the class as ClassInterfaceType.None and the interface as InterfaceIsDual
  4. after building, generate the TLB with dscom tlbexport
  5. register *.comhost.dll with regsvr32
  6. register the TLB with dscom tlbregister
  7. add a reference in VBA and use it with full type info

The COM entry point is *.comhost.dll, the type information lives in *.tlb, and VBA reads that TLB to do early binding. That is the whole shape of it.

The big picture

File Role
VbaTypedComSample.dll the actual .NET 8 implementation
VbaTypedComSample.comhost.dll the entry point COM calls into
VbaTypedComSample.tlb the type information VBA reads
VbaTypedComSample.deps.json dependency resolution info
VbaTypedComSample.runtimeconfig.json .NET runtime startup info

Decide this first: match 32-bit / 64-bit

If the bitness of Office/VBA and your COM server do not match, you will get the classic “ActiveX component can’t create object” family of errors.

Consumer .NET side TLB generation Registration command
64-bit Office x64 / win-x64 dscom C:\Windows\System32\regsvr32.exe
32-bit Office x86 / win-x86 dscom32.exe C:\Windows\SysWOW64\regsvr32.exe

Building the .NET 8 side

.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <EnableComHosting>true</EnableComHosting>
    <PlatformTarget>x64</PlatformTarget>
    <NETCoreSdkRuntimeIdentifier>win-x64</NETCoreSdkRuntimeIdentifier>
  </PropertyGroup>
</Project>

The key bit is EnableComHosting=true. With that flag the build also produces *.comhost.dll.

Make the assembly non-COM-visible by default

using System.Runtime.InteropServices;
[assembly: ComVisible(false)]

The interface and class you actually expose

namespace VbaTypedComSample;

[ComVisible(true)]
[Guid("2A1BBEDE-DE6E-4C34-AD60-2E9E0E33E999")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface ICalculator
{
    [DispId(1)] int Add(int x, int y);
    [DispId(2)] double Divide(double x, double y);
    [DispId(3)] string Hello(string name);
}

[ComVisible(true)]
[Guid("FAD1C752-0BB6-4DDD-889F-FE446350847A")]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(ICalculator))]
public class Calculator : ICalculator
{
    public int Add(int x, int y) => checked(x + y);
    public double Divide(double x, double y) { /* divide-by-zero check */ }
    public string Hello(string name) { /* handles empty strings */ }
}

Things to keep in mind

  • give the interface and the class their own separate Guids
  • use ClassInterfaceType.None so you are not relying on AutoDual
  • the interface VBA sees should be InterfaceIsDual
  • assigning DispId values up front limits the damage if methods get reordered later
  • you need a public parameterless constructor (COM news up the object that way)

Build and generate the TLB

dotnet build -c Release
dotnet tool install --global dscom
dscom tlbexport .\bin\Release\net8.0-windows\VbaTypedComSample.dll --out .\bin\Release\net8.0-windows\VbaTypedComSample.tlb

For 32-bit Office, swap in dscom32.exe.

Register the COM host and TLB (run elevated)

# 64-bit Office
$out = Resolve-Path .\bin\Release\net8.0-windows
C:\Windows\System32\regsvr32.exe "$out\VbaTypedComSample.comhost.dll"
dscom tlbregister "$out\VbaTypedComSample.tlb"

Using it from VBA

  1. open Excel/Access and switch to the VBA editor
  2. Tools -> References -> tick VbaTypedComSample.tlb
  3. write the code:
Option Explicit

Public Sub UseCalculator()
    Dim calc As VbaTypedComSample.ICalculator
    Set calc = New VbaTypedComSample.Calculator

    Debug.Print calc.Add(10, 20)
    Debug.Print calc.Divide(10, 4)
    Debug.Print calc.Hello("VBA")
End Sub

IntelliSense lights up, and method-name typos surface before you ever hit Run.

Exceptions surface as COM errors on the VBA side

Public Sub UseWithErrorHandling()
    On Error GoTo EH
    Dim calc As VbaTypedComSample.ICalculator
    Set calc = New VbaTypedComSample.Calculator
    Debug.Print calc.Divide(10, 0)
    Exit Sub
EH:
    Debug.Print Err.Number, Err.Description
End Sub

Files you actually need to ship

VbaTypedComSample.dll
VbaTypedComSample.comhost.dll
VbaTypedComSample.deps.json
VbaTypedComSample.runtimeconfig.json
VbaTypedComSample.tlb

The client PC also needs the matching .NET 8 runtime installed.

Gotchas

  1. don’t leave it on AnyCPU - x64 for 64-bit Office, x86 for 32-bit Office
  2. avoid ClassInterfaceType.AutoDual - it breaks easily when member order changes after release
  3. don’t regenerate GUIDs casually - in COM the GUID is the contract; changing it breaks every existing reference
  4. don’t break a published interface - if you need bigger changes, add a new ICalculator2 instead
  5. keep types boring at the boundary - int, double, bool, string, DateTime, decimal, and enum are the safe set across the VBA boundary
  6. don’t update while Office is open - the DLL stays locked and your build or re-register will fail

Wrap-up

The trick to .NET 8 + VBA interop is to think of the COM host and the TLB as two separate things.

  • entry point: *.comhost.dll
  • type information: *.tlb
  • actual implementation: *.dll

Once you internalize the pipeline - EnableComHosting=true -> dscom tlbexport -> regsvr32 plus dscom tlbregister -> VBA reference - you can ship type-safe interop without surprises.

References

Related Articles

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

Related Topics

These topic pages place the article in a broader service and decision context.

Where This Topic Connects

This article connects naturally to the following service pages.

Back to the Blog