AdaskoTheBeAsT.Interop.Execution.Hosting
2.1.0
dotnet add package AdaskoTheBeAsT.Interop.Execution.Hosting --version 2.1.0
NuGet\Install-Package AdaskoTheBeAsT.Interop.Execution.Hosting -Version 2.1.0
<PackageReference Include="AdaskoTheBeAsT.Interop.Execution.Hosting" Version="2.1.0" />
<PackageVersion Include="AdaskoTheBeAsT.Interop.Execution.Hosting" Version="2.1.0" />
<PackageReference Include="AdaskoTheBeAsT.Interop.Execution.Hosting" />
paket add AdaskoTheBeAsT.Interop.Execution.Hosting --version 2.1.0
#r "nuget: AdaskoTheBeAsT.Interop.Execution.Hosting, 2.1.0"
#:package AdaskoTheBeAsT.Interop.Execution.Hosting@2.1.0
#addin nuget:?package=AdaskoTheBeAsT.Interop.Execution.Hosting&version=2.1.0
#tool nuget:?package=AdaskoTheBeAsT.Interop.Execution.Hosting&version=2.1.0
AdaskoTheBeAsT.Interop
A focused interop toolbox for dedicated-thread execution, native library isolation, and COM-friendly workloads.
π¬ Code quality β SonarCloud
π Hello, interop friend
Interop code is fun, right up until it isn't. You know the signs:
- 𧬠a native library that secretly wants thread affinity
- π’ a COM component that quietly insists on STA + message pumping
- π§Έ an engine that loses its mind if two threads touch it at once
- β»οΈ a workload that needs explicit load / unload / recycle, not "hope the process survives"
AdaskoTheBeAsT.Interop.Execution is the reusable boilerplate you keep rewriting in every project: a dedicated worker thread (or a pool of them), a session it owns, a queue in front of it, and all the cancellation / disposal / telemetry plumbing that should just be a library by now. π¦
And now it is. β¨
Changelog
2.1.0 (Unreleased)
The version in Directory.Build.props is 2.1.0
for all three packages. This release updates dependencies and build tooling
without changing the library's public APIs or supported target frameworks.
- Updated Microsoft.Extensions dependency minimums to 10.0.12 for .NET 10 and .NET Framework, and 9.0.20 for .NET 9. .NET 8 dependencies are unchanged.
- Updated
System.Threading.ChannelsandSystem.Diagnostics.DiagnosticSourceminimums to 10.0.12 for .NET Framework. - Updated the .NET SDK to 10.0.401,
Meziantou.Analyzerto 3.0.231, andMicrosoft.CodeAnalysis.NetAnalyzersto 10.0.401. - Aligned test-project dependencies with the updated package versions.
See the full changelog for release history.
Why upgrade to 2.0.0?
2.0.0 makes worker execution and shutdown more reliable and raises the minimum
.NET Framework version to 4.7.2. Dropping
net462, net47, and net471 is a breaking change and the reason for the major
version. Existing integration APIs remain available on supported targets.
The comparison below is against the tagged 1.0.0 release.
| If your application... | What could go wrong in 1.0.0 | What 2.0.0 improves |
|---|---|---|
Uses ExecuteValueAsync under load |
A caller could return a pooled source while the worker still used it; delegate failures bypassed worker recovery. | Completion is the worker's last access to the item. Task and ValueTask requests share failure, cancellation, recycling, and outcome reporting. |
| Recycles native sessions | A request could report success before required teardown finished. | Awaiting the request includes recycle teardown. Cleanup failures become observable instead of looking like successful work. |
| Submits work before initialization finishes | An async submission could block its calling thread on session creation. | Submissions enqueue without waiting for creation and retain per-worker FIFO order. Startup failures settle the returned requests. |
| Shuts down from several places | A repeated or reentrant disposal could make an external caller think teardown was already complete. | Every external DisposeAsync() joins actual teardown, even after a synchronous disposal timeout. |
| Receives bursts of work | Admission was unbounded. | Opt into QueueCapacity to limit waiting requests and reject excess work explicitly, including during startup. |
| Needs a shutdown policy | Pending-work handling was not an explicit choice. | Choose Drain or CancelPending. Hosted shutdown also observes the host's cancellation deadline without canceling cleanup. |
| Relies on faults and tracing | Fault handlers ran on the owning thread before Fault was published; listener errors could disrupt execution. |
Fault state is latched before off-thread notification. Listener exceptions are contained, and spans use the submitting request's activity context. |
| Reuses options or custom pool schedulers | Later option mutations could alter live behavior; a scheduler could select a foreign concrete worker. | Workers snapshot configuration, pools reject non-members, and partial pool construction rolls back earlier workers. |
What is not new: pooled ExecuteValueAsync, DI/Hosting,
session recycling, schedulers, snapshots, and scoped diagnostics already shipped
in 1.0.0. This is a correctness and lifecycle upgrade, not a claim of faster
native execution or a new benchmark result.
Before upgrading: applications targeting .NET Framework 4.6.2, 4.7, or 4.7.1 must retarget to 4.7.2 or later, or remain on 1.0.0. Modern .NET 8, 9, and 10 targets are unchanged.
On supported targets, existing public signatures are retained. The new options
default to unlimited admission (QueueCapacity = 0) and Drain; applications
receive the fixes without opting into queue limits. Completion timing, event
ordering, and option validation do change, so review the
1.0.x to 2.0.0 migration guide before upgrading.
See the changelog for release details.
β¨ Why you'll love this
- Pooled ValueTask path.
ExecuteValueAsyncreusesIValueTaskSource<T>work items on every supported TFM, includingnet472, avoiding a per-request source/Task allocation when a pooled item is available. This is not a guarantee of zero total allocations. (Completion contract) - DI + Hosting. Use
AddExecutionWorker<TSession>()for plain DI, orAddExecutionWorkerHostedService<TSession>()to register both the worker and its hosted lifecycle. - π« Pluggable schedulers.
LeastQueuedandRoundRobinship in the box; bring your own viaIWorkerScheduler<TSession>. (ADR-0002) - π Batteries-included observability.
ActivitySource+Meterwith public constant names, ready for OpenTelemetry. (ADR-0003) - πͺ First-class Windows STA. Flip a boolean, get an STA worker thread on Windows; silently ignored elsewhere.
- β»οΈ Real session recycling. After N operations, after a failure, or both β your call.
- π‘οΈ Terminal-once faulting. When a worker goes bad, it says so once, loudly, via
WorkerFaultedβ no silent-dead-thread surprises. - 6 target frameworks.
net10.0,net9.0,net8.0,net481,net48,net472. .NET Framework 4.7.2 is the minimum for 2.0.0. - βοΈ Source Link + snupkg. Step into the library from your debugger without guessing.
π¦ Packages
| Package | What it gives you |
|---|---|
AdaskoTheBeAsT.Interop.Execution |
β Core: ExecutionWorker<TSession>, ExecutionWorkerPool<TSession>, IExecutionSessionFactory<TSession>, options, schedulers, diagnostics. |
AdaskoTheBeAsT.Interop.Execution.DependencyInjection |
π§© Microsoft.Extensions.DependencyInjection helpers: AddExecutionWorker<TSession>() / AddExecutionWorkerPool<TSession>() with IOptions<T> binding. |
AdaskoTheBeAsT.Interop.Execution.Hosting |
ποΈ Microsoft.Extensions.Hosting integration: IHostedService wrappers driving worker / pool lifetime from the generic host. |
β¬οΈ Install
dotnet add package AdaskoTheBeAsT.Interop.Execution
dotnet add package AdaskoTheBeAsT.Interop.Execution.DependencyInjection
dotnet add package AdaskoTheBeAsT.Interop.Execution.Hosting
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; in-box System.Threading.Channels + System.Diagnostics.DiagnosticSource. |
net9.0 |
β | Primary target. |
net8.0 |
β | Primary target. |
net481 |
β | Windows desktop; System.Threading.Channels + System.Diagnostics.DiagnosticSource via NuGet + IsExternalInit polyfill. |
net48 |
β | Same as above. |
net472 |
β | Same as above. |
This six-target matrix applies to all three packages in 2.0.0.
Removed: net462 (.NET Framework 4.6.2), net47 (4.7), and net471 (4.7.1).
Retarget affected projects and deployment environments to .NET Framework 4.7.2
or later before installing 2.0.0; see the migration guide.
CI enables TreatWarningsAsErrors=true, ContinuousIntegrationBuild=true, and
Deterministic=true. All four test projects target this same matrix.
π‘ The core idea
Instead of every interop-heavy engine reinventing:
- a queue π
- a worker thread π§΅
- startup synchronisation π
- session lifetime β³
- disposal logic ποΈ
- failure / recycle behaviour β»οΈ
...you park that generic machinery in ExecutionWorker<TSession> or ExecutionWorkerPool<TSession>. Your engine becomes a thin adapter that answers three questions:
- π± How do I create a session?
- π₯ How do I dispose a session?
- π οΈ What work should run on that session?
That's it. The rest is the library's problem now.
ββββββββββββββββββββββββββββββββ
ExecuteAsync(x) βββΆ β Channel<ExecutionWorkItem> β
β (multi-writer, 1 reader) β
ββββββββββββββββ¬ββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββ
β Dedicated Thread β
β owns ONE TSession β βββ STA on Windows
β runs work in FIFO β if you ask
βββββββββββββ¬ββββββββββββ
β
βΌ
βββββββββββββββ
β TSession β (native libs, COM, ...)
βββββββββββββββ
π Main types
βοΈ ExecutionWorker<TSession>
A single dedicated background thread that owns one TSession. Submitted work runs sequentially in FIFO order. Implements IDisposable and IAsyncDisposable.
Owns π
- a multi-writer / single-reader
Channelof work items - one dedicated background
Thread(optionally STA on Windows) - startup / shutdown lifecycle (
InitializeAsync(CancellationToken)+ syncInitialize) - configurable draining or cancellation of pending items on shutdown
- session reuse + session recycle after failure or after N operations
- observability via
Name,IsFaulted,Fault,QueueDepth,WorkerFaulted, and the uniformGetSnapshot()
βοΈβοΈβοΈβοΈ ExecutionWorkerPool<TSession>
Fan-out pool of ExecutionWorker<TSession> instances. Each pool worker owns a private session and a private queue; a pluggable IWorkerScheduler<TSession> picks which worker receives each submission.
Owns π
- multiple
ExecutionWorker<TSession>instances - pluggable work distribution (see Scheduling below)
- one session per worker (ideal for isolated native DLL sets)
- separate worker-owned sessions (process-global native state still needs adapter-level isolation)
- parallel initialization and parallel disposal
- aggregate observability (
QueueDepth,IsAnyFaulted,WorkerFaults, forwardedWorkerFaulted, and per-worker snapshots viaGetSnapshot().Workers)
π IExecutionSessionFactory<TSession>
public interface IExecutionSessionFactory<TSession>
where TSession : class
{
TSession CreateSession(CancellationToken cancellationToken);
void DisposeSession(TSession session);
}
Creates the thread-affine session (loading native libs, initialising modules) and disposes / unloads it. Both methods run on the dedicated worker thread.
ποΈ ExecutionWorkerOptions
Name, UseStaThread, MaxOperationsPerSession (0 = unlimited), DisposeTimeout (default Timeout.InfiniteTimeSpan), Diagnostics (scoped ExecutionDiagnostics instance β defaults to a process-wide Shared singleton). Parameterless ctor + positional ctor + public setters so it binds cleanly via IOptions<T>.
QueueCapacity limits requests waiting for startup or execution (0 = unlimited).
ShutdownMode selects Drain (default) or CancelPending. Options are validated
and copied when the worker is constructed; later mutations do not reconfigure it.
ποΈ ExecutionWorkerPoolOptions
WorkerCount, Name, UseStaThread, MaxOperationsPerSession, DisposeTimeout, SchedulingStrategy (default LeastQueued), Diagnostics. Same binding story.
QueueCapacity and ShutdownMode apply to each worker. Pool options are also
copied at construction. Capacity is per worker, not a shared pool-wide limit.
ποΈ ExecutionRequestOptions
Per-call knob: RecycleSessionOnFailure (default false).
πͺ STA behavior
If UseStaThread: true is set:
- β
On Windows, the worker thread is configured as
STAviaSetApartmentState(ApartmentState.STA)(guarded byOperatingSystem.IsWindows()onnet5+andPlatformID.Win32NTon older TFMs). - π€· On non-Windows, the flag is silently ignored.
That makes the option safe for cross-platform callers that want "STA when possible" behaviour.
The worker does not run a COM or UI message pump. STA alone is insufficient for components that require one. Separate worker sessions also do not isolate process-global native state; the adapter must supply any required process-wide serialization or process isolation.
Startup, admission, cancellation, and shutdown
- Submit synchronous delegates only. An async lambda can escape the owning thread and outlive its session. Do not synchronously wait for nested work on the same worker.
- Async submissions return without waiting for initial session creation. Work is queued in FIFO order on each worker, including during startup.
InitializeAsync(token)cancels only that caller's wait. Shared startup continues for other callers. Disposing the worker separately requests cancellation of initial session creation.- A full
QueueCapacityfaults the submission withInvalidOperationException; it does not block or silently drop work. The executing request is excluded from the limit. A pool does not retry another worker after capacity rejection. - A request canceled before execution is skipped when dequeued. Once running, its delegate must observe its own token. Neither request cancellation nor shutdown forcibly interrupts managed or native code.
Draincompletes queued work on an available session.CancelPendingskips requests not yet started. Both let running delegates finish before teardown. If initial creation is canceled or fails, queued requests cannot run and are completed with cancellation or failure.- Every external
DisposeAsync()joins the same actual teardown. A call from the owning worker only requests shutdown, avoiding a self-deadlock. SynchronousDispose()can abandon its wait atDisposeTimeout; cleanup continues and a laterDisposeAsync()still joins it. - Hosted
StopAsync(token)uses the host deadline to bound its wait, not the cleanup itself. If the token fires before teardown completes, awaitingStopAsyncthrowsOperationCanceledException. Container disposal may still wait for a blocked native call.
Hard deadlines and recovery from a hung native call require a separate process. See ADR-0010 for the ownership and compatibility decisions.
π Quick example
This complete console example uses top-level statements on modern .NET. The
session is a stub; replace Render and the factory cleanup with your native API.
using System.Text;
using AdaskoTheBeAsT.Interop.Execution;
CancellationToken cancellationToken = CancellationToken.None;
// 1. Spin up the worker.
await using var worker = new ExecutionWorker<NativeSession>(
new NativeSessionFactory(),
new ExecutionWorkerOptions(
name: "Native Render Worker",
useStaThread: true,
maxOperationsPerSession: 500));
await worker.InitializeAsync(cancellationToken);
// 2. Throw work at it. Returns when the work item completes.
byte[] bytes = await worker.ExecuteAsync(
(session, ct) =>
{
ct.ThrowIfCancellationRequested();
return session.Render("<h1>Hello</h1>");
},
new ExecutionRequestOptions(recycleSessionOnFailure: true),
cancellationToken);
Console.WriteLine($"Produced {bytes.Length} bytes.");
public sealed class NativeSession
{
public byte[] Render(string html) => Encoding.UTF8.GetBytes(html);
}
public sealed class NativeSessionFactory : IExecutionSessionFactory<NativeSession>
{
public NativeSession CreateSession(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return new NativeSession();
}
public void DisposeSession(NativeSession session)
{
// Free native handles here, on the owning worker thread.
}
}
Pooled ValueTask hot path
The concrete ExecutionWorker<TSession> and ExecutionWorkerPool<TSession>
types expose instance ExecuteValueAsync overloads backed by pooled
IValueTaskSource<T> work items on every supported TFM. Use the same synchronous
delegate as with ExecuteAsync; only the returned completion type changes.
The IExecutionWorker<TSession> and IExecutionWorkerPool<TSession> interfaces
expose ExecuteAsync, not ExecuteValueAsync. Keep using ExecuteAsync when
consuming those interfaces through DI.
byte[] bytes = await worker.ExecuteValueAsync(
(session, ct) =>
{
ct.ThrowIfCancellationRequested();
return session.Render("<h1>Hello</h1>");
},
cancellationToken: cancellationToken);
Await each returned ValueTask exactly once. If you need to share or repeatedly
await a result, use ExecuteAsync, or call AsTask() once and retain that Task.
Do not discard, double-await, or synchronously read an incomplete ValueTask.
Pooling avoids per-request source/Task allocations when an item can be reused; startup, pool misses, delegates, channels, continuations, and tracing can still allocate. The pooling API already existed in 1.0.0. The 2.0.0 improvement is safe completion ownership and the same recovery behavior as the Task path. See ADR-0010, which supersedes the lifecycle design in ADR-0007.
ππππ Pool: multiple workers
If one worker is not enough, use ExecutionWorkerPool<TSession>. Great fit when:
- π you have several native library copies in separate folders
- π¦ each worker should load its own isolated DLL set
- β»οΈ one failed worker session should recycle without touching the others
- ποΈ you want several dedicated threads, but still serialised execution per worker
await using var pool = new ExecutionWorkerPool<NativePoolSession>(
workerIndex => new NativePoolSessionFactory($@"c:\native\slot-{workerIndex + 1:D2}"),
new ExecutionWorkerPoolOptions(
workerCount: 4,
name: "Native Pool",
useStaThread: true,
maxOperationsPerSession: 250));
await pool.InitializeAsync(cancellationToken);
var result = await pool.ExecuteAsync(
(session, ct) => session.Render("<h1>Hello from pool</h1>"),
new ExecutionRequestOptions(recycleSessionOnFailure: true),
cancellationToken);
π‘ Tip: need every worker to share the exact same factory? There's a single-factory constructor overload too:
new ExecutionWorkerPool<T>(factory, options). Nice and tidy for stateless factories. (ADR-0008)
π¦ Scheduling
The pool ships with two built-in schedulers and a public IWorkerScheduler<TSession> seam if you need something bespoke.
| Built-in | Icon | Semantics |
|---|---|---|
LeastQueuedWorkerScheduler<TSession> (default) |
βοΈ | Picks the healthy worker with the smallest QueueDepth. Ties break via a shared rolling index so equal-depth workers rotate. Skips faulted workers. Early-exits when it finds a zero-depth worker. |
RoundRobinWorkerScheduler<TSession> |
π | Strict rotation across healthy workers via an Interlocked index. Skips faulted workers. |
Swap built-ins via options, or plug in a custom scheduler via the pool ctor:
// Option A: pick a built-in via options.
var opts = new ExecutionWorkerPoolOptions(
workerCount: 4,
schedulingStrategy: SchedulingStrategy.RoundRobin);
// Option B: inject a custom scheduler.
IWorkerScheduler<NativeSession> custom = new MyAffinityScheduler();
await using var pool = new ExecutionWorkerPool<NativeSession>(
workerIndex => new NativeSessionFactory(),
new ExecutionWorkerPoolOptions(workerCount: 4),
custom);
Rationale, trade-offs, and the faulted-worker contract are captured in ADR-0002.
π€ When to use what
1οΈβ£ Choose ExecutionWorker<TSession> when
- π the native engine is effectively process-global
- π₯΅ the library is known to be thread-sensitive
- β you want strict serialised access to one engine instance
- π you want exactly one owner thread
4οΈβ£ Choose ExecutionWorkerPool<TSession> when
- π€π€π€π€ you have isolated native copies per worker
- ποΈποΈ the library can run in parallel across separate worker-owned sessions
- π you want better throughput
- π§ you want one worker to recycle independently from the others
β»οΈ Session recycle story
You can choose to recycle the session:
- β after a failed request β
ExecutionRequestOptions.RecycleSessionOnFailure = true - π― after a fixed number of operations β
ExecutionWorkerOptions.MaxOperationsPerSession > 0 - β¨ or both
Set maxOperationsPerSession: 0 when you want unlimited session lifetime and only failure-based recycling.
Task and ValueTask calls use the same failure and recycle policy. Completion
is published only after any required session teardown. A teardown error after
a successful delegate faults the request and worker. If the delegate also
failed, its exception remains the request failure and Fault records the
terminal teardown error. A canceled request does not count as a successful
operation or trigger failure-based recycling.
π§© DI integration
using AdaskoTheBeAsT.Interop.Execution;
using AdaskoTheBeAsT.Interop.Execution.DependencyInjection;
services.AddSingleton<IExecutionSessionFactory<NativeSession>, NativeSessionFactory>();
services.AddExecutionWorker<NativeSession>(options =>
{
options.Name = "Native Render Worker";
options.UseStaThread = true;
options.MaxOperationsPerSession = 500;
});
// resolve IExecutionWorker<NativeSession> from DI and use it as usual
AddExecutionWorkerPool<TSession> is the pool-flavoured equivalent and binds IOptions<ExecutionWorkerPoolOptions>.
ποΈ Generic host integration
using AdaskoTheBeAsT.Interop.Execution.Hosting;
services.AddSingleton<IExecutionSessionFactory<NativeSession>, NativeSessionFactory>();
services.AddExecutionWorkerHostedService<NativeSession>(options =>
{
options.Name = "Native Render Worker";
options.UseStaThread = true;
});
The registration includes the worker and its IHostedService wrapper; you do
not need a separate AddExecutionWorker<TSession>() call. The wrapper drives
InitializeAsync on StartAsync and joins DisposeAsync on StopAsync.
In 2.0.0, a canceled stop token cancels only that wait, not worker cleanup.
Repeated stops join the same teardown, subject to each caller's token.
AddExecutionWorkerPoolHostedService<TSession> covers the pool.
π Observability
Every worker emits to an ActivitySource and Meter named AdaskoTheBeAsT.Interop.Execution (customisable per worker via ExecutionWorkerOptions.Diagnostics β see ADR-0009 for scoped emitters).
| Instrument | Kind | Tags |
|---|---|---|
ExecutionWorker.Execute |
π Activity (span) |
worker.name |
execution.worker.operations |
π Counter<long> |
worker.name, outcome β success / faulted / cancelled |
execution.worker.session_recycles |
π Counter<long> |
worker.name, reason β max_operations / failure |
execution.worker.queue_depth |
π ObservableGauge<int> |
worker.name |
All these identifiers are exposed as public const string on ExecutionDiagnosticNames, so telemetry pipelines can subscribe without hard-coding strings:
using AdaskoTheBeAsT.Interop.Execution;
using OpenTelemetry.Trace;
using OpenTelemetry.Metrics;
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddSource(ExecutionDiagnosticNames.SourceName))
.WithMetrics(m => m.AddMeter(ExecutionDiagnosticNames.SourceName));
When no activity listener is attached, StartActivity returns null and no
execution span is created. This does not guarantee allocation-free execution.
See ADR-0003 for why the identifiers are a public contract.
β οΈ Faulting semantics
ExecutionWorker<TSession> is terminal-once: a session creation or teardown
failure latches Fault before WorkerFaulted is queued to the thread pool.
Subscribers run off the owning thread; throwing subscribers are contained.
Notification is asynchronous and may arrive after disposal completes. The first
terminal exception wins, and the worker cannot be re-initialised.
A delegate exception alone faults that request, optionally recycling its session.
An OperationCanceledException is cancellation only when the request token was
canceled; otherwise it is a failure for both Task and ValueTask calls. Future
submissions rethrow a terminal fault synchronously unless disposal has already
been requested, in which case they throw ObjectDisposedException.
Disposal joins teardown without rethrowing terminal session errors. Inspect
Fault, IsFaulted, or WorkerFaulted for those errors. Diagnostic-listener
exceptions do not fail requests; execution spans use the submitting activity
as their parent.
Pool consumers observe the same contract aggregated: IsAnyFaulted, WorkerFaults, and a forwarded WorkerFaulted event carrying the originating worker name. π
π Migrating from 1.0.x
Start with the 2.0.0 migration guide. It covers retargeting, package updates, before/after behavior, copyable option examples, and an upgrade checklist.
- Retarget .NET Framework 4.6.2, 4.7, and 4.7.1 projects to 4.7.2 or later before upgrading. If you cannot retarget, remain on 1.0.0.
- On supported targets, keep your existing factory, worker/pool, and DI/Hosting APIs. Public signatures are retained from tagged 1.0.0, but framework support is not backward-compatible.
- Review request completion, fault-event ordering, cancellation, and repeated disposal. API compatibility does not mean identical runtime behavior.
- Configure options before constructing or resolving a worker. They are now
snapshotted, and
DisposeTimeoutvalues aboveint.MaxValuemilliseconds are rejected during validation. - Opt into
QueueCapacityorCancelPendingonly if your application needs them. Handle rejection/cancellation explicitly. - Do not use a timeout as proof that a native call stopped. Only completed
external
DisposeAsync()confirms teardown; inspectFaultseparately.
π§ͺ Build and test
dotnet build .\AdaskoTheBeAsT.Interop.slnx
dotnet test .\AdaskoTheBeAsT.Interop.slnx --no-build
| Project | Role |
|---|---|
test/unit/AdaskoTheBeAsT.Interop.Execution.Test |
π¬ Unit + behavioural (fault propagation, dispose idempotency, cancellation, telemetry smoke, scheduler contract). |
test/unit/AdaskoTheBeAsT.Interop.Execution.DependencyInjection.Test |
π§© DI registration, options binding, lifetime. |
test/unit/AdaskoTheBeAsT.Interop.Execution.Hosting.Test |
ποΈ IHostedService start/stop lifecycle, idempotent shutdown. |
test/integ/AdaskoTheBeAsT.Interop.Execution.IntegrationTest |
π€ Multi-threaded submission, STA on Windows, reentrant dispose, session recycling, zero-alloc ValueTask, snapshot surface, scoped diagnostics. |
All four test projects target the same six-framework matrix as the packages.
π Architecture Decision Records
Small, self-contained design decisions taken on this codebase live under docs/adr/. Start with the index. Highlights:
- π§ ADR-0002 β pluggable worker scheduler
- π·οΈ ADR-0003 β public diagnostic constants
- β‘ ADR-0007 β zero-allocation
ExecuteValueAsync - πΈ ADR-0008 β uniform snapshot surface
- π ADR-0009 β scoped
ExecutionDiagnostics - ADR-0010: 2.0.0 completion ownership and lifecycle
π Contributing
Found a bug? Got an idea? Spotted a typo that's been haunting you? π»
- π Open an issue describing the problem or the proposal.
- π οΈ Fork + branch (
feature/your-idea). - β
Run
dotnet build+dotnet testacross the full matrix. - β¨ Add/update tests and an ADR if the change is load-bearing.
- π Open a PR β the strict-build + CI will do the rest.
π Further reading
- π
wkhtml.mdβ WkHtml migration notes. - π
docs/adr/β design rationale for every recent change. - π
CHANGELOG.mdβ what landed when. - 1.0.x to 2.0.0 migration guide: retargeting, upgrade steps, and behavioral changes.
<p align="center"> Built for the kind of interop code that likes <strong>one owner thread</strong>, <strong>explicit lifecycle</strong>, and <strong>zero drama</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 | 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 Framework | net472 is compatible. net48 is compatible. net481 is compatible. |
-
.NETFramework 4.7.2
- AdaskoTheBeAsT.Interop.Execution (>= 2.1.0)
- AdaskoTheBeAsT.Interop.Execution.DependencyInjection (>= 2.1.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12 && < 11.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.12 && < 11.0.0)
-
.NETFramework 4.8
- AdaskoTheBeAsT.Interop.Execution (>= 2.1.0)
- AdaskoTheBeAsT.Interop.Execution.DependencyInjection (>= 2.1.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12 && < 11.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.12 && < 11.0.0)
-
.NETFramework 4.8.1
- AdaskoTheBeAsT.Interop.Execution (>= 2.1.0)
- AdaskoTheBeAsT.Interop.Execution.DependencyInjection (>= 2.1.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12 && < 11.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.12 && < 11.0.0)
-
net10.0
- AdaskoTheBeAsT.Interop.Execution (>= 2.1.0)
- AdaskoTheBeAsT.Interop.Execution.DependencyInjection (>= 2.1.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12 && < 11.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.12 && < 11.0.0)
-
net8.0
- AdaskoTheBeAsT.Interop.Execution (>= 2.1.0)
- AdaskoTheBeAsT.Interop.Execution.DependencyInjection (>= 2.1.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2 && < 9.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.1 && < 9.0.0)
-
net9.0
- AdaskoTheBeAsT.Interop.Execution (>= 2.1.0)
- AdaskoTheBeAsT.Interop.Execution.DependencyInjection (>= 2.1.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.20 && < 10.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.20 && < 10.0.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on AdaskoTheBeAsT.Interop.Execution.Hosting:
| Package | Downloads |
|---|---|
|
AdaskoTheBeAsT.WkHtmlToX.Hosting
Microsoft.Extensions.Hosting integration for AdaskoTheBeAsT.WkHtmlToX: AddWkHtmlToXHostedService() registers an IHostedService that drives the WkHtmlToX engine worker lifecycle through the generic host. |
GitHub repositories
This package is not used by any popular GitHub repositories.
## [2.1.0] - Unreleased
A dependency and build-tooling update for all three packages. Public APIs and
the six-target framework matrix are unchanged from 2.0.0.
### Changed
- Raised Microsoft.Extensions dependency minimums in the DependencyInjection
and Hosting packages from `10.0.11` to `10.0.12` for .NET 10 and .NET Framework,
and from `9.0.19` to `9.0.20` for .NET 9. Existing upper bounds and .NET 8
dependencies are unchanged.
- Raised `System.Threading.Channels` and `System.Diagnostics.DiagnosticSource`
dependency minimums from `10.0.11` to `10.0.12` for .NET Framework in the core
package, retaining the existing upper bounds.
- Aligned test-project dependency versions with the package updates.
- Updated the .NET SDK in `global.json` from `10.0.400` to `10.0.401`.
- Updated `Meziantou.Analyzer` from `3.0.217` to `3.0.231` and
`Microsoft.CodeAnalysis.NetAnalyzers` from `10.0.400` to `10.0.401`.