COM Components in C#: Consume, Expose, and Stop RCW Leaks
Most teams only meet COM components C# interop when something legacy refuses to die: an Office automation path, a vendor OCX, a C++ service that still speaks IUnknown, or a VBA macro that has to call into a .NET library. The docs explain RCW and CCW. Production still fails on apartment threads, missing ProgIDs, and objects that never release.
I treat COM as a boundary, not a coding style. This post is the practical map I use when I have to consume or expose COM from C# without turning the process into a leaky, STA-hostile mess.
What “COM components C#” actually means
Two directions, one binary contract:
| Direction | Wrapper | You are… | Typical pain |
|---|---|---|---|
| Consume COM from .NET | RCW (Runtime Callable Wrapper) | Calling a native COM server from C# | Registration, bitness, lifetime, STA |
| Expose .NET to COM | CCW (COM Callable Wrapper) | Letting VBA/C++/scripts call your assembly | Guids, visibility, reg-free vs registry, hosting |
COM itself is language-agnostic: interfaces (v-tables), reference counting via IUnknown, and activation through the registry or side-by-side manifests. C# does not “speak COM” natively — the CLR builds proxies. That is useful until you forget the proxy is real and the native object has its own lifetime rules.
Official starting points: Microsoft’s COM interop overview and exposing .NET components to COM.
When I still use COM (and when I refuse)
| Situation | Use COM? | Why |
|---|---|---|
| Vendor only ships a COM API | Yes | No choice; isolate it behind your own interface |
| Excel/Word automation from a service | Avoid if possible | Office + server sessions are fragile; prefer file SDKs / Graph / Open XML |
| In-process plugin for a desktop host that loads COM | Yes | Match the host’s contract; keep surface tiny |
| Greenfield service-to-service | No | gRPC, HTTP, or message bus — not IUnknown across the network |
| Need cross-language in-process on Windows only | Maybe | COM still wins for some desktop shells; prefer .NET hosting APIs first |
Rule of thumb: if the only reason for COM is “we always did it this way,” redesign. If the reason is a locked vendor surface or a host process you do not control, contain COM behind one adapter assembly and never let RCWs leak into domain code.
Consuming COM from C# without the usual footguns
The happy path is still:
- Add a COM reference (or
tlbimp/ embedded interop types). - Activate via type library metadata or
Type.GetTypeFromProgID/Activator.CreateInstance. - Call methods on the RCW.
- Release deterministically when you are done.
using System.Runtime.InteropServices;
// Prefer a typed interop assembly when you have a stable TLB.
// Dynamic activation is fine for one-off tools; bad as a library default.
var t = Type.GetTypeFromProgID("LegacyVendor.Widget", throwOnError: true);
dynamic widget = Activator.CreateInstance(t)!;
try
{
widget.Connect("CONFIG=prod");
var status = (string)widget.GetStatus();
Console.WriteLine(status);
}
finally
{
// RCWs are reference-counted on the COM side.
// Leaving them to finalizers is how long-running hosts accumulate leaks.
if (Marshal.IsComObject(widget))
Marshal.FinalReleaseComObject(widget);
}
What I enforce in reviews:
- Bitness match — 32-bit COM servers need a 32-bit host (or a surrogate). “Works on my laptop” often means x86 VS hosting.
- STA vs MTA — UI-bound COM (Office, many ActiveX controls) wants an STA thread. A thread-pool worker calling into STA COM without marshaling is a random hang generator.
- One owner for release — do not pass raw RCWs through async continuations across threads without understanding the apartment.
- Embedded Interop Types (
EmbedInteropTypes) when you only need a slice of a huge PIA and want to avoid shipping PIAs.
Exposing C# as COM components
When VBA, C++, or a legacy runner must call you, you ship a class with stable interface GUIDs and a registration story.
using System.Runtime.InteropServices;
[ComVisible(true)]
[Guid("a1b2c3d4-e5f6-7890-abcd-ef1234567890")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IOrderLookup
{
string FindOrderId(string externalRef);
}
[ComVisible(true)]
[Guid("b2c3d4e5-f6a7-8901-bcde-f12345678901")]
[ClassInterface(ClassInterfaceType.None)]
[ProgId("Contoso.Integration.OrderLookup")]
public class OrderLookup : IOrderLookup
{
public string FindOrderId(string externalRef)
{
// Keep this thin. Heavy logic stays in non-COM assemblies.
return OrderService.Resolve(externalRef);
}
}
Checklist I actually use:
[ClassInterface(ClassInterfaceType.None)]— expose only the interfaces you designed; auto-dual class interfaces become versioning landmines.- Stable
Guidvalues committed to source control — regenerating GUIDs breaks every client. - Separate contract assembly (interfaces) from implementation when multiple hosts version independently.
- .NET Framework still has the classic “Register for COM Interop” path; on modern .NET, follow the current expose-to-COM guidance (ComWrappers / registration tooling) rather than assuming mscoree host tricks from 2010 still apply unchanged.
- Prefer reg-free COM (side-by-side manifests) for desktop apps you control end-to-end; use registry registration when the host is Excel or an IT-managed thick client.
Decision guide: how I structure the boundary
Host / VBA / C++ → COM adapter assembly (CCW or RCW only)
↓
Internal .NET interfaces (no COM attributes)
↓
Domain services, HTTP, DB, messaging
| Choice | Prefer | Avoid |
|---|---|---|
| Where COM attributes live | Thin adapter project | Domain entities marked [ComVisible] |
| Error model | Map to HRESULT + short message | Throwing rich .NET exceptions across the boundary unchecked |
| Data shapes | Primitives, arrays, simple DTOs | Deep object graphs, callbacks you cannot lifetime-manage |
| Threading | Document STA/MTA requirements in README | Assuming thread-pool = fine |
| Testing | Smoke host that activates ProgID in CI (Windows runner) | “It compiled” as the only check |
This is the same instinct I use on integration microservices: keep the dirty protocol at the edge, keep the core boring. COM is just an older protocol.
Pitfalls that show up in tickets
- “Class not registered” — wrong bitness, wrong user hive (per-user vs machine), or installer skipped
regasm/native registration. - RPC_E_WRONG_THREAD / mysterious hangs — apartment mismatch; fix the thread model, do not spray
Task.Runrandomly. - Memory growth in long-running hosts — RCWs not released; event sinks still subscribed; Excel quit but child objects alive.
- Versioning breaks — changed method order on a dual interface without a new IID.
- Security — elevating a process just so COM activation works. Fix registration and identity instead.
- Cloud hosts — classic COM servers often need a full Windows desktop session. Do not put Office automation on a locked-down Server Core container and expect stability.
FAQ: COM components in C#
What are COM components in C# used for today?
Mostly boundaries: calling vendor-native APIs, plugin hosts, and some Office/desktop automation. Greenfield distributed systems should not invent new COM contracts between services.
What is the difference between RCW and CCW?
RCW lets managed code call a COM object. CCW lets COM callers invoke a managed object. Both are proxies with lifetime rules you must respect.
How do I create COM components in C#?
Mark interfaces/classes [ComVisible(true)] with explicit GUIDs, use ClassInterfaceType.None, assign a ProgID, and register (or ship a reg-free manifest). Keep the COM-visible surface thin and push logic into normal .NET libraries.
Why does FinalReleaseComObject matter?
Because the GC finalizer timing is not a resource plan. In servers and add-ins that create many COM objects, deterministic release prevents handle and memory growth.
Can I use COM components C# interop on .NET 6/8?
Yes for many Windows scenarios, but the hosting and registration story is not identical to old .NET Framework desktop defaults. Read the current Microsoft expose-to-COM docs for your TFM before copying a 2012 blog post’s regasm steps blindly.
Is COM the same as C++/CLI or P/Invoke?
No. P/Invoke targets C exports. C++/CLI is a mixed-language bridge. COM is an interface/activation model with IUnknown and (usually) registration. Pick the thinnest bridge that matches the native surface you must call.
Closing
If you search for com components c#, you usually need one of three answers: how to call a legacy server safely, how to expose a small .NET surface to a COM host, or how to stop the process leaking. The pattern is the same every time — treat COM as an edge adapter, own lifetime and threading explicitly, and keep domain code free of [ComVisible] attributes.
Related reading on this blog: Azure Service Bus peek-lock (another boundary that punishes sloppy lifetime), and the integration series starting at microservices integration patterns on Azure.