AdaskoTheBeAsT.Interop.Execution.DependencyInjection 1.0.0

dotnet add package AdaskoTheBeAsT.Interop.Execution.DependencyInjection --version 1.0.0
                    
NuGet\Install-Package AdaskoTheBeAsT.Interop.Execution.DependencyInjection -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AdaskoTheBeAsT.Interop.Execution.DependencyInjection" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AdaskoTheBeAsT.Interop.Execution.DependencyInjection" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="AdaskoTheBeAsT.Interop.Execution.DependencyInjection" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add AdaskoTheBeAsT.Interop.Execution.DependencyInjection --version 1.0.0
                    
#r "nuget: AdaskoTheBeAsT.Interop.Execution.DependencyInjection, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package AdaskoTheBeAsT.Interop.Execution.DependencyInjection@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=AdaskoTheBeAsT.Interop.Execution.DependencyInjection&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=AdaskoTheBeAsT.Interop.Execution.DependencyInjection&version=1.0.0
                    
Install as a Cake Tool

AdaskoTheBeAsT.Interop

A focused interop toolbox for dedicated-thread execution, native library isolation, and COM-friendly workloads.

NuGet NuGet NuGet License: MIT TFMs Warnings Deterministic

πŸ”¬ Code quality β€” SonarCloud

Quality Gate Status Coverage Maintainability Rating Reliability Rating Security Rating Bugs Vulnerabilities Code Smells Duplicated Lines (%) Technical Debt Lines of Code


πŸ‘‹ 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. ✨


✨ Why you'll love this

  • ⚑ Zero-allocation hot path. ExecuteValueAsync is backed by pooled IValueTaskSource<T> on every TFM (yes, even net462). No Task wrapping on the hot path. (ADR-0007)
  • 🧩 Drop-in DI + Hosting. services.AddExecutionWorker<TSession>() + AddExecutionWorkerHostedService<TSession>() and you're done.
  • πŸ’« Pluggable schedulers. LeastQueued and RoundRobin ship in the box; bring your own via IWorkerScheduler<TSession>. (ADR-0002)
  • πŸ”­ Batteries-included observability. ActivitySource + Meter with 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.
  • πŸ–₯️ 9 TFMs, all green. net10.0, net9.0, net8.0, net481, net48, net472, net471, net47, net462 β€” tested across the full matrix on every build.
  • ✏️ 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.
net471 βœ… Same as above.
net47 βœ… Same as above.
net462 βœ… Same as above.

Every cell is built with TreatWarningsAsErrors=true, ContinuousIntegrationBuild=true, Deterministic=true, and exercised in CI.


πŸ’‘ 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:

  1. 🌱 How do I create a session?
  2. πŸ₯€ How do I dispose a session?
  3. πŸ› οΈ 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 Channel of work items
  • one dedicated background Thread (optionally STA on Windows)
  • startup / shutdown lifecycle (InitializeAsync(CancellationToken) + sync Initialize)
  • cooperative 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 uniform GetSnapshot()

βš™οΈβš™οΈβš™οΈβš™οΈ 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)
  • per-worker isolation for native state
  • parallel initialization and parallel disposal
  • aggregate observability (QueueDepth, IsAnyFaulted, WorkerFaults, Workers, forwarded WorkerFaulted, uniform GetSnapshot())

🏭 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>.

πŸŽ›οΈ ExecutionWorkerPoolOptions

WorkerCount, Name, UseStaThread, MaxOperationsPerSession, DisposeTimeout, SchedulingStrategy (default LeastQueued), Diagnostics. Same binding story.

πŸŽ›οΈ ExecutionRequestOptions

Per-call knob: RecycleSessionOnFailure (default false).


πŸͺŸ STA behavior

If UseStaThread: true is set:

  • βœ… On Windows, the worker thread is configured as STA via SetApartmentState(ApartmentState.STA) (guarded by OperatingSystem.IsWindows() on net5+ and PlatformID.Win32NT on 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.


πŸš€ Quick example

using AdaskoTheBeAsT.Interop.Execution;

public sealed class NativeSession
{
    public byte[] Render(string html) => []; // call into your native lib here
}

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
    }
}

// 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) => session.Render("<h1>Hello</h1>"),
    new ExecutionRequestOptions(recycleSessionOnFailure: true),
    cancellationToken);

⚑ Zero-alloc ValueTask hot path

When the caller is already in ValueTask land (e.g. wrapping a native-interop async API) the instance ExecuteValueAsync overloads keep you in that domain with pooled IValueTaskSource<T> work items β€” no inner Task allocation, on every TFM:

int sessionId = await worker.ExecuteValueAsync(
    (session, _) => session.SessionId,
    cancellationToken: cancellationToken);

See ADR-0007 for the gritty details; ADR-0004 is kept as historical context.


🏭🏭🏭🏭 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.


🧩 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 IHostedService wrappers drive InitializeAsync on StartAsync and DisposeAsync on StopAsync, idempotent against double-stop. 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 listener is attached, StartActivity returns null and the instrumentation is allocation-free. ⚑

See ADR-0003 for why the identifiers are a public contract.


⚠️ Faulting semantics

ExecutionWorker<TSession> is terminal-once: when a work item throws a non-cancellation exception during startup or session creation β€” or when DisposeSession throws during shutdown β€” _fatalFailure latches, IsFaulted flips to true, and WorkerFaulted fires exactly once. Subsequent ExecuteAsync calls rethrow the original exception synchronously (unwrapped via ExceptionDispatchInfo). The worker cannot be re-initialised after faulting.

Pool consumers observe the same contract aggregated: IsAnyFaulted, WorkerFaults, and a forwarded WorkerFaulted event carrying the originating worker name. πŸ””


πŸ§ͺ 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 projects run across the full 9-target matrix.


πŸ“œ Architecture Decision Records

Small, self-contained design decisions taken on this codebase live under docs/adr/. Start with the index. Highlights:


πŸ™‹ Contributing

Found a bug? Got an idea? Spotted a typo that's been haunting you? πŸ‘»

  1. πŸ™ Open an issue describing the problem or the proposal.
  2. πŸ› οΈ Fork + branch (feature/your-idea).
  3. βœ… Run dotnet build + dotnet test across the full matrix.
  4. ✨ Add/update tests and an ADR if the change is load-bearing.
  5. πŸš€ 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.

<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 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 net462 is compatible.  net463 was computed.  net47 is compatible.  net471 is compatible.  net472 is compatible.  net48 is compatible.  net481 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on AdaskoTheBeAsT.Interop.Execution.DependencyInjection:

Package Downloads
AdaskoTheBeAsT.WkHtmlToX.DependencyInjection

Microsoft.Extensions.DependencyInjection helpers for AdaskoTheBeAsT.WkHtmlToX: AddWkHtmlToX() registers the engine, converters and the underlying execution worker as singletons without hooking into IHostedService.

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.

AdaskoTheBeAsT.Interop.Execution.Hosting

Microsoft.Extensions.Hosting integration for AdaskoTheBeAsT.Interop.Execution: IHostedService wrappers that drive ExecutionWorker / ExecutionWorkerPool startup and graceful drain through the generic host lifecycle. Targets net10.0, net9.0, net8.0, and net481/net48/net472/net471/net47/net462.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 195 4/19/2026

## [Unreleased]

### Added

#### `AdaskoTheBeAsT.Interop.Execution` (core)

- **Zero-allocation `ExecuteValueAsync` hot path on every supported TFM.**
 `ExecutionWorker<TSession>` and `ExecutionWorkerPool<TSession>` expose
 instance `ExecuteValueAsync` overloads backed by pooled
 `IValueTaskSource<TResult>` / `IValueTaskSource` work items
 (`ManualResetValueTaskSourceCore<T>`). The `IValueTaskSource` primitives
 ship natively from `net8.0+` and are available on `net462`..`net481`
 through the `System.Threading.Tasks.Extensions` NuGet facade (pulled
 in transitively by `System.Threading.Channels`), so the same zero-alloc
 code path runs on all nine TFMs β€” no `Task` β†’ `ValueTask` wrapper
 fallback anywhere, and no public `ExecutionWorkerValueTaskExtensions`
 static class to duplicate the API surface.
 See [`docs/adr/0007-zero-alloc-value-task-source.md`](docs/adr/0007-zero-alloc-value-task-source.md).
- **`ExecutionWorkerPool<TSession>` single-factory constructors.** Two new
 overloads accept a single `IExecutionSessionFactory<TSession>` that is
 shared across every pool worker, eliminating the `Func<int, factory>`
 boilerplate at call sites that do not need per-worker isolation. The
 original per-index factory constructors remain for callers that do.
 See [`docs/adr/0008-uniform-snapshot-surface.md`](docs/adr/0008-uniform-snapshot-surface.md).
- **Unified `Name` / `GetSnapshot` observability surface** on both worker
 and pool:
 - `IExecutionWorker<TSession>.Name` and `IExecutionWorkerPool<TSession>.Name`;
 - `ExecutionWorkerSnapshot` (public `readonly struct`) with `Name`,
   `QueueDepth`, `IsFaulted`, `Fault`;
 - `ExecutionWorkerPoolSnapshot` (public `readonly struct`) with `Name`,
   `WorkerCount`, aggregate `QueueDepth`, `IsAnyFaulted`, and an
   index-aligned `IReadOnlyList<ExecutionWorkerSnapshot> Workers`;
 - `IExecutionWorker<TSession>.GetSnapshot()` and
   `IExecutionWorkerPool<TSession>.GetSnapshot()` capture all fields in a
   single call so dashboards cannot observe inconsistent mixes of values.
- **Scoped `ExecutionDiagnostics`** public class replaces the previous
 process-wide static diagnostics class. `ExecutionDiagnostics.Shared` is a
 lazy singleton that continues to emit under
 `ExecutionDiagnosticNames.SourceName`, so every existing OpenTelemetry /
 `MeterListener` / `ActivityListener` subscriber by that name is unaffected.
 New overloads on both `ExecutionWorkerOptions` and
 `ExecutionWorkerPoolOptions` accept a custom `ExecutionDiagnostics` scope;
 workers wired to a custom scope do not contribute measurements to
 `Shared`, removing the parallel-test-host race on the previously shared
 `Counter<long>` / `ObservableGauge<int>` state.
 See [`docs/adr/0009-scoped-execution-diagnostics.md`](docs/adr/0009-scoped-execution-diagnostics.md).

### Quality

- **SonarCloud PR sweep (round 2) β€” 29 additional INFO-severity findings
 addressed.** Modern-BCL suggestions that target `net6.0+` / `net7.0+` /
 `net8.0+` are adopted on the matching TFMs via `#if` preprocessor
 guards while legacy `net462`..`net481` keeps the explicit-throw fallback,
 so nothing is regressed on older frameworks:
 - **`CA1510` (13 findings).** `ArgumentNullException.ThrowIfNull` on
   `net8.0+`, explicit `throw new ArgumentNullException(nameof(x))` on
   `net462`..`net7.0` in `ExecutionWorker`, `ExecutionWorkerPool`,
   `ExecutionDiagnostics`, `RoundRobinWorkerScheduler`,
   `LeastQueuedWorkerScheduler`, `ExecutionWorkerServiceCollectionExtensions`,
   `ExecutionWorkerHostingExtensions`.
 - **`CA1513` (2 findings).** `ObjectDisposedException.ThrowIf` on
   `net7.0+`, explicit `throw new ObjectDisposedException(TypeName)`
   otherwise β€” `ExecutionWorker.ThrowIfDisposed`,
   `ExecutionWorkerPool.ThrowIfDisposed`.
 - **`CA2263` (1 finding).** `Enum.IsDefined<TEnum>(TEnum)` generic
   overload on `net5.0+`, non-generic `Enum.IsDefined(Type, object)`
   otherwise β€” `ExecutionWorkerPoolOptions` validation.
 - **`MA0042` / `MA0040` (src β€” 2 findings).** Extended existing
   `VSTHRD002` / `VSTHRD103` scoped pragmas in `ExecutionWorker` to
   cover the related Meziantou cancellation rules, with the reasoning
   comments expanded to document why the sync `Cancel()` and the
   no-token `disposeTask.Wait(timeout)` are the correct primitives
   inside the non-async `DisposeAsync` / sync-disposal paths.
 - **Test-side `MA0040` (7 findings).** Passed
   `TestContext.Current.CancellationToken` (xUnit v3, `net8.0+`) or
   `CancellationToken.None` (xUnit v2, legacy TFMs) through a
   preprocessor-guarded local to the relevant `ExecuteAsync` /
   `ExecuteValueAsync` calls in `ExecutionWorkerTest`. The two
   `ManualResetEventSlim.Wait(TimeSpan)` sites inside a session-factory
   delegate in `ExecutionWorkerPoolTest` stay under a scoped
   `MA0040` pragma because the callback signature has no token.
 - **Test-side `MA0042` / `RCS1261` (3 findings).** The two intentionally
   synchronous `Fact` methods in `ExecutionWorkerPoolTest`
   (`IsAnyFaulted_ShouldBeFalseForHealthyPool`,
   `SetWorkerThreadForTesting_ShouldThrowWhenWorkerThreadIsNull`) and
   the single sync assertion block in `ExecutionWorkerTest`
   (`InitializeAsync_ShouldReturnCancelledTaskForPreCancelledTokenAsync`)
   keep `using var` + `Cancel()` under scoped pragmas because
   `await using` / `CancelAsync` would force them to become async and
   invalidate the sync-return assertions.
 - **`IDE0350` (2 findings).** Two `ActivityListener.Sample` ref-lambdas
   in `ExecutionWorkerTest` stay under a scoped pragma because the
   analyzer's suggested simplification cannot compile against the
   `SampleActivity` delegate signature (`ref ActivityCreationOptions<ActivityContext>`).

- **SonarCloud PR sweep β€” 123 INFO-severity findings addressed.** All
 open code-smells reported on the pull request were either fixed or
 intentionally suppressed with explanatory comments. Highlights:
 - **Async-dispose preference (60 findings: `MA0042` + `RCS1261`).**
   Converted `using var worker = new ExecutionWorker<T>(...)` and
   `using var workerPool = new ExecutionWorkerPool<T>(...)` to
   `await using` in every async test method. The two intentional
   `Dispose()` timeout tests keep sync disposal under file-scoped
   pragmas (`MA0042`, `RCS1261`, `IDISPxxx`, `VSTHRD103`, `S6966`,
   `S125`) because they specifically exercise the synchronous
   timeout branch.
 - **Primary constructors (`IDE0290`, 11 findings).** `ExecutionWorkerHostedService`,
   `ExecutionWorkerPoolHostedService`, `ExecutionWorker.ExecutionWorkItem<T>`
   nested class, `ExecutionWorkerRegistration`, `ExecutionWorkerSnapshot`,
   `WorkerFaultedEventArgs`, plus test sessions (`IntegrationSession`,
   `DiTestSession`, `DiSecondTestSession`, `HostedTestSession`,
   `MeterSnapshot.RecordedMeasurement`) all moved to primary
   constructors with field-style initializers that preserve every
   `ArgumentNullException` guard.
 - **Auto-properties (`RCS1085`, 3 findings).** `ExecutionDiagnostics`
   converted `_activitySource`, `_operationsCounter`, and
   `_sessionRecyclesCounter` private fields into auto-implemented
   properties.
 - **FluentAssertions style (`FAA0001`, 9 findings).** Replaced
   `result.Should().BeGreaterThan(0)` with `result.Should().BePositive()`
   and replaced `observedSessionIds[i].Should().Be(x)` with
   `observedSessionIds.Should().HaveElementAt(i, x)`.
 - **Collection initializers (`IDE0028` / `IDE0300` / `IDE0301`,
   8 findings).** `Array.Empty<T>()` and explicit `new[] { ... }`
   occurrences in scheduler tests, `ExecutionWorkerPoolSnapshot`'s
   `EmptyWorkers` field, and `MeterSnapshot._measurements` switched
   to the `[]` collection-expression syntax.
 - **`ValueTask` discards (`CA2012`, 5 findings).** Pragma-suppressed
   around the explicit-throw assertions in
   `ExecuteValueAsync_ShouldThrowWhenActionDelegateIsNull`,
   `ExecuteValueAsync_ShouldThrowObjectDisposedExceptionAfterDispose`,
   and the foreign-scheduler / null-scheduler pool tests, where
   the test contract is that the call throws synchronously and
   no usable `ValueTask` is ever produced.
 - **Named arguments (`MA0003`, 3 findings).** `throw new ArgumentException(...)`
   in `ExecutionWorker.Process`, `new ExecutionWorkerSnapshot(...)`
   in scheduler test stubs, and `new ManualResetEventSlim(false)`
   in dispose-timeout tests now spell parameters by name.
 - **Polyfill in canonical namespace (`IDE0130`, `MA0182`, `RCS1251`).**
   `Polyfills/IsExternalInit.cs` MUST live in
   `System.Runtime.CompilerServices` for the C# compiler to
   recognize it as the init-only marker on legacy `net46x` / `net47x` /
   `net48x` TFMs; the three rules are pragma-suppressed at file
   scope with an inline justification comment.
 - **Documentation langword (`MA0154`, 2 findings).** `ExecutionWorkerOptions`
   and `ExecutionWorkerPoolOptions` use `<see langword="new"/>`
   instead of `<c>new</c>` in their summaries.
 - **Misc fixes:** unused `workerIndex` parameter in
   `ExecutionWorkerServiceCollectionExtensions` lambda renamed to
   `_` (`RCS1163`); cancellation overload added to
   `TaskCompletionSource.TrySetCanceled` in `ExecutionHelpers`
   polyfill (`MA0040`); culture-invariant `int.ToString` in
   interpolated assertion message (`MA0076`); `if/else if` ladder
   in `Initialize_ShouldDisposeAlreadyStartedWorkers...` test
   rewritten as a `switch` (`CC0019`); single-statement lambdas in
   multi-threaded / value-task tests collapsed to expression-bodied
   form (`RCS1021`); `IDE0042` deconstructed `requiredTags` foreach
   in `MeterSnapshot.MatchesTags`; `RCS1118` upgraded
   `expectedSessionCount` to a `const` in
   `SessionRecyclingExecutionWorkerTest`; `RCS1205` reordered
   named arguments in `MultiThreadedExecutionWorkerTest`; `CA1806`
   discarded `new ExecutionDiagnostics(...)` in
   `ScopedDiagnosticsTest` lambdas (with `IDISP004` co-suppressed
   because the constructor throws before producing a disposable).
- **Quality Gate: OK.** All 6 conditions passed; 100 % new-code
 coverage; A ratings on Reliability, Maintainability, and Security.

### Tests

- **Coverage raised further from ~92% to ~96.5% overall** by adding a second
 round of 17 tests targeting the remaining defensive branches:
 - `ExecutionWorker.InitializeAsync` pre-cancelled token fast-path
   (`Task.FromCanceled`).
 - `ExecutionWorker.SetFatalFailure` double-fire idempotency β€” the
   `WorkerFaulted` event raises exactly once thanks to the CAS in
   `RaiseFaultedOnce`.
 - `ExecutionWorker.ProcessWorkItem` success + failure paths under a
   registered `ActivityListener` (both `Activity.Status = Ok` and
   `Activity.Status = Error` branches of the `SetStatus` calls).
 - Synchronous `ExecutionWorker.Dispose` + `ExecutionWorkerPool.Dispose`
   timeout branches: a blocking `IExecutionSessionFactory` wedges the
   worker thread; `Dispose` abandons the wait after
   `DisposeTimeout` without throwing.
 - `ExecutionWorkerPool.IsAnyFaulted` returns `false` for a healthy pool
   (post-loop `return false` branch).
 - `ExecutionWorkerPool.SetWorkerThreadForTesting` argument-null guard.
 - Pool `ExecuteAsync` and `ExecuteValueAsync` both throw
   `InvalidOperationException("The worker scheduler returned null.")`
   when a custom scheduler violates the contract.
 - Direct unit tests on internal pooled work items covering:
   explicit non-generic `IValueTaskSource` interface on
   `PooledValueExecutionWorkItem<,>` (`GetStatus` / `OnCompleted` /
   `GetResult`), `TrySetCanceled` β†’ cancelled `ValueTask(<T>)`, the
   stale-token guard in `GetResult` (`token != Version` mismatch), the
   defensive `_action is null` branch (rented with a null delegate),
   and the `Options` + `CancellationToken` property accessors.
- **Coverage raised from ~86% to ~92% overall** by adding ~45 focused tests:
 - `TrivialCoverageTest` and `DiagnosticsAndHelpersTest` cover all the
   small defensive branches in `ExecutionWorkerOptions` /
   `ExecutionWorkerPoolOptions` (negative `DisposeTimeout`, undefined
   `SchedulingStrategy`, fresh-instance `Default`),
   `WorkerFaultedEventArgs` (null-exception guard), `ExecutionHelpers`
   (null-action guard), `ExecutionWorkerRegistration` (null-accessor
   guard), `ExecutionDiagnostics` (null/whitespace source name,
   idempotent Dispose, no-op Dispose on `Shared`, null-registration
   guards), `ExecutionWorkerPoolSnapshot` (null-workers fallback,
   aggregation), and both `RoundRobinWorkerScheduler` /
   `LeastQueuedWorkerScheduler` (null / empty / single-worker /
   all-faulted / healthy-lowest-depth paths).
 - New `ExecutionWorkerHostingExtensionsTest` covers the two
   `services is null` guards.
 - New null / pre-cancelled / post-dispose / channel-closed tests for
   `ExecutionWorker.ExecuteValueAsync` (void + `TResult`).
 - Pool-level tests now exercise the shared-factory constructor
   (with and without an explicit scheduler), `ExecuteValueAsync` void
   and `TResult` routing through `SelectConcreteWorker`, and the
   foreign-worker `InvalidOperationException` branch via a custom
   `IExecutionWorker<T>` stub that is deliberately not an
   `ExecutionWorker<T>`.
 - DI-level tests now cover the `IOptionsFactory<T>` fallback branch
   (by removing the open-generic `IOptionsMonitor<>` descriptor
   before building the provider), the unnamed-`IOptions<T>` fallback
   when no configure delegate is supplied, and the
   `ExecutionWorkerOptions.Default` fallback when nothing at all is
   registered. Same coverage is added for the pool pipeline.

### Removed

#### `AdaskoTheBeAsT.Interop.Execution` (core)

- **`ExecutionWorkerValueTaskExtensions` public static class** β€” deleted.
 Its only job was to dispatch `.ExecuteValueAsync(...)` calls on
 `IExecutionWorker<T>` / `IExecutionWorkerPool<T>` to the concrete type's
 instance method when on `net8.0+`, and on older TFMs to wrap
 `ExecuteAsync(...)`'s `Task` in a `ValueTask` as a best-effort ergonomic
 fallback (not zero-alloc). Now that the instance `ExecuteValueAsync`
 overloads are unconditionally available on every TFM, the extension
 added nothing but a second public-API surface and a `Task β†’ ValueTask`
 wrapper on the slow path for third-party `IExecutionWorker<T>`
 implementations β€” callers can write `new ValueTask(worker.ExecuteAsync(...))`
 themselves with identical semantics.

### Fixed

#### `AdaskoTheBeAsT.Interop.Execution` (core)

- **`WorkerFaulted` / `IsFaulted` publication ordering.** `ExecutionWorker<TSession>.SetFatalFailure`
 now dispatches the `WorkerFaulted` event *before* writing the fault through
 the volatile `_fatalFailure` field. This establishes a happens-before
 chain so any observer that sees `IsFaulted == true` (or
 `IsAnyFaulted == true` on a pool) has also seen every `WorkerFaulted`
 subscriber complete. The previous ordering made it possible for a caller
 polling `IsFaulted` to read `true` during the narrow window between the
 volatile write and the event dispatch, and assert on subscriber state
 that had not yet been produced. `WorkerFaultedEventArgs` still carries
 the exception, so event handlers do not rely on `IsFaulted` / `Fault`
 inside their own scope.
- **`ExecutionWorkerPool<TSession>.Dispose` / `DisposeAsync` now share a
 single in-flight drain `Task`.** Previously a caller whose synchronous
 `Dispose()` timed out via `DisposeTimeout` would have the `_asyncDisposed`
 interlocked flag flipped to 1, so any subsequent `await pool.DisposeAsync()`
 became an instant no-op even though the background drain was still
 running. The pool now caches the dispose `Task` on first invocation
 behind `_disposeLock`, and both the sync and async paths await the same
 instance β€” callers who await `DisposeAsync()` after a timed-out
 `Dispose()` correctly observe drain completion (or the fault) instead of
 a silent early return.
- **`MeterSnapshot.Last()` determinism.** The test helper used a
 `ConcurrentBag<RecordedMeasurement>` whose enumeration order is
 thread-local-stack-based, not insertion-ordered. `Last()` therefore
 returned whichever thread's local slot happened to be iterated last β€”
 non-deterministic, and the root cause of intermittent flakes on
 telemetry assertions under parallel test-host load. Replaced with a
 coarse-locked `List<T>` whose reverse-iteration returns the strictly
 last-inserted matching measurement.
- **`ExecutionWorkerOptions.Default` is no longer a mutable shared
 singleton.** Changed from `static ExecutionWorkerOptions Default { get; } = new();`
 to `static ExecutionWorkerOptions Default => new();` so every fallback
 site (null-coalescing path in `ExecutionWorker` ctor; DI extension
 fallback) receives a fresh instance. Prevents accidental global-state
 poisoning via e.g. `ExecutionWorkerOptions.Default.DisposeTimeout = TimeSpan.Zero`.
- **NuGet package metadata mismatch.** All three packages'
 `<Description>` strings, the README TFM matrix, and the DI-test
 conditional `ItemGroup` still mentioned `netstandard2.0`, which was
 dropped from `TargetFrameworks` earlier. Descriptions and metadata are
 now aligned with the actual shipping 9-TFM matrix (`net10.0`,
 `net9.0`, `net8.0`, `net481`, `net48`, `net472`, `net471`, `net47`,
 `net462`).
- **DI: `AddExecutionWorker<TSession>` / `AddExecutionWorkerPool<TSession>`
 no longer leak `configure` delegates across different `TSession` types.**
 Previously the extension called `services.Configure(configure)` which
 pushed the delegate into the single global
 `IOptions<ExecutionWorkerOptions>` (or `...PoolOptions`) pipeline, so
 multiple `AddExecutionWorker<T>(...)` calls for different `TSession`
 types stacked their delegates and produced a single merged options
 instance shared by every worker (last-wins on `Name` + union of the
 other properties). The extension now registers a *named* options entry
 keyed by `typeof(TSession).FullName`, and the factory resolves via
 `IOptionsMonitor<T>.Get(name)` so each `TSession` binding receives its
 own isolated options. Consumers who only call `AddExecutionWorker<T>`
 once (the common case) observe no behavioural change; consumers who
 pre-bind configuration to the unnamed `IOptions<T>` pipeline (i.e. do
 not pass a `configure` delegate) continue to get the existing shared
 behaviour. Two new regression tests
 (`AddExecutionWorker_ShouldIsolateOptionsAcrossDifferentSessionTypesAsync`,
 `AddExecutionWorkerPool_ShouldIsolateOptionsAcrossDifferentSessionTypesAsync`)
 pin the isolation contract.