Revision history (1 updates, last updated Sep 1, 2026)
A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.
- Retranslated as a full translation of the Japanese original. The previous English version was an abridgement that carried only part of the source, so sections, tables, Mermaid diagrams, figure captions and FAQ entries were missing. All of them have been restored to match the Japanese original, and the technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.21614519)
- First published
Cite this article(DOI: 10.5281/zenodo.21614518)
This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.
Go Komura (2026). Using a .NET 8 DLL from VBA with Full Typing - COM Exposure and dscom TLB. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21614518 https://comcomponent.com/en/blog/2026/03/16/007-dotnet8-dll-typed-vba-com-dscom-tlb/
- DOI (latest version)
- 10.5281/zenodo.21614518
- DOI (this version)
- 10.5281/zenodo.22217155
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 moving only the parts you want to hand off - heavy computation, string processing, HTTP, cryptography, and business logic - over 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.
flowchart TB
accTitle: The late binding swamp
accDescr: Diagram showing that leaning on CreateObject for late binding fills the VBA side with Object, weakens IntelliSense, and lets method name typos go unnoticed until runtime.
lb1["Late binding with CreateObject"] --> lb2["The VBA side is full of Object"]
lb2 --> lb3["IntelliSense gets weak"]
lb2 --> lb4["Typos surface only at runtime"]
Figure 1: The more you lean on late binding, the deeper you 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)
What You Need
| Item | What is required |
|---|---|
| OS | Windows. COM registration is involved, so you must be able to run regsvr32 as an administrator |
| .NET SDK | The .NET 8 SDK. EnableComHosting is a .NET 5 and later feature |
| Office | Excel or Access. Check whether yours is the 32-bit or the 64-bit edition first (section 3) |
| TLB generation tool | dscom. The 64-bit and 32-bit builds are obtained in different ways (section 6) |
| Client PC | A .NET 8 runtime with the same bitness as Office (section 9) |
Before starting the procedure, I strongly recommend writing down the versions in your own environment. When you later run into “the same steps, but it does not work”, this is the only information you will have to compare against.
# The list of .NET SDKs and runtimes (it also shows whether x64 or x86 is installed)
dotnet --info
# The Windows build number
winver
You can check the edition and bitness of Office from File > Account > About Excel in Excel. The end of the dialog title line reads either 32-bit or 64-bit.
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 onAutoDual - Make the interface consumed from VBA
InterfaceIsDual - From the built
*.dll, generate a*.tlbwithdscom tlbexport - Register the
*.comhost.dllwithregsvr32 - Register the
*.tlbwithdscom 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.
flowchart TB
accTitle: The single path to typed use
accDescr: Diagram showing the flow of building with EnableComHosting, generating a TLB with dscom tlbexport, registering the comhost with regsvr32, registering the TLB with dscom tlbregister, and using it with full typing from a VBA reference.
st1["Build with EnableComHosting"] --> st2["Generate the TLB with dscom tlbexport"]
st2 --> st3["Register the comhost with regsvr32"]
st3 --> st4["Register the TLB with dscom tlbregister"]
st4 --> st5["Add the reference in VBA and use it typed"]
Figure 2: Build, generate the TLB, perform the two registrations, add the reference - follow that order and you can call it with full typing.
Knowledge map for this article
Using a .NET 8 class library from VBA in a typed way requires a type library, the COM type information, and a tool called dscom generates and registers it as the successor to tlbexp.exe and RegAsm.exe, which were discontinued with .NET Framework. On the .NET side, a COM host built with EnableComHosting serves as the COM activation entry point, and registration with regsvr32 and a bitness match with Office are prerequisites. On the VBA side, loading the type library through a reference makes early binding available, which is more type-safe than late binding with CreateObject. Compatibility after release depends on how IIDs and CLSIDs are handled and on which ClassInterfaceType is chosen, and the combination of ClassInterfaceType.None, InterfaceIsDual, and DispId is the standard way to avoid breaking VBA references.
flowchart LR
accTitle: Using a .NET 8 DLL from VBA with typed access
accDescr: Diagram showing that VBA needs a type library to use COM in a typed way with early binding, that dscom generates and registers that TLB, that the .NET 8 side is exposed as a COM host, and how ClassInterfaceType and the handling of IIDs and CLSIDs affect the compatibility of VBA references
vba["VBA (Visual Basic for Applications)"]
dscom["dscom"]
type_library["Type Library (TLB)"]
com_early_binding["Early binding (VBA)"]
com_late_binding["Late binding (CreateObject)"]
tlbexp_regasm["tlbexp.exe / RegAsm.exe"]
comhost["COM host (*.comhost.dll)"]
regsvr32["regsvr32"]
dotnet[".NET (Core and Later)"]
com["COM (Component Object Model)"]
iid["IID (Interface Identifier)"]
clsid["CLSID (Class ID)"]
vba_reference_break["Broken VBA References and Registration"]
classinterfacetype_autodual["ClassInterfaceType.AutoDual"]
classinterfacetype_none["ClassInterfaceType.None"]
dispid_attribute["DispIdAttribute"]
interface_is_dual["InterfaceIsDual (Dual Interface)"]
hresult["HRESULT"]
dotnet_exception[".NET Exceptions"]
com_visible_attribute["ComVisibleAttribute"]
bitness_match_requirement["Bitness Match Requirement"]
vba -.->|"requires"| type_library
com_early_binding -->|"requires"| type_library
vba -->|"uses"| com_early_binding
vba -->|"uses"| com_late_binding
com_late_binding -.->|"not recommended for"| vba
dscom -->|"implements"| type_library
dscom -->|"successor to"| tlbexp_regasm
type_library -.->|"configured by"| dscom
comhost -->|"configured by"| regsvr32
comhost -->|"requires"| dotnet
comhost -->|"implements"| com
vba -.->|"uses"| com
com -->|"requires"| iid
com -->|"requires"| clsid
iid -.->|"may cause"| vba_reference_break
clsid -.->|"may cause"| vba_reference_break
classinterfacetype_autodual -.->|"may cause"| vba_reference_break
classinterfacetype_autodual -->|"not recommended for"| vba
classinterfacetype_none -->|"recommended for"| vba
dispid_attribute -->|"mitigates"| vba_reference_break
interface_is_dual -->|"recommended for"| vba
com -->|"uses"| hresult
dotnet_exception -.->|"verified by"| hresult
dotnet -->|"configured by"| com_visible_attribute
comhost -->|"requires"| bitness_match_requirement
dscom -.->|"requires"| bitness_match_requirement
regsvr32 -->|"requires"| bitness_match_requirement
In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (27 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle
2. The Big Picture
First, let’s see what plays which role in a single diagram.
flowchart LR
VBA["VBA / Excel / Access"] -->|type info from the referenced TLB| TLB["VbaTypedComSample.tlb"]
VBA -->|COM calls| COMHOST["VbaTypedComSample.comhost.dll"]
COMHOST --> DOTNET["VbaTypedComSample.dll (.NET 8)"]
DOTNET --> RUNTIME[".NET 8 Runtime"]
Figure 3: VBA gets its type information from the TLB and calls the .NET 8 implementation through the comhost.
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 | Suggested .NET target | 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.
flowchart TB
accTitle: The mismatch that AnyCPU invites
accDescr: Diagram showing that leaving the project as AnyCPU tends to build the comhost for 64-bit, where it may not mesh with 32-bit Office, so specifying x86 or x64 to match Office is the safer choice.
b1["Left as AnyCPU"] --> b2["The comhost tends to be built as 64-bit"]
b2 --> b3["Does not mesh with 32-bit Office"]
b3 -.-> b4["Specify the bitness to match Office"]
Figure 4: A bitness mismatch is the shortest route to ActiveX component can’t create object.
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.Noneso you do not depend on an auto-generated class interface - Use
InterfaceIsDualso it is easy to work with from VBA - Assigning
DispIds means fewer things go wrong when the method order is changed after publication - COM will
Newthe class, so provide a public parameterless constructor
flowchart TB
accTitle: How to assemble the exposed types
accDescr: Diagram showing the structure where the explicit interface ICalculator carries InterfaceIsDual and DispId, the class Calculator implements it with ClassInterfaceType.None, and separate Guids are assigned to the interface and the class.
if1["ICalculator (explicit interface)"] --> d1["InterfaceIsDual and DispId"]
cl1["Calculator (class)"] -->|"implements"| if1
cl1 --> d2["ClassInterfaceType.None"]
if1 -.-> g1["Assign a separate Guid to each"]
cl1 -.-> g1
Figure 5: The core of typed exposure is the pairing of an explicit interface with a class set to None.
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 What dscom Is
dscom is an open source command line tool for generating and registering COM type libraries (TLBs) from .NET assemblies. It is published by dSPACE and licensed under Apache-2.0.
Why is it needed? Because tlbexp.exe and RegAsm.exe were removed in .NET 5 and later. In the .NET Framework era, those two handled TLB generation and assembly registration, but .NET 5+ ships no successor for them. dscom is the tool built to fill that gap.
flowchart TB
accTitle: The gap that dscom fills
accDescr: Diagram showing that tlbexp.exe and RegAsm.exe handled TLB generation and assembly registration in the .NET Framework era, that both were removed in .NET 5 and later with no successor in the box, and that dscom fills that gap.
old1["The .NET Framework era"] --> old2["tlbexp.exe and RegAsm.exe"]
new1[".NET 5 and later"] --> new2["Both removed, no successor"]
new2 --> ds1["dscom fills the gap"]
Figure 6: From .NET 5 onward, the tool for generating a TLB has been replaced by dscom.
These are the only subcommands you need to remember.
| Subcommand | Role |
|---|---|
tlbexport |
Writes out a TLB from an assembly |
tlbregister |
Registers a TLB with the system |
tlbunregister |
Unregisters a TLB |
tlbdump |
Dumps the contents of a TLB so you can inspect it |
tlbembed |
Embeds a TLB into a file |
tlbdump is handy for checking that the generated TLB contains the types you intended, before you even open VBA.
6.2 For 64-bit
If all you need is a 64-bit TLB, you can install it as a dotnet tool.
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.3 For 32-bit Office - Where to Get dscom32.exe
This is where support for 32-bit Office gets stuck most often.
The dscom you get from dotnet tool install can only handle AnyCPU or 64-bit assemblies, and the only TLB it can produce is a 64-bit one. To build a 32-bit TLB, you need a separate executable, dscom32.exe, and it is downloaded from the GitHub releases page rather than from NuGet.
- Where to get it: https://github.com/dspace-group/dscom/releases
dscom.exe… builds a 64-bit TLB from an AnyCPU or 64-bit assemblydscom32.exe… builds a 32-bit TLB from an AnyCPU or 32-bit assembly
In the examples in this article, the downloaded dscom32.exe is placed in a tools folder directly under the project. You can put it wherever you like, but do not ship it together with the build output. It is a development-time tool and is not needed at run time.
There is one more prerequisite that is easy to overlook. To run dscom32.exe, the x86 build of the .NET runtime must be installed. This comes from how dscom loads hostfxr.dll, and it will not run on a machine that only has the x64 build. Check the list in the output of dotnet --info to see whether an x86 runtime is present.
flowchart TB
accTitle: Getting ready to build a 32-bit TLB
accDescr: Diagram showing that the dscom installed as a dotnet tool can only build 64-bit TLBs, so a 32-bit TLB is built with dscom32.exe obtained from the GitHub releases page, which in turn requires the x86 build of the .NET runtime.
p1["A 32-bit TLB is needed"] --> p2["Get dscom32.exe from the releases page"]
p2 --> p3["Check whether an x86 runtime is present"]
p3 --> p4["Run tlbexport with dscom32.exe"]
p1 -.-> p5["The dotnet tool build makes 64-bit TLBs only"]
Figure 7: 32-bit support differs from the very first step of obtaining the tool, which is where people get stuck.
.\tools\dscom32.exe tlbexport .\bin\Release\net8.0-windows\VbaTypedComSample.dll --out .\bin\Release\net8.0-windows\VbaTypedComSample.tlb
The dscom documentation makes the same recommendation: because leaving the project as AnyCPU produces a 64-bit *.comhost.dll, compile the assembly itself as 32-bit if you intend to use it from 32-bit clients. That is the same conclusion as section 3.
If running this by hand after every build gets tedious, adding the dSPACE.Runtime.InteropServices.BuildTasks package lets you generate the TLB automatically at compile time.
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.
regsvr32registers the*.comhost.dllas a COM servertlbregisterregisters the*.tlbas a type library
flowchart TB
accTitle: Registration comes in two parts
accDescr: Diagram showing the two registrations performed with administrator rights, where regsvr32 registers the comhost as a COM server and dscom tlbregister registers the TLB as a type library.
adm["Run with administrator rights"] --> r1["Register the comhost with regsvr32"]
adm --> r2["Register the TLB with tlbregister"]
r1 -.-> m1["Registers the COM activation entry point"]
r2 -.-> m2["Registers the type information VBA sees"]
Figure 8: The activation entry point and the type information are separate things, so registration comes as a pair.
8. Adding the Reference in VBA and Using It with Typing
- Open Excel or Access
- Press
Alt+F11to open the VBA editor (VBE). From the ribbon, that is theDevelopertab >Visual Basic. If theDevelopertab is not shown, enable it underFile>Options>Customize Ribbonby checkingDeveloper - In the VBE menu, choose
Tools>References - The
Available Referenceslist is sorted alphabetically. If registration succeeded, the library name appears there (by default it matches the assembly name,VbaTypedComSample), so tick the checkbox on the left and clickOK - If it does not appear in the list, click the
Browse...button and selectVbaTypedComSample.tlbdirectly
When it does not show up in the list, the cause is usually one of two things: the bitness mismatch from section 3, or a tlbregister from section 7 that never succeeded. 32-bit Office cannot see a TLB that was registered as 64-bit.
flowchart TB
accTitle: Triaging a library that is missing from References
accDescr: Diagram showing that when the library does not appear in the References list the cause is usually either a bitness mismatch or a tlbregister that never succeeded, and that 32-bit Office cannot see a TLB registered as 64-bit.
q1["The library is missing from the list"] --> a1["Suspect the bitness mismatch from section 3"]
q1 --> a2["Suspect a failed tlbregister from section 7"]
a1 -.-> nt1["32-bit Office cannot see a 64-bit TLB"]
Figure 9: When it is missing from the list, the cause narrows down to bitness or registration almost every time.
To confirm that the reference took effect, open View > Object Browser (F2) and check whether you can select VbaTypedComSample in the library dropdown at the top left. If you can see ICalculator and Calculator there, along with Add / Divide / Hello, the TLB was built correctly.
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
Place the cursor inside this procedure, press F5 to run it, and open the Immediate window with Ctrl + G. Three lines appear, matching the implementation from section 4.
30
2.5
Hello, VBA
If that is what you get, then the reference, the COM registration, the runtime startup, and the marshaling of arguments and return values are all working end to end. Conversely, if the values are wrong, suspect the .NET implementation; if it will not run at all, suspect sections 3 and 7.
flowchart TB
accTitle: Triaging from the run result
accDescr: Diagram showing the triage where a sample run that matches expectations means everything from the reference to marshaling works, wrong values point at the .NET implementation, and a failure to run at all points at the bitness in section 3 and the registration in section 7.
r1["Run the VBA sample"] --> q1{"What is the result"}
q1 -->|"As expected"| ok1["The whole path works"]
q1 -->|"Wrong values"| ng1["Suspect the .NET implementation"]
q1 -->|"Will not run"| ng2["Suspect bitness and registration"]
Figure 10: Three lines of output are enough to decide which layer to suspect.
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
Objecteverywhere
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 Hex$(Err.Number)
Debug.Print Err.Description
End Sub
Knowing how to read the values that come out here makes triage much faster.
| Item | What it holds |
|---|---|
Err.Number |
The HRESULT corresponding to the .NET exception, as a signed Long. Decimal is hard to read, so convert it to hex with Hex$(Err.Number) |
Err.Description |
The .NET exception message, passed through verbatim via the COM IErrorInfo. For the code above, it is a string containing Cannot divide by zero. |
HRESULT values are fixed per exception type. The one corresponding to ArgumentOutOfRangeException is COR_E_ARGUMENTOUTOFRANGE, with the value 0x80131502. So if Hex$(Err.Number) reads 80131502, the ArgumentOutOfRangeException from the .NET side is arriving exactly as expected.
The main ones are as follows.
| .NET exception | HRESULT constant | Value |
|---|---|---|
ArgumentException |
COR_E_ARGUMENT |
0x80070057 |
ArgumentOutOfRangeException |
COR_E_ARGUMENTOUTOFRANGE |
0x80131502 |
InvalidOperationException |
COR_E_INVALIDOPERATION |
0x80131509 |
NotSupportedException |
COR_E_NOTSUPPORTED |
0x80131515 |
| Any other general exception | COR_E_EXCEPTION |
0x80131500 |
If you want to branch on the exception type on the VBA side, this HRESULT is what you branch on. That said, a design that branches on HRESULT is fragile against changes to the exception types on the .NET side, so returning business-level failures as return values or error codes rather than exceptions makes for a more stable boundary.
flowchart TB
accTitle: How a .NET exception reaches VBA
accDescr: Diagram showing that an exception thrown on the .NET side is converted to an HRESULT at the COM boundary, that Err.Number receives that HRESULT as a signed Long, and that Err.Description receives the exception message by way of IErrorInfo.
x1["An exception is thrown on the .NET side"] --> x2["Converted to an HRESULT at the COM boundary"]
x2 --> x3["Lands in Err.Number as a signed value"]
x2 --> x4["The message lands in Err.Description"]
x3 -.-> h1["Convert it to hex with Hex$ to read it"]
Figure 11: At the COM boundary an exception changes shape into an HRESULT and reaches VBA through Err.
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.
Concretely, here is what to install.
- The download page is .NET 8 downloads
- What you need is the runtime, not the SDK. The sample in this article is a class library with no UI, so the
.NET Runtimeis enough. If you use WPF or Windows Forms types, you need the.NET Desktop Runtime - Match the bitness to Office. That means the x64 runtime for 64-bit Office and the x86 runtime for 32-bit Office. For the same reason we specified
x64/x86explicitly in section 3, a mismatch here means it will not start - To see whether it is already installed, run
dotnet --list-runtimeson the client PC and look for aMicrosoft.NETCore.App 8.xline
Always write this - the required runtime type, version, and bitness - into your deployment documentation. If it is missing from the installation instructions, someone on site will see ActiveX component can't create object., start suspecting bitness, and burn hours on it.
flowchart TB
accTitle: What to line up at deployment time
accDescr: Diagram showing that deployment means placing the full output set rather than the DLL alone, installing a .NET 8 runtime with the same bitness as Office on the client PC, and documenting the required runtime type, version, and bitness.
h1["Deploy the full output set"] --> u1["It runs on the client"]
h2["A .NET 8 runtime of the same bitness"] --> u1
h3["Runtime details written in the deployment docs"] -.-> u1
Figure 12: The DLL alone will not run; it takes the full set plus the runtime before anything works.
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 an explicit interface 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
ICalculatoras is - If the change is substantial, introduce a new
ICalculator2 - The class may implement both
flowchart TB
accTitle: How to protect a published interface
accDescr: Diagram showing the compatibility approach of leaving the published ICalculator untouched, adding a new ICalculator2 when the change is substantial, and letting the class implement both.
k1["The published ICalculator"] --> k2["Leave it as is"]
k3["A substantial change is wanted"] --> k4["Add a new ICalculator2"]
k2 --> k5["The class can implement both"]
k4 --> k5
Figure 13: Keeping the existing contract and adding the new one alongside it is the COM way.
10.5 Keep the Types Plain
At the boundary exposed to VBA, it is safer not to get fancy.
To start with, these are the types that play well.
intdoubleboolstringDateTimedecimalenum
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
Unregistering is done in the reverse order of registration, using commands of the same bitness you registered with. Administrator rights are required, just as they were for registration.
# For 64-bit Office / 64-bit COM
$out = Resolve-Path .\bin\Release\net8.0-windows
dscom tlbunregister "$out\VbaTypedComSample.tlb"
C:\Windows\System32\regsvr32.exe /u "$out\VbaTypedComSample.comhost.dll"
# For 32-bit Office (on 64-bit Windows)
$out = Resolve-Path .\bin\Release\net8.0-windows
.\tools\dscom32.exe tlbunregister "$out\VbaTypedComSample.tlb"
C:\Windows\SysWOW64\regsvr32.exe /u "$out\VbaTypedComSample.comhost.dll"
The /u option of regsvr32 is what unregisters. You cannot unregister with a different regsvr32 than the one you registered with (something registered as 64-bit cannot be removed with the regsvr32 in SysWOW64). If you move or delete the folder before unregistering, the registry is left holding registrations that point at paths which no longer exist.
flowchart TB
accTitle: How to fold up registrations around an update
accDescr: Diagram showing the update flow of closing Office, unregistering if needed in the reverse order and with commands of the same bitness used for registration, rebuilding, and registering again.
u1["Close Office"] --> u2["Unregister in reverse order if needed"]
u2 --> u3["Rebuild"]
u3 --> u4["Register again"]
u2 -.-> u5["Use commands of the same bitness as registration"]
Figure 14: Unregistering and re-registering as a pair avoids both a locked DLL and leftover registrations.
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 an explicit interface (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
- Complete sample code for this article (COM-exposed library, scripts, VBA, tests) - komurasoft-blog-samples (GitHub)
- Expose .NET components to COM - Microsoft Learn
- Qualify .NET types for interoperation - Microsoft Learn
- ComInterfaceType Enum - Microsoft Learn
- ClassInterfaceType Enum - Microsoft Learn
- COM Callable Wrapper - Microsoft Learn
- DispIdAttribute Class - Microsoft Learn
- dscom - NuGet Gallery
- dspace-group/dscom - GitHub (dscom itself, with the subcommand list and the explanation of 32-bit support)
- dscom releases page (where to get
dscom32.exe) - How to map HRESULTs and exceptions - Microsoft Learn
- How to use the Regsvr32 tool and troubleshoot Regsvr32 error messages - Microsoft Support
- .NET 8 downloads
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Why EXCEL.EXE Processes Remain After C# Excel COM Automation — Reference Release Patterns and the Replacement Decision
A practical look at why EXCEL.EXE processes remain running after automating Excel from C# via Microsoft.Office.Interop.Excel, explained t...
Migrating Excel VBA Macros to Power Automate — What to Replace with Office Scripts, and What to Leave as VBA
A guide to whether Excel VBA macros can migrate to Power Automate, covering what Office Scripts can replace, what only VBA can still do, ...
DLL and COM Interface Backward Compatibility — A Decision Table for Which Changes Break Callers
Which changes to a DLL or COM component actually break their callers? We lay out the three layers of compatibility — binary, source, and ...
Do Business Apps Run on Windows on Arm? — The Reality of x64 Emulation (Prism) and Native DLLs/COM
An answer, aimed at developers and IT staff, to 'will our business app run on Windows on Arm?' Covers how x64 emulation (Prism) works, th...
Choosing Windows Inter-Process Communication ── A Decision Table for Named Pipes / TCP / gRPC / Shared Memory / COM
How do you choose the right way for Windows applications to talk to each other? This article organizes named pipes, local TCP, gRPC, shar...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
ActiveX Migration
Topic page for staged decisions around keeping, wrapping, or replacing COM / ActiveX / OCX assets.
Where This Topic Connects
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.
Frequently Asked Questions
Common questions about the topic of this article.
- What do I need in order to use a .NET 8 DLL from VBA with full typing (early binding)?
- Build the .NET 8 class library with EnableComHosting=true so that a *.comhost.dll is produced, then create the *.tlb with dscom tlbexport. Next, register the *.comhost.dll with regsvr32 and the *.tlb with dscom tlbregister, and add that TLB as a reference in VBA. You can then use it with full typing in the form Dim x As LibraryName.IYourInterface. In terms of roles: the *.comhost.dll is the COM activation entry point, the *.tlb is the type information VBA sees, and the *.dll is the implementation itself.
- What causes the error 'ActiveX component can't create object'?
- The typical cause is a bitness mismatch between Office/VBA and the COM server. For 64-bit Office, build for x64/win-x64 and register with the regsvr32 in System32. For 32-bit Office (on 64-bit Windows), build for x86/win-x86, register with the regsvr32 in SysWOW64, and generate the TLB with dscom32.exe. With the COM host in .NET 5+, leaving the project as AnyCPU tends to push the *.comhost.dll toward the 64-bit side, where it may not mesh with 32-bit Office, so it is safer to specify x86 or x64 explicitly to match Office.
- Should I avoid ClassInterfaceType.AutoDual?
- It looks convenient, but it breaks easily once you touch member order or composition after publication, so avoid it. If you want stable, typed use from VBA, the established practice is to define an explicit interface, set the class to ClassInterfaceType.None, and mark the interface consumed from VBA as InterfaceIsDual. Assigning DispIds reduces the risk of breakage when the method order changes. Also, in COM the GUID is the contract itself, so carelessly regenerating an IID or CLSID after publication breaks existing VBA references and registrations.
- Can I just hand over the DLL by itself when deploying?
- The DLL alone will not work. Deploy the implementation *.dll, the *.comhost.dll, the *.deps.json, the *.runtimeconfig.json, the *.tlb, and any dependency DLLs together. The client PC also needs the corresponding .NET 8 runtime, because the COM host is not a self-contained deployment and is operated as framework-dependent in practice. Note as well that if you change the deployment location later, the registration has to be redone.