Using a .NET 8 DLL from VBA with Full Typing - COM Exposure and dscom TLB

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

There are still plenty of situations where you want to call .NET 8 code from VBA. In particular, when you want to keep your existing Excel or Access assets as they are, while offloading just the heavy parts - string processing, HTTP, cryptography, business logic - to C#.

However, if you lean on late binding via CreateObject, the VBA side ends up littered with Object. IntelliSense becomes weak, typos in method names go unnoticed until runtime, and you gradually sink into a swamp of string-based plumbing.

So this time, we focus on exposing a .NET 8 DLL to COM, generating a type library (TLB) with dscom, and consuming it from VBA with typed early binding.

We will set aside the old .NET Framework + RegAsm story, hand-writing IDL and compiling it with MIDL, and Reg-Free COM. Here we cover only the single path of .NET 8 / COM host / dscom / VBA early binding.

All the code in this article is published on GitHub as a complete, buildable, verifiable sample set (the COM-exposed library, TLB generation and registration scripts, VBA modules, and unit tests).

dotnet8-dll-typed-vba-com-dscom-tlb - komurasoft-blog-samples (GitHub)

1. The Conclusion First

Laying out the conclusion up front, the flow is as follows.

  • Build a .NET 8 class library with EnableComHosting=true
  • Create an explicit interface and a class to expose to COM
  • Set the class to ClassInterfaceType.None; do not fall back on AutoDual
  • Make the interface consumed from VBA InterfaceIsDual
  • From the built *.dll, generate a *.tlb with dscom tlbexport
  • Register the *.comhost.dll with regsvr32
  • Register the *.tlb with dscom tlbregister
  • In VBA, add a reference and use it with full typing, like Dim x As LibraryName.IYourInterface

In short, the setup is: the COM entry point is the *.comhost.dll produced by the .NET SDK, the type information is the *.tlb produced by dscom, and VBA early-binds against that TLB.

2. The Big Picture

First, let’s see what plays which role in a single diagram.

type info from the referenced TLBCOM callsVBA / Excel / AccessVbaTypedComSample.tlbVbaTypedComSample.comhost.dllVbaTypedComSample.dll (.NET 8).NET 8 Runtime

The roles are as follows.

File Role
VbaTypedComSample.dll The .NET 8 implementation itself
VbaTypedComSample.comhost.dll The entry point called from COM
VbaTypedComSample.tlb The type information VBA sees
VbaTypedComSample.deps.json Dependency resolution information
VbaTypedComSample.runtimeconfig.json .NET runtime startup information

What matters here is that the TLB is what VBA needs in order to know the types, and the comhost is what is needed as the COM activation entry point.

The fact that you cannot just hand over a single .dll and call it a day is one of the less straightforward aspects of the COM world.

3. Decide This First - Match 32-bit / 64-bit

Get this wrong, and you will very likely roll downhill toward ActiveX component can't create object.

Make sure the bitness of Office / VBA and the COM server match.

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

With the COM host in .NET 5+, leaving the project as AnyCPU tends to push the *.comhost.dll toward the 64-bit side, which may not mesh with 32-bit Office. So it is safer to explicitly specify x86 / x64 to match your Office installation.

The code in this article uses 64-bit Office as the example. For 32-bit Office, read the x64 that appears later as x86, and win-x64 as win-x86.

4. Building the .NET 8 Side

Here we build a minimal sample where VBA can call Add, Divide, and Hello.

4.1 .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 point is EnableComHosting. With this set, VbaTypedComSample.comhost.dll is generated at build time.

4.2 Keep the Whole Assembly COM-Invisible by Default

Since we only want the types we expose to COM marked ComVisible(true), the easy approach is to set the whole assembly to false.

using System.Runtime.InteropServices;

[assembly: ComVisible(false)]

4.3 Write the Interface and Class to Expose

using System.Runtime.InteropServices;

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 Calculator()
    {
    }

    public int Add(int x, int y) => checked(x + y);

    public double Divide(double x, double y)
    {
        if (y == 0)
        {
            throw new ArgumentOutOfRangeException(nameof(y), "Cannot divide by zero.");
        }

        return x / y;
    }

    public string Hello(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            return "Hello";
        }

        return $"Hello, {name}";
    }
}

The points worth noting in this code are the following.

  • Assign separate Guids to the interface and the class
  • Use ClassInterfaceType.None so you do not depend on an auto-generated class interface
  • Use InterfaceIsDual so it is easy to work with from VBA
  • Assigning DispIds helps reduce accidents when the method order is changed after publication
  • COM will New the class, so provide a public parameterless constructor

5. Building

Do a Release build.

dotnet build -c Release

After the build, the output folder contains at least the following files.

bin/
  Release/
    net8.0-windows/
      VbaTypedComSample.dll
      VbaTypedComSample.comhost.dll
      VbaTypedComSample.deps.json
      VbaTypedComSample.runtimeconfig.json

This folder is what you use for deployment and registration. If you move things to a different location later, you have to redo the registration.

6. Generating the TLB with dscom

6.1 For 64-bit

First, install dscom.

dotnet tool install --global dscom

Then generate the TLB from the built assembly.

dscom tlbexport .\bin\Release\net8.0-windows\VbaTypedComSample.dll --out .\bin\Release\net8.0-windows\VbaTypedComSample.tlb

6.2 For 32-bit Office

This part is a small trap. If you are generating a TLB for 32-bit Office, it is safer to use dscom32.exe.

.\tools\dscom32.exe tlbexport .\bin\Release\net8.0-windows\VbaTypedComSample.dll --out .\bin\Release\net8.0-windows\VbaTypedComSample.tlb

7. Registering the COM Host and TLB

Run this from an elevated (administrator) Command Prompt / PowerShell.

7.1 For 64-bit Office / 64-bit COM

$out = Resolve-Path .\bin\Release\net8.0-windows

C:\Windows\System32\regsvr32.exe "$out\VbaTypedComSample.comhost.dll"
dscom tlbregister "$out\VbaTypedComSample.tlb"

7.2 For 32-bit Office (on 64-bit Windows)

$out = Resolve-Path .\bin\Release\net8.0-windows

C:\Windows\SysWOW64\regsvr32.exe "$out\VbaTypedComSample.comhost.dll"
.\tools\dscom32.exe tlbregister "$out\VbaTypedComSample.tlb"

Two things are happening here.

  • regsvr32 registers the *.comhost.dll as a COM server
  • tlbregister registers the *.tlb as a type library

8. Adding the Reference in VBA and Using It with Typing

  1. Open Excel or Access
  2. Open the VBA editor
  3. Tools -> References
  4. If the library appears in the list, check it
  5. If it does not appear in the list, choose VbaTypedComSample.tlb via Browse...
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

With this, the VBA side gets the following benefits.

  • IntelliSense works
  • Typos in method names are easier to catch before execution
  • The public API can be inspected in the Object Browser
  • More readable than writing raw Object everywhere

8.1 Exceptions Surface as COM Errors on the VBA Side

For example, when an exception is thrown on the .NET side, as with Divide(10, 0), it appears as a COM error on the VBA side.

Option Explicit

Public Sub UseCalculatorWithErrorHandling()
    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
    Debug.Print Err.Description
End Sub

9. How to Think About Deployment

The important thing at deployment time is to ship the full output set, not just the DLL by itself.

VbaTypedComSample.dll
VbaTypedComSample.comhost.dll
VbaTypedComSample.deps.json
VbaTypedComSample.runtimeconfig.json
VbaTypedComSample.tlb
(plus any dependency DLLs if needed)

In addition, the client PC needs the corresponding .NET 8 runtime. The COM host does not work as a self-contained deployment; in practice it has to be operated as framework-dependent.

10. Pitfalls

10.1 Do Not Leave It on AnyCPU

If the bitness of VBA / Office and the bitness of the COM host get out of sync, things fail in rather unpleasant ways.

  • For 64-bit Office: x64 / win-x64
  • For 32-bit Office: x86 / win-x86

10.2 Do Not Use ClassInterfaceType.AutoDual

It looks convenient at first glance, but it breaks easily once you touch member order or composition after publication.

If you want stable, typed use from VBA, the established practice is to define explicit interfaces and set the class to ClassInterfaceType.None.

10.3 Do Not Regenerate GUIDs Carelessly

In COM, the GUID is the contract itself. Carelessly swapping out an IID or CLSID after publication breaks existing VBA references and registrations.

10.4 Do Not Break a Published Interface

In COM, even “just adding one method later” does not always end peacefully.

  • Keep ICalculator as is
  • If the change is substantial, introduce a new ICalculator2
  • The class may implement both

10.5 Keep the Types Plain

At the boundary exposed to VBA, it is safer not to get fancy.

The types that play well are, to start with, around here.

  • int
  • double
  • bool
  • string
  • DateTime
  • decimal
  • enum

10.6 Do Not Update While Office Is Open

Excel or Access can keep a hold on the DLL, causing trouble during builds or re-registration.

  • Close Office
  • Unregister if necessary
  • Rebuild
  • Register again

11. Summary

The topic of using a .NET 8 DLL from VBA with full typing is not such a scary procedure once you narrow it down to COM exposure + TLB generation with dscom. On the .NET 8 side, set EnableComHosting=true and prepare explicit interfaces (class set to ClassInterfaceType.None, the VBA-facing interface set to InterfaceIsDual), generate the TLB with dscom tlbexport, register the *.comhost.dll with regsvr32 and the *.tlb with dscom tlbregister. After that, just add the reference in VBA and use early binding.

When in doubt, the trick is to think of the COM host and the TLB separately.

  • The activation entry point is the *.comhost.dll
  • The type information is the *.tlb
  • The implementation itself is the *.dll

12. References

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

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

This article connects naturally to the following service pages.

Windows App Development

Designing the integration surface across VBA, COM, Office, .NET 8, and type library generation is closely tied to Windows application development, so this topic pairs well with our Windows app development service.

Technical Consulting & Design Review

If you want to sort out the boundary design between existing VBA assets and .NET 8 - including bitness, registration, TLB generation, and deployment strategy - this works well as a technical consulting and design review engagement.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog