AdaskoTheBeAsT.Interop.COM
3.0.0
dotnet add package AdaskoTheBeAsT.Interop.COM --version 3.0.0
NuGet\Install-Package AdaskoTheBeAsT.Interop.COM -Version 3.0.0
<PackageReference Include="AdaskoTheBeAsT.Interop.COM" Version="3.0.0" />
<PackageVersion Include="AdaskoTheBeAsT.Interop.COM" Version="3.0.0" />
<PackageReference Include="AdaskoTheBeAsT.Interop.COM" />
paket add AdaskoTheBeAsT.Interop.COM --version 3.0.0
#r "nuget: AdaskoTheBeAsT.Interop.COM, 3.0.0"
#:package AdaskoTheBeAsT.Interop.COM@3.0.0
#addin nuget:?package=AdaskoTheBeAsT.Interop.COM&version=3.0.0
#tool nuget:?package=AdaskoTheBeAsT.Interop.COM&version=3.0.0
AdaskoTheBeAsT.Interop.COM
Registration-free COM interop for .NET β skip
regsvr32, skip the registry, skip the drama.
π¬ Code quality β SonarCloud
Badges reflect the shared umbrella project
AdaskoTheBeAsT_AdaskoTheBeAsT.Interopwhich hosts the combined analysis for everyAdaskoTheBeAsT.Interop.*library in the monorepo.
π Hello, COM-wrangler
So you've got a COM component. Maybe it's a vendor ActiveX control from 2005 that still refuses to die. Maybe it's an in-house ATL library that your colleague shipped and then went on sabbatical. Maybe it's a third-party SDK that cheerfully assumes regsvr32 ran as Administrator on every machine that will ever exist. π
All you wanted to do was call a single method on it. From a .NET process. Ideally without:
- π« polluting the Windows registry
- π requiring Administrator rights to install
- π§ breaking when another version of the same COM component is registered globally
- π² guessing whether your thread is in the right apartment
- π leaking activation contexts because the API that frees them is
extern "C" void WINAPIand nobody documented what happens if you forget
AdaskoTheBeAsT.Interop.COM is the small, focused library that turns all of that into a single call:
executor.Execute(comDllPath, manifestPath, () =>
{
var obj = new YourCom.SomeClass();
obj.DoTheThing();
});
The activation context is created from your manifest, pushed on the current thread, your Action runs in a real STA with message pumping, and everything is torn down cleanly even when you throw. β¨
β¨ Why you'll love this
- π No COM registration ever.
regsvr32stays uninstalled. No admin rights. Your build server thanks you. - π Manifest-based side-by-side activation. Standard Windows SxS β battle-tested since Windows XP SP2, used by every in-box OS component.
- π§΅ Real STA, real message pump.
CreateActCtxβActivateActCtxβCoInitialize(STA)β run work βPeekMessage/TranslateMessage/DispatchMessagepump βDeactivateActCtxβReleaseActCtx. You just write the innerAction. (ADR-0011) - π§Ή
ComObjectHandle<T>isIDisposable.using var handle = creation.Value!;β that's your whole cleanup story. Forget to dispose? A diagnostic-only finalizer emits aHandleLeakedevent on anEventSourceso you find the bug instead of crashing later. (ADR-0012, ADR-0018) - π§© Drop-in DI.
services.AddSingleton<IComExecutor, ComExecutor>();and injectIComExecutoranywhere. Unit tests replace it with aMock<IComExecutor>in one line. (ADR-0013) - π₯οΈ 10 TFMs, all green.
net10.0,net9.0,net8.0,net481,net48,net472,net471,net47,net462,netstandard2.0βTreatWarningsAsErrors=trueon every cell. (ADR-0007) - ποΈ AnyCPU library. Consumers no longer force 32-bit; a single NuGet works for both x86 and x64 host processes. (ADR-0016)
- πͺ
[SupportedOSPlatform("windows")]on the public surface. Static analysis catches cross-platform call-sites at compile time on TFMs that understand the attribute. (ADR-0014) - π Built-in observability.
EventSourcenamedAdaskoTheBeAsT.Interop.COMemitsHandleLeaked(Event ID1). Subscribe withdotnet-trace --providers AdaskoTheBeAsT.Interop.COMor an in-processEventListener. - βοΈ Source Link + snupkg. F11 steps into this library from your debugger. No guessing which version is deployed.
- π 19 ADRs documenting every meaningful design choice.
- π§ͺ 567 test invocations (63 tests Γ 9 TFMs) plus 92.8 % local line coverage of the shipped assembly. 9 of 10 source files at 100 %.
π¦ Package
| Package | What it gives you |
|---|---|
AdaskoTheBeAsT.Interop.COM |
β IComExecutor + ComExecutor, static Executor, ComObjectHandle<T>, ComPathDescriptor, ComObjectCreationResult<T>, Result, ComInteropEventSource. One assembly, zero runtime dependencies beyond the BCL. |
β¬οΈ Install
dotnet add package AdaskoTheBeAsT.Interop.COM
# or, via the Package Manager Console
Install-Package AdaskoTheBeAsT.Interop.COM
Symbols ship as .snupkg with Source Link and embedded untracked sources. Step in. Look around. It's fine.
πΊοΈ Target framework matrix
| TFM | Status | Notes |
|---|---|---|
net10.0 |
β | Primary target; LibraryImport source-gen P/Invoke, [SupportedOSPlatform], AllowUnsafeBlocks. |
net9.0 |
β | Primary target. |
net8.0 |
β | Primary target. |
net481 |
β | Windows desktop. |
net48 |
β | Windows desktop. |
net472 |
β | Windows desktop. |
net471 |
β | Windows desktop. |
net47 |
β | Windows desktop. |
net462 |
β | Windows desktop. |
netstandard2.0 |
β | Classic fallback; [SupportedOSPlatform] downgrades to no-op. |
Every cell is built with TreatWarningsAsErrors=true, ContinuousIntegrationBuild=true, Deterministic=true, and exercised by CI. Consumers on a single TFM always win via NuGet TFM precedence β you don't have to know this matrix exists.
π Quick Start
Recommended API:
IComExecutor(implemented byComExecutor). Inject it into your types and it will be trivially testable. The staticExecutorclass remains available for backward compatibility with v2.x code, but all new code should useIComExecutor.
Register ComExecutor Once
using AdaskoTheBeAsT.Interop.COM;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddSingleton<IComExecutor, ComExecutor>();
// ... resolve IComExecutor wherever you need to run COM code.
Basic Usage (recommended)
using AdaskoTheBeAsT.Interop.COM;
public sealed class MyComRunner
{
private readonly IComExecutor _executor;
public MyComRunner(IComExecutor executor) => _executor = executor;
public void Run(string comDllPath, string manifestPath)
{
var result = _executor.Execute(comDllPath, manifestPath, () =>
{
var comObject = new MyCom.MyComClass();
var output = comObject.DoSomething();
Console.WriteLine(output);
});
if (!result.Success)
{
Console.WriteLine($"Error: {result.Exception?.Message}");
}
}
}
Basic Usage (static, legacy β v2.x compatible)
Use this form only if you have existing code that depends on the static entry point; no new code should do this.
using AdaskoTheBeAsT.Interop.COM;
var result = Executor.Execute(comDllPath, manifestPath, () =>
{
var comObject = new MyCom.MyComClass();
Console.WriteLine(comObject.DoSomething());
});
Real-World Example
using AdaskoTheBeAsT.Interop.COM;
public sealed class ComStringProcessor
{
private readonly IComExecutor _executor;
public ComStringProcessor(IComExecutor executor) => _executor = executor;
public string ConcatenateStrings(string str1, string str2)
{
var comDllPath = Path.Combine(AppContext.BaseDirectory, "NativeCOM.dll");
var manifestPath = Path.Combine(AppContext.BaseDirectory, "NativeCOM.manifest");
string? result = null;
var executionResult = _executor.Execute(comDllPath, manifestPath, () =>
{
var concatenator = new NativeCOM.StringConcatenatorClass();
result = concatenator.ConcatStrings(str1, str2);
});
if (!executionResult.Success)
{
throw new InvalidOperationException(
"COM execution failed",
executionResult.Exception);
}
return result ?? string.Empty;
}
}
Unit-testing ComStringProcessor
Because IComExecutor is an interface, the class above is trivial to test without any real COM on the box:
var executor = new Mock<IComExecutor>();
executor
.Setup(e => e.Execute(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Action>()))
.Callback<string, string, Action>((_, _, action) => action())
.Returns(new Result { Success = true });
var sut = new ComStringProcessor(executor.Object);
// assert against sut.ConcatenateStrings(...) without loading the COM DLL.
Create a COM Object and Release It Later
Use IComExecutor.Create(...) when the COM object should outlive a single callback and you want to release it explicitly.
Recommended: using statement with IComExecutor
ComObjectHandle<T> implements IDisposable, so the simplest way to guarantee release is a using block:
using AdaskoTheBeAsT.Interop.COM;
public sealed class ComSessionRunner
{
private readonly IComExecutor _executor;
public ComSessionRunner(IComExecutor executor) => _executor = executor;
public void Run(string comDllPath, string manifestPath)
{
var creation = _executor.Create(
comDllPath,
manifestPath,
() => new NativeCOM.StringConcatenatorClass());
if (!creation.Success)
{
throw new InvalidOperationException("COM object creation failed.", creation.Exception);
}
using var handle = creation.Value
?? throw new InvalidOperationException("The COM handle was not created.");
var concatenator = handle.ComObject
?? throw new InvalidOperationException("The COM object was not created.");
var output = concatenator.ConcatStrings("Hello", "World!");
Console.WriteLine(output);
// handle.Dispose() is called automatically at the end of scope,
// which in turn releases the COM object and activation contexts.
}
}
Explicit IComExecutor.Free(...) (when you cannot use using)
If the handle has to outlive a local scope, call Free explicitly:
var creation = _executor.Create(
comDllPath,
manifestPath,
() => new NativeCOM.StringConcatenatorClass());
if (!creation.Success)
{
throw new InvalidOperationException("COM object creation failed.", creation.Exception);
}
var handle = creation.Value
?? throw new InvalidOperationException("The COM handle was not created.");
try
{
var concatenator = handle.ComObject
?? throw new InvalidOperationException("The COM object was not created.");
var output = concatenator.ConcatStrings("Hello", "World!");
Console.WriteLine(output);
}
finally
{
var release = _executor.Free(handle);
if (!release.Success)
{
throw new InvalidOperationException("COM object release failed.", release.Exception);
}
}
The static equivalents (
Executor.Create,Executor.Free) still work unchanged for existing v2.x code.
Multiple COM Contexts
When you need to work with multiple COM components simultaneously:
var descriptors = new List<ComPathDescriptor>
{
new ComPathDescriptor(@"C:\MyApp\ComLib1.dll", @"C:\MyApp\ComLib1.manifest"),
new ComPathDescriptor(@"C:\MyApp\ComLib2.dll", @"C:\MyApp\ComLib2.manifest"),
};
var result = _executor.Execute(descriptors, () =>
{
// Both COM libraries are now active
var obj1 = new ComLib1.MyClass();
var obj2 = new ComLib2.AnotherClass();
// Use both objects together
var data = obj1.GetData();
obj2.ProcessData(data);
});
Using with AdaskoTheBeAsT.Interop.Threading
When you also need a dedicated STA thread, timeout handling, or a reusable STA scheduler, combine this package with AdaskoTheBeAsT.Interop.Threading.
dotnet add package AdaskoTheBeAsT.Interop.Threading
One-Off COM Call with Timeout
using AdaskoTheBeAsT.Interop.COM;
using AdaskoTheBeAsT.Interop.Threading;
public sealed class TimedComStringProcessor
{
private readonly IComExecutor _executor;
private readonly string _comDllPath;
private readonly string _manifestPath;
public TimedComStringProcessor(IComExecutor executor, string comDllPath, string manifestPath)
{
_executor = executor;
_comDllPath = comDllPath;
_manifestPath = manifestPath;
}
public Task<string> ConcatAsync(string left, string right, CancellationToken cancellationToken)
=> SingleThreadedApartmentTask.RunWithTimeoutAsync(
TimeSpan.FromSeconds(10),
() =>
{
string output = string.Empty;
var execution = _executor.Execute(_comDllPath, _manifestPath, () =>
{
var concatenator = new NativeCOM.StringConcatenatorClass();
output = concatenator.ConcatStrings(left, right);
});
if (!execution.Success)
{
throw new InvalidOperationException("The COM call failed.", execution.Exception);
}
return output;
},
cancellationToken);
}
Reuse One COM Object on a Single STA Thread
using AdaskoTheBeAsT.Interop.COM;
using AdaskoTheBeAsT.Interop.Threading;
public sealed class ScheduledComStringProcessor : IAsyncDisposable
{
private readonly IComExecutor _executor;
private readonly string _comDllPath;
private readonly string _manifestPath;
private ComObjectHandle<NativeCOM.StringConcatenatorClass>? _handle;
private NativeCOM.StringConcatenatorClass? _concatenator;
public ScheduledComStringProcessor(IComExecutor executor, string comDllPath, string manifestPath)
{
_executor = executor;
_comDllPath = comDllPath;
_manifestPath = manifestPath;
}
public Task InitializeAsync(CancellationToken cancellationToken)
=> SingleThreadedApartmentTaskScheduler.RunAsync(
() =>
{
var creation = _executor.Create(
_comDllPath,
_manifestPath,
() => new NativeCOM.StringConcatenatorClass());
if (!creation.Success)
{
throw new InvalidOperationException("Failed to create the COM object.", creation.Exception);
}
_handle = creation.Value
?? throw new InvalidOperationException("The COM handle was not created.");
_concatenator = _handle.ComObject
?? throw new InvalidOperationException("The COM object was not created.");
return 0;
},
cancellationToken);
public Task<string> ConcatAsync(string left, string right, CancellationToken cancellationToken)
=> SingleThreadedApartmentTaskScheduler.RunAsync(
() =>
{
if (_concatenator is null)
{
throw new InvalidOperationException("The COM object is not initialized.");
}
return _concatenator.ConcatStrings(left, right);
},
cancellationToken);
public ValueTask DisposeAsync()
=> new(
SingleThreadedApartmentTaskScheduler.RunAsync(
() =>
{
if (_handle is not null)
{
var release = _executor.Free(_handle);
_concatenator = null;
_handle = null;
if (!release.Success)
{
throw new InvalidOperationException("Failed to release the COM object.", release.Exception);
}
}
return 0;
},
CancellationToken.None));
}
π§© Dependency Injection
This library does not ship a separate *.DependencyInjection NuGet package (see ADR-0017). ComExecutor is stateless and needs no configuration, so the canonical wiring is a one-liner using Microsoft.Extensions.DependencyInjection. Logging, metrics, and named/keyed resolution are achieved by composing IComExecutor with standard DI building blocks β no library-specific helpers required.
Register as a singleton (canonical)
using AdaskoTheBeAsT.Interop.COM;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddSingleton<IComExecutor, ComExecutor>();
// Consume:
public sealed class MyService
{
private readonly IComExecutor _executor;
public MyService(IComExecutor executor) => _executor = executor;
}
ComExecutor holds no state, so a singleton is always correct. There is no reason to register it as scoped or transient.
Register as a keyed singleton (.NET 8+)
When an application has several logical COM workloads β different manifests, different DLLs, different configuration β keyed registrations let each component resolve "its" executor by a string key. This is only meaningful when you also keep per-key ComPathDescriptor data somewhere (e.g. keyed options, a dictionary), because ComExecutor itself carries no state.
services.AddKeyedSingleton<IComExecutor, ComExecutor>("primary");
services.AddKeyedSingleton<IComExecutor, ComExecutor>("reporting");
public sealed class ReportGenerator
{
private readonly IComExecutor _executor;
public ReportGenerator([FromKeyedServices("reporting")] IComExecutor executor)
=> _executor = executor;
}
// Manual resolution:
var primary = serviceProvider.GetRequiredKeyedService<IComExecutor>("primary");
If you also want to inject the paths/manifests by key, pair the keyed executor with a keyed ComPathDescriptor:
services.AddKeyedSingleton<IComExecutor, ComExecutor>("reporting");
services.AddKeyedSingleton(
"reporting",
new ComPathDescriptor(@"C:\COM\Reporting.dll", @"C:\COM\Reporting.manifest"));
Add logging via a decorator
ComExecutor is sealed. To add cross-cutting concerns, wrap it with your own IComExecutor decorator β this is idiomatic .NET DI composition and needs no library-specific plumbing:
using Microsoft.Extensions.Logging;
public sealed class LoggingComExecutor : IComExecutor
{
private readonly IComExecutor _inner;
private readonly ILogger<LoggingComExecutor> _logger;
public LoggingComExecutor(IComExecutor inner, ILogger<LoggingComExecutor> logger)
{
_inner = inner;
_logger = logger;
}
public Result Execute(string comAssemblyPath, string manifestPath, Action action)
{
_logger.LogDebug("COM Execute starting for {Manifest}", manifestPath);
var result = _inner.Execute(comAssemblyPath, manifestPath, action);
if (!result.Success)
{
_logger.LogError(result.Exception, "COM Execute failed for {Manifest}", manifestPath);
}
return result;
}
public Result Execute(ICollection<ComPathDescriptor> descriptors, Action action)
=> _inner.Execute(descriptors, action);
public ComObjectCreationResult<T> Create<T>(string comAssemblyPath, string manifestPath, Func<T> factory)
where T : class
=> _inner.Create(comAssemblyPath, manifestPath, factory);
public ComObjectCreationResult<T> Create<T>(ICollection<ComPathDescriptor> descriptors, Func<T> factory)
where T : class
=> _inner.Create(descriptors, factory);
public Result Free<T>(ComObjectHandle<T> handle)
where T : class
=> _inner.Free(handle);
}
Register the decorator. Without Scrutor:
services.AddSingleton<ComExecutor>();
services.AddSingleton<IComExecutor>(sp =>
new LoggingComExecutor(
sp.GetRequiredService<ComExecutor>(),
sp.GetRequiredService<ILogger<LoggingComExecutor>>()));
With Scrutor:
services.AddSingleton<IComExecutor, ComExecutor>();
services.Decorate<IComExecutor, LoggingComExecutor>();
Publish metrics via System.Diagnostics.Metrics.Meter
The decorator pattern composes just as cleanly with Meter / IMeterFactory (available in .NET 8+, or via System.Diagnostics.DiagnosticSource on older TFMs):
using System.Diagnostics;
using System.Diagnostics.Metrics;
public sealed class MetricsComExecutor : IComExecutor, IDisposable
{
public const string MeterName = "AdaskoTheBeAsT.Interop.COM";
private readonly IComExecutor _inner;
private readonly Meter _meter;
private readonly Counter<long> _executions;
private readonly Counter<long> _executionFailures;
private readonly Histogram<double> _executionDurationMs;
public MetricsComExecutor(IComExecutor inner, IMeterFactory meterFactory)
{
_inner = inner;
_meter = meterFactory.Create(MeterName);
_executions = _meter.CreateCounter<long>("com.execute.count");
_executionFailures = _meter.CreateCounter<long>("com.execute.failures");
_executionDurationMs = _meter.CreateHistogram<double>(
"com.execute.duration",
unit: "ms");
}
public Result Execute(string comAssemblyPath, string manifestPath, Action action)
{
var tag = new KeyValuePair<string, object?>("manifest", manifestPath);
var sw = Stopwatch.StartNew();
var result = _inner.Execute(comAssemblyPath, manifestPath, action);
sw.Stop();
_executions.Add(1, tag);
_executionDurationMs.Record(sw.Elapsed.TotalMilliseconds, tag);
if (!result.Success)
{
_executionFailures.Add(1, tag);
}
return result;
}
// Delegate the rest of the interface identically, instrumenting as needed.
public Result Execute(ICollection<ComPathDescriptor> descriptors, Action action)
=> _inner.Execute(descriptors, action);
public ComObjectCreationResult<T> Create<T>(string comAssemblyPath, string manifestPath, Func<T> factory)
where T : class
=> _inner.Create(comAssemblyPath, manifestPath, factory);
public ComObjectCreationResult<T> Create<T>(ICollection<ComPathDescriptor> descriptors, Func<T> factory)
where T : class
=> _inner.Create(descriptors, factory);
public Result Free<T>(ComObjectHandle<T> handle)
where T : class
=> _inner.Free(handle);
public void Dispose() => _meter.Dispose();
}
Register (logging + metrics, chained):
services.AddLogging();
services.AddMetrics();
services.AddSingleton<ComExecutor>();
services.AddSingleton<IComExecutor>(sp =>
{
IComExecutor exec = sp.GetRequiredService<ComExecutor>();
exec = new LoggingComExecutor(exec, sp.GetRequiredService<ILogger<LoggingComExecutor>>());
exec = new MetricsComExecutor(exec, sp.GetRequiredService<IMeterFactory>());
return exec;
});
Or with Scrutor:
services.AddSingleton<IComExecutor, ComExecutor>();
services.Decorate<IComExecutor, MetricsComExecutor>();
services.Decorate<IComExecutor, LoggingComExecutor>(); // runs first (outermost)
Consume the metrics via your preferred exporter β OpenTelemetry, Application Insights, dotnet-counters, etc. β by subscribing to the meter name "AdaskoTheBeAsT.Interop.COM".
βοΈ How It Works
- Activation Context Creation - Creates Windows activation context from your manifest file
- Context Activation - Activates the context to enable registration-free COM
- COM Execution - Runs your code with the COM object in STA
- Message Pumping - Processes Windows messages for COM callbacks
- Cleanup - Automatically deactivates and releases contexts
π Creating COM Manifests
Using ManifestMaker (Commercial Tool)
You can generate manifests for your COM DLLs using ManifestMaker, a commercial desktop tool:
- Download and install ManifestMaker from https://www.manifestmaker.com/
- Load your COM DLL file or enter COM details manually
- Configure settings:
- Assembly name and version
- COM class details (CLSID, threading model, ProgID)
- File dependencies
- Generate and download your manifest file
- Place the manifest file in the same directory as your COM DLL
Benefits:
- β¨ Automatically extracts COM metadata from DLL
- π― Generates proper manifest structure
- π§ Supports type libraries and dependencies
Note: ManifestMaker is a paid service. Check their website for current pricing and licensing terms.
Manual Manifest Creation
If you prefer to create manifests manually or need custom configuration:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
type="win32"
name="NativeCOM"
version="1.0.0.0"/>
<file name="NativeCOM.dll">
<comClass
clsid="{YOUR-CLSID-HERE}"
threadingModel="Apartment"
progid="NativeCOM.StringConcatenator"/>
</file>
</assembly>
Finding Your COM CLSID
If you need to find the CLSID of your COM component:
Option 1: Using OleView (Windows SDK)
# Open OleView.exe from Windows SDK
# Navigate to "All Objects" and search for your component
Option 2: Using Registry (if previously registered)
# Search in Registry Editor (regedit)
HKEY_CLASSES_ROOT\CLSID
HKEY_CLASSES_ROOT\YourProgId
Option 3: Using PowerShell
# Load the DLL and inspect COM registration
$assembly = [System.Reflection.Assembly]::LoadFile("C:\path\to\your.dll")
$types = $assembly.GetTypes() | Where-Object { $_.IsComVisible -eq $true }
$types | ForEach-Object { $_.GUID }
Option 4: Using TlbImp/RegAsm
# For .NET COM components
regasm YourAssembly.dll /regfile:output.reg
# Inspect output.reg for CLSID values
# For native COM
tlbimp NativeCOM.dll /out:Interop.dll /verbose
Manifest Threading Models
Choose the appropriate threading model for your COM component:
| Threading Model | Description | Use When |
|---|---|---|
Apartment |
Single-threaded apartment (STA) | UI components, most legacy COM |
Free |
Multi-threaded apartment (MTA) | Thread-safe, no UI interaction |
Both |
Supports both STA and MTA | Flexible components |
Neutral |
Thread-neutral | Advanced scenarios |
Recommendation: Use Apartment for most scenarios, especially if unsure.
Manifest File Example with Type Library
For COM components with type libraries:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
type="win32"
name="NativeCOM"
version="1.0.0.0"/>
<file name="NativeCOM.dll">
<comClass
clsid="{12345678-1234-1234-1234-123456789012}"
threadingModel="Apartment"
progid="NativeCOM.StringConcatenator.1"
description="String Concatenator COM Component"/>
<typelib
tlbid="{87654321-4321-4321-4321-210987654321}"
version="1.0"
helpdir=""
flags="HASDISKIMAGE"/>
</file>
</assembly>
Validating Your Manifest
After creating your manifest, validate it:
- Well-formed XML - Ensure proper XML syntax
- Correct CLSID - Match your COM component's CLSID
- File name - Must match the actual DLL name
- Threading model - Appropriate for your component
- Test - Run with this library to verify it works
// Quick validation test (using injected IComExecutor)
var result = executor.Execute(@"C:\path\to\COM.dll", @"C:\path\to\COM.manifest", () =>
{
Console.WriteLine("Manifest loaded successfully!");
});
if (!result.Success)
{
Console.WriteLine($"Manifest error: {result.Exception?.Message}");
}
π API Reference
IComExecutor Interface (recommended)
The primary entry point for new code. Implemented by ComExecutor. Register ComExecutor as a singleton with your DI container and depend on the interface; every method below is testable via standard mocking tools.
[SupportedOSPlatform("windows")]
public interface IComExecutor
{
Result Execute(string comAssemblyPath, string manifestPath, Action action);
Result Execute(ICollection<ComPathDescriptor> comPathDescriptors, Action action);
ComObjectCreationResult<T> Create<T>(string comAssemblyPath, string manifestPath, Func<T> factory)
where T : class;
ComObjectCreationResult<T> Create<T>(ICollection<ComPathDescriptor> comPathDescriptors, Func<T> factory)
where T : class;
Result Free<T>(ComObjectHandle<T> comObjectHandle) where T : class;
}
Execute(string comAssemblyPath, string manifestPath, Action action)
Executes an action with a single COM activation context.
Parameters:
comAssemblyPath- Full path to the COM DLLmanifestPath- Full path to the manifest fileaction- Code to execute in the activation context
Returns: Result object containing success status and any exception
Execute(ICollection<ComPathDescriptor> comPathDescriptors, Action action)
Executes an action with multiple COM activation contexts, activated in order and deactivated in reverse order.
Create<T>(string comAssemblyPath, string manifestPath, Func<T> factory)
Creates a COM object inside a registration-free activation context and returns a ComObjectHandle<T>.
Release the handle later by calling handle.Dispose() (via using) or executor.Free(handle) on the same thread.
Create<T>(ICollection<ComPathDescriptor> comPathDescriptors, Func<T> factory)
Creates a COM object inside multiple registration-free activation contexts and returns a ComObjectHandle<T>.
Free<T>(ComObjectHandle<T> comObjectHandle)
Releases a handle created by one of the Create overloads. Idempotent; a second call on an already-released handle is a no-op and returns success.
ComExecutor Class (recommended implementation)
Stateless default implementation of IComExecutor. Register as a singleton:
services.AddSingleton<IComExecutor, ComExecutor>();
[SupportedOSPlatform("windows")]
public sealed class ComExecutor : IComExecutor { /* delegates to the static Executor */ }
Executor Static Class (legacy; kept for v2.x compatibility)
The original static entry point, kept so existing code compiles unchanged. Prefer IComExecutor + ComExecutor for all new code. ComExecutor itself is a thin forwarder to this class, so the two APIs have identical behaviour.
[SupportedOSPlatform("windows")]
public static class Executor
{
public static Result Execute(string comAssemblyPath, string manifestPath, Action action);
public static Result Execute(ICollection<ComPathDescriptor> comPathDescriptors, Action action);
public static ComObjectCreationResult<T> Create<T>(string comAssemblyPath, string manifestPath, Func<T> factory)
where T : class;
public static ComObjectCreationResult<T> Create<T>(ICollection<ComPathDescriptor> comPathDescriptors, Func<T> factory)
where T : class;
public static Result Free<T>(ComObjectHandle<T> comObjectHandle) where T : class;
}
Result Class
public class Result
{
public bool Success { get; set; }
public Exception? Exception { get; set; }
}
ComObjectCreationResult<T> Class
public sealed class ComObjectCreationResult<T> : Result
where T : class
{
public ComObjectHandle<T>? Value { get; set; }
}
ComObjectHandle<T> Class
Starting with v3.0.0 the handle implements IDisposable, so the simplest way to release it is a using block.
Calling Dispose() is equivalent to calling executor.Free(handle) and is idempotent.
[SupportedOSPlatform("windows")]
public sealed class ComObjectHandle<T>
: IDisposable
where T : class
{
public T? ComObject { get; internal set; }
public bool IsReleased { get; }
public void Dispose();
}
ComPathDescriptor Class
public sealed class ComPathDescriptor
{
public ComPathDescriptor(string comAssemblyPath, string comManifestPath);
public string ComAssemblyPath { get; }
public string ComManifestPath { get; }
}
ActCtxWrongSizeException Class
Thrown when the internal activation-context structure size does not match the current process architecture.
π― Best Practices
- β
Always check
Result.Successbefore assuming COM execution succeeded - β Use absolute paths for COM DLL and manifest files
- β Keep manifests alongside DLLs for easy deployment
- β Handle exceptions - COM calls can fail in various ways
- β Test on target platform - COM behavior can vary by Windows version
- β οΈ Avoid long-running operations in the action delegate
- β οΈ Be aware of STA threading model requirements
π Requirements
- OS: Windows (uses Windows-specific APIs: kernel32.dll, user32.dll). The public surface is annotated with
[SupportedOSPlatform("windows")]on .NET 8+ so cross-platform analyzers (CA1416) will flag misuse. - Framework: .NET Standard 2.0, .NET 8.0 / 9.0 / 10.0, .NET Framework 4.6.2 / 4.7 / 4.7.1 / 4.7.2 / 4.8 / 4.8.1
- Architecture: x86, x64 (defined in your project configuration)
π©Ί Troubleshooting
Common Issues
Issue: ActCtxWrongSizeException
Solution: This indicates a platform mismatch. Ensure your target platform (x86/x64) matches your COM DLL architecture.
Issue: Win32Exception when creating context
Solution: Check that:
- COM DLL path is valid and file exists
- Manifest file path is valid and file exists
- Manifest XML is well-formed
- CLSID in manifest matches the COM component
Issue: COM object creation fails silently
Solution: Verify the manifest progid and clsid match your COM component registration.
Detecting leaked ComObjectHandle<T> in production
If a caller forgets to Dispose() / Executor.Free a ComObjectHandle<T>, its finalizer emits a warning-level event on the System.Diagnostics.Tracing.EventSource named AdaskoTheBeAsT.Interop.COM (Event ID 1, HandleLeaked, payload: short type name). The signal is visible in Release builds and to out-of-process diagnostic tools.
Collect from the command line with dotnet-trace:
dotnet tool install --global dotnet-trace
dotnet-trace collect --providers AdaskoTheBeAsT.Interop.COM --process-id <pid>
Or subscribe in-process via EventListener:
using System.Diagnostics.Tracing;
public sealed class LeakedHandleListener : EventListener
{
private readonly ILogger<LeakedHandleListener> _logger;
public LeakedHandleListener(ILogger<LeakedHandleListener> logger) => _logger = logger;
protected override void OnEventSourceCreated(EventSource source)
{
if (source.Name == "AdaskoTheBeAsT.Interop.COM")
{
EnableEvents(source, EventLevel.Warning);
}
}
protected override void OnEventWritten(EventWrittenEventArgs e)
{
if (e.EventId == 1 && e.Payload is { Count: > 0 })
{
_logger.LogWarning("Leaked ComObjectHandle<{Type}>", e.Payload[0]);
}
}
}
Any non-zero rate of HandleLeaked events is a bug in the consuming code β either add a using statement around the handle or call Executor.Free explicitly on the creating thread.
π§ͺ Building from Source
git clone https://github.com/AdaskoTheBeAsT/AdaskoTheBeAsT.Interop.COM.git
cd AdaskoTheBeAsT.Interop.COM
dotnet restore
dotnet build
dotnet test
The project includes:
- C# library (
src/AdaskoTheBeAsT.Interop.COM) - Native COM example (
src/NativeCOM) - Unit tests (
test/unit/AdaskoTheBeAsT.Interop.COM.Test)
β Code Quality
This project maintains high code quality standards with:
- 20+ static analyzers (StyleCop, Roslynator, SonarAnalyzer, etc.)
- Nullable reference types enabled
- Comprehensive documentation
- Unit test coverage
- Strict warning-as-error policy
π Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Ensure all analyzers pass
- Add unit tests for new functionality
- Submit a pull request
π Changelog
Historical design decisions are captured as ADRs in docs/adr/.
v3.0.0
Breaking only in the sense that new analyzer warnings may surface in consumer code; the runtime API is source-compatible with v2.1.0.
- Added β
ComObjectHandle<T>implementsIDisposable. Preferusing var handle = creation.Value!;over the explicitExecutor.Free(handle)call. A diagnostic-only finalizer emits a warning-levelHandleLeakedevent on theAdaskoTheBeAsT.Interop.COMEventSourcein all build flavours if a handle was never released, and additionally raisesDebug.Failwhen a debugger is attached. (ADR-0012, ADR-0018) - Added β
ComInteropEventSource(EventSourcename:AdaskoTheBeAsT.Interop.COM). Subscribe withdotnet-trace collect --providers AdaskoTheBeAsT.Interop.COMor an in-processEventListenerto detect leakedComObjectHandle<T>in production. (ADR-0018) - Added β
IComExecutorinterface andComExecutorclass, now the recommended API for all new code. Register asservices.AddSingleton<IComExecutor, ComExecutor>();and depend onIComExecutor. The staticExecutorclass is retained for source-level compatibility with v2.x but is demoted to "legacy" in the documentation. (ADR-0013) - Added β
[SupportedOSPlatform("windows")]onExecutor,ComObjectHandle<T>,IComExecutor, andComExecutorfor TFMs that support it. Removed the in-method#pragma warning disable CA1416. (ADR-0014) - Added β Target frameworks:
.NET 10.0,.NET Framework 4.6.2,4.7,4.7.1,4.7.2,4.8,4.8.1. The full matrix is nownetstandard2.0,net462,net47,net471,net472,net48,net481,net8.0,net9.0,net10.0. (ADR-0007) - Added β
ComObjectHandle<T>.IsReleasedis nowpublic(wasinternal). - Added β GitHub Actions CI workflow using the shared reusable pipeline. (ADR-0015)
- Added β Expanded test suite from 7 to 63 tests across 9 test classes (
ComPathDescriptorTest,ActCtxWrongSizeExceptionTest,ResultTest,ExecuteValidationTest,ComObjectHandleDisposeTest,ComExecutorTest,ComInteropEventSourceTest,MessagePumpTest). Local line coverage ofAdaskoTheBeAsT.Interop.COM.dllis now 92.8 % (the remaining ~7 % are native-failurecatch (Exception ex)branches reachable only via fault-injection). - Changed β The managed library now builds as
AnyCPU(previouslyx86). 64-bit host processes can finally load the package. The existingActCtxWrongSizeExceptionguard continues to verify the correctACTCTXlayout at runtime for the current bitness. (ADR-0016) - Fixed β
NativeMethodsandActCtxWrongSizeExceptionnow compile cleanly on every non-NET8+ target (previously a recent TFM addition broke the .NET Framework builds). - Docs β Added
docs/adr/with architecture decision records for every significant choice since v1.0.0.
v2.1.0 (2026-04-07)
- Added β
Executor.Create<T>/Executor.Free<T>lifetime API,ComObjectHandle<T>,ComObjectCreationResult<T>. (ADR-0010) - Added β STA message pumping via
NativeMethods.PumpPendingMessagesaround every public API path. (ADR-0011) - Docs β Expanded README with scheduler/Threading samples.
v2.0.0 (2025-08-10 / 2025-11-22)
- Added β
ComPathDescriptorplus collection overloads ofExecute. (ADR-0009) - Added β
.NET 8.0/.NET 9.0target frameworks withLibraryImportsource-generated P/Invoke. (ADR-0008)
v1.0.0 (2023-12-09)
- Initial public release: static
Executor,Resultpattern,netstandard2.0target, internalNativeMethodswrappingkernel32/user32. (ADR-0002, ADR-0003, ADR-0004, ADR-0005)
π Migration Guide: v2.x β v3.0
The public API is source-compatible. No symbol has been removed or renamed. The following notes help you adopt the v3.0 idioms and explain the observable changes.
1. Prefer IComExecutor over the static Executor
IComExecutor (implemented by ComExecutor) is now the primary API. It is functionally identical to the static Executor but can be injected and mocked. Migrate at your own pace β the static API stays available indefinitely β but every new class you write should depend on IComExecutor.
Old:
var result = Executor.Execute(dll, manifest, Work);
New:
public sealed class MyService
{
private readonly IComExecutor _executor;
public MyService(IComExecutor executor) => _executor = executor;
public void Run() => _executor.Execute(dll, manifest, Work);
}
// Composition root:
services.AddSingleton<IComExecutor, ComExecutor>();
2. Prefer using over executor.Free
Old:
var creation = _executor.Create(dll, manifest, () => new Foo());
var handle = creation.Value!;
try
{
Use(handle.ComObject!);
}
finally
{
_executor.Free(handle);
}
New (recommended):
var creation = _executor.Create(dll, manifest, () => new Foo());
using var handle = creation.Value!;
Use(handle.ComObject!);
Both forms are valid and equivalent. Dispose simply calls Executor.Free under the hood.
3. Handle the new CA1416 warning on non-Windows projects
Because Executor, IComExecutor, ComExecutor, and ComObjectHandle<T> are now annotated with [SupportedOSPlatform("windows")], consumers that target frameworks like net8.0 (without TargetPlatformIdentifier=Windows) will start to see CA1416 warnings at the call-sites. Fix options, in order of preference:
- Add
<TargetFramework>net8.0-windows</TargetFramework>(or the equivalent platform-qualified TFM) to the calling project. This is the correct, honest expression of the dependency. - Guard calls with
if (OperatingSystem.IsWindows()) { ... }. - Annotate the calling member with
[SupportedOSPlatform("windows")].
4. Unit-test code that depends on IComExecutor
Because IComExecutor is an interface, Moq / NSubstitute / your favourite substitution library can replace it directly:
var mock = new Mock<IComExecutor>();
mock.Setup(e => e.Execute(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Action>()))
.Callback<string, string, Action>((_, _, a) => a())
.Returns(new Result { Success = true });
var sut = new MyService(mock.Object);
// assert...
There is no pressure to migrate existing call-sites that use the static Executor; both styles coexist indefinitely.
5. Watch for Debug.Fail messages on leaked handles
If you forget to call Dispose/Executor.Free, the finalizer will emit Debug.Fail("ComObjectHandle<...> was not disposed...") in Debug builds. Treat these as bug reports, not as runtime errors β Release builds simply ignore the message.
6. Target framework changes
- If you were installing the
netstandard2.0DLL into a .NET Framework 4.6.2-4.8.1 app, v3.0 now ships dedicated DLLs for each of those TFMs. NuGet will pick the best match automatically; no action required. .NET 10.0is available as a first-class target.
ποΈ Building native COM payloads for 32-bit and 64-bit hosts
Registration-free COM requires three artefacts that match the bitness of the host process: the native COM DLL (produced by your C++ project), the side-by-side manifest (processorArchitecture="x86" vs "amd64"), and the managed interop assembly (Interop.<Lib>.dll) produced by TlbImp.exe. The .NET library in this repository is AnyCPU β it will load in either bitness β but the native DLL is arch-specific. This section documents two patterns you can use when you need to ship or test your COM surface on both architectures.
The sample in src/NativeCOM (built through NativeCOM.slnx) and the test project (test/unit/AdaskoTheBeAsT.Interop.COM.Test) follow Option A out of the box.
Option A - one agnostic interop assembly
Interop.<Lib>.dll is metadata-only. It contains COM interop attributes, IIDs, CLSIDs, and type declarations, but zero executable code. If you pass /machine:Agnostic to TlbImp, you get a pure MSIL assembly that loads from both 32-bit and 64-bit processes. Only the native DLL and the manifest differ between arches.
Generate the agnostic interop assembly once (the existing generatelib.bat in this repo does exactly this):
"C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\x64\TlbImp.exe" ^
.\x86\Debug\NativeCOM.dll ^
/out:.\x86\Debug\Interop.NativeCOM.dll ^
/namespace:NativeCOM ^
/machine:Agnostic
Then route the arch-specific pieces with TFM conditions in your csproj:
<PropertyGroup Condition="$(TargetFramework.StartsWith('net4'))">
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<Reference Include="Interop.NativeCOM">
<HintPath>..\..\..\x86\Debug\Interop.NativeCOM.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup Condition="$(TargetFramework.StartsWith('net4'))">
<None Include="..\..\..\x86\Debug\NativeCOM.dll">
<Link>NativeCOM.dll</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="..\..\..\manifest\NativeCOM\NativeCOM.manifest">
<Link>NativeCOM.manifest</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup Condition="!$(TargetFramework.StartsWith('net4'))">
<None Include="..\..\..\x64\Debug\NativeCOM.dll">
<Link>NativeCOM.dll</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="..\..\..\manifest\NativeCOM\NativeCOM.x64.manifest">
<Link>NativeCOM.manifest</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
The two manifests differ only in processorArchitecture:
<assemblyIdentity name="NativeCOM" version="1.0.0.0" type="win32" processorArchitecture="x86"/>
<assemblyIdentity name="NativeCOM" version="1.0.0.0" type="win32" processorArchitecture="amd64"/>
Verify the interop assembly is truly architecture-neutral:
corflags .\x86\Debug\Interop.NativeCOM.dll
# Expected:
# ILONLY : 1
# 32BITREQ : 0
# 32BITPREF : 0
Pros - one interop assembly to ship and track in source control. No arch-matching logic in the consuming csproj beyond the native DLL and manifest pair.
Cons - none of any consequence for metadata-only interop assemblies. If your TlbImp-generated assembly ever had to do platform-specific marshalling (extremely rare and usually a sign your IDL uses non-portable types like raw LONG_PTR in public signatures), the agnostic flag would hide the mismatch.
Option B - two arch-specific interop assemblies
If you prefer explicit per-arch artefacts (sometimes required by compliance tooling or when your IDL genuinely has architecture-dependent types), build two separate Interop.<Lib>.dll copies - one per bitness. This requires the COM DLL's embedded type library to be emitted with the correct SYSKIND, otherwise TlbImp /machine:X64 will fail with TI2010: A single valid machine type compatible with the input type library must be specified.
Step 1 - make MIDL emit a 64-bit TLB for x64 configurations. Open NativeCOM.vcxproj (or your own C++ project), locate the <ItemDefinitionGroup> entries for the x64 configurations, and add <TargetEnvironment>X64</TargetEnvironment> inside the <Midl> block:
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Midl>
<TargetEnvironment>X64</TargetEnvironment>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<MkTypLibCompatible>false</MkTypLibCompatible>
</Midl>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Midl>
<TargetEnvironment>X64</TargetEnvironment>
</Midl>
</ItemDefinitionGroup>
For the Win32 configurations either leave <TargetEnvironment> unset (the default produces a 32-bit TLB) or set it explicitly to Win32.
Step 2 - rebuild the native DLL for both platforms. NativeCOM.slnx in this repository carries both x64 and Win32 configurations; run the build in Visual Studio (or a Developer Command Prompt) for each arch so you end up with x86\Debug\NativeCOM.dll and x64\Debug\NativeCOM.dll whose embedded TLBs have the matching SYSKIND.
Step 3 - run TlbImp twice, once per arch. Point it at each arch-specific DLL and emit two arch-stamped interop assemblies:
"C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\x64\TlbImp.exe" ^
.\x86\Debug\NativeCOM.dll ^
/out:.\x86\Debug\Interop.NativeCOM.dll ^
/namespace:NativeCOM ^
/machine:X86
"C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\x64\TlbImp.exe" ^
.\x64\Debug\NativeCOM.dll ^
/out:.\x64\Debug\Interop.NativeCOM.dll ^
/namespace:NativeCOM ^
/machine:X64
Verify the two assemblies have the 32BITREQ flag set correctly:
corflags .\x86\Debug\Interop.NativeCOM.dll # 32BITREQ : 1
corflags .\x64\Debug\Interop.NativeCOM.dll # 32BITREQ : 0 (x64 only)
Step 4 - TFM-condition the Reference as well. Unlike Option A, the interop assembly now differs per arch, so extend the TFM conditions to both the native payload, manifest, and the managed reference:
<ItemGroup Condition="$(TargetFramework.StartsWith('net4'))">
<Reference Include="Interop.NativeCOM">
<HintPath>..\..\..\x86\Debug\Interop.NativeCOM.dll</HintPath>
</Reference>
<None Include="..\..\..\x86\Debug\NativeCOM.dll">
<Link>NativeCOM.dll</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="..\..\..\manifest\NativeCOM\NativeCOM.manifest">
<Link>NativeCOM.manifest</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup Condition="!$(TargetFramework.StartsWith('net4'))">
<Reference Include="Interop.NativeCOM">
<HintPath>..\..\..\x64\Debug\Interop.NativeCOM.dll</HintPath>
</Reference>
<None Include="..\..\..\x64\Debug\NativeCOM.dll">
<Link>NativeCOM.dll</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="..\..\..\manifest\NativeCOM\NativeCOM.x64.manifest">
<Link>NativeCOM.manifest</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
Pros - explicit, auditable per-arch artefacts; each interop assembly is stamped with the expected /machine: value; catches TLB/SYSKIND drift early (if the C++ build is misconfigured, the TI2010 error fires at TlbImp time rather than at runtime).
Cons - two interop assemblies to version-control and keep in sync; a vcxproj configuration mistake (forgetting <TargetEnvironment>X64</TargetEnvironment> on one configuration) surfaces as the TI2010 error.
Which option to pick
For most registration-free COM scenarios Option A is the simpler choice and is what this repository uses for its sample test harness. Pick Option B when:
- Your IDL contains architecture-dependent signatures (raw
LONG_PTR,SIZE_T, inline pointer arithmetic exposed across COM boundaries). - A downstream policy requires per-architecture managed artefacts (for example, a signed-assembly policy that stamps each binary with an explicit machine type).
- You want build-time detection of TLB/SYSKIND drift in your native project.
In both options the managed AdaskoTheBeAsT.Interop.COM library itself stays AnyCPU - only the native payloads, manifests, and interop assemblies (when using Option B) are split by arch.
π License
This project is licensed under the MIT License β see the LICENSE file for details.
π€ Credits
Created and maintained by Adam PluciΕski.
π Related Resources
- π Registration-Free COM Interop
- π Activation Contexts
- π Side-by-Side Assemblies
- π
TlbImp.exereference - π§ This library's ADR index
Questions or issues? π Open an issue on GitHub or start a discussion β PRs welcome too. π¬
<p align="center"> Built for the kind of code that calls into a <strong>20-year-old ActiveX control</strong>, <strong>forgets to register anything</strong>, and <strong>still ships on Monday</strong>. β¨<br/> Made with β€οΈ (and a lot of coffee β) by <a href="https://github.com/AdaskoTheBeAsT">AdaskoTheBeAsT</a>. </p>
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 is compatible. net463 was computed. net47 is compatible. net471 is compatible. net472 is compatible. net48 is compatible. net481 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.6.2
- No dependencies.
-
.NETFramework 4.7
- No dependencies.
-
.NETFramework 4.7.1
- No dependencies.
-
.NETFramework 4.7.2
- No dependencies.
-
.NETFramework 4.8
- No dependencies.
-
.NETFramework 4.8.1
- No dependencies.
-
.NETStandard 2.0
- No dependencies.
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.