KeelMatrix.ShutdownSpec 0.1.0

Prefix Reserved
dotnet add package KeelMatrix.ShutdownSpec --version 0.1.0
                    
NuGet\Install-Package KeelMatrix.ShutdownSpec -Version 0.1.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="KeelMatrix.ShutdownSpec" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="KeelMatrix.ShutdownSpec" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="KeelMatrix.ShutdownSpec" />
                    
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 KeelMatrix.ShutdownSpec --version 0.1.0
                    
#r "nuget: KeelMatrix.ShutdownSpec, 0.1.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 KeelMatrix.ShutdownSpec@0.1.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=KeelMatrix.ShutdownSpec&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=KeelMatrix.ShutdownSpec&version=0.1.0
                    
Install as a Cake Tool

KeelMatrix.ShutdownSpec

KeelMatrix.ShutdownSpec is a test-framework-neutral NuGet library for checking that an IHostedService or BackgroundService starts, observes application-owned lifecycle checkpoints, and completes graceful shutdown within a declared test contract.

Install

dotnet add package KeelMatrix.ShutdownSpec

Quick Start

using KeelMatrix.ShutdownSpec;
using Microsoft.Extensions.Hosting;

var ready = new ShutdownProbe("ready");
var executionEntered = new ShutdownProbe("execution-entered");
var result = await ShutdownHarness
    .For(() => new Worker(ready, executionEntered))
    .WithReadinessProbe(ready)
    .WithExecutionProbe(executionEntered)
    .RunAsync();

result.ShouldCompleteWithoutFault();

sealed class Worker : BackgroundService
{
    private readonly ShutdownProbe _ready;
    private readonly ShutdownProbe _executionEntered;

    public Worker(ShutdownProbe ready, ShutdownProbe executionEntered)
    {
        _ready = ready;
        _executionEntered = executionEntered;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _ready.MarkObserved();
        _executionEntered.MarkObserved();
        try { await Task.Delay(Timeout.InfiniteTimeSpan, stoppingToken); }
        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { }
    }
}

The returned ShutdownResult records lifecycle facts and a stable ShutdownOutcome. Readiness is reported separately from the independently observed execution-entry checkpoint; a readiness probe never proves that the execution body entered. Assertion methods throw ShutdownAssertionException, so the same API works with xUnit, NUnit, MSTest, or plain code.

Direct IHostedService

For a service with no BackgroundService loop, pass the service directly through the same framework-neutral API:

var result = await ShutdownHarness
    .For(() => new DirectService())
    .RunAsync();

result.ShouldCompleteWithoutFault();

sealed class DirectService : IHostedService
{
    public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

Use WithExecutionProbe for an application-owned checkpoint marked from the execution body. When testing expected cancellation, use WithStoppingProbe with a ShutdownProbe registered through ObserveCancellation; cancellation without those independent observations is reported conservatively.

After StopAsync returns, an observable BackgroundService.ExecuteTask is still awaited within the remaining configured bounds. A task that remains running is reported as ServiceNoncompletion; completion, fault, and cancellation are classified from the final observed task state.

Representative failure output for a worker that ignores cancellation:

KMSHUT101: ServiceNoncompletion.
Phase: Stopping
Startup completed: True; stop initiated: True; stop completed: True
Readiness observed: False; execution entry observed: False
Execution: Running; completed: False; canceled: False
Harness deadline: False; startup deadline: False; shutdown deadline: True
Cleanup completed: True; cleanup timed out: False

Probes and deadlines

Create a ShutdownProbe in the test and mark it from application-owned code. Register it with WithProbe and assert it with ShouldObserve. Use WithReadinessProbe when shutdown must not begin until a readiness checkpoint has been observed. Use a separate WithExecutionProbe checkpoint from the execution body; readiness never proves execution entry.

WithStartupDeadline, WithShutdownDeadline, and WithHarnessDeadline are separate. The startup token passed to StartAsync, the host shutdown token passed to StopAsync, the BackgroundService stopping token, the caller cancellation token, and the harness's outer safety deadline are recorded separately. A test deadline does not set or predict a production host timeout.

The default test bounds are 5 seconds for startup, 5 seconds for shutdown, 15 seconds for the complete harness, and 1 second for cleanup. Use independently marked WithExecutionProbe and WithStoppingProbe checkpoints when a cancellation result must be proven; without execution-entry evidence, cancellation is classified conservatively and is never a successful expected-cancellation result.

The package targets net8.0 and netstandard2.0 and is validated against Microsoft.Extensions.Hosting 10.0.12. The repository's public CI matrix validates it on Windows, Linux, and macOS. See the canonical supported-platform statement for the full compatibility boundary. The package makes no network requests.

Limitations

ShutdownSpec does not infer queue or database drain state, make network requests, collect telemetry, control processes, or replace the host. It can bound asynchronous operations, but it cannot safely regain control from an arbitrary synchronous infinite loop on the calling process's thread; the truthful result is returned while the blocked thread may remain.

See the canonical lifecycle contract for outcome codes and failure diagnostics.

Troubleshooting

  • StartupNoncompletion means StartAsync or the readiness probe exceeded the startup deadline. Check the readiness probe and startup path before increasing the deadline.
  • FactoryNoncompletion means the synchronous service factory did not return before the outer deadline. The harness bounds its wait, but it cannot interrupt a synchronous block in the same process.
  • ServiceNoncompletion means stop or an observable execution task did not finish before the applicable shutdown/outer deadline. StopCompleted can be true when a custom StopAsync returns early; inspect the execution state and exact deadline provenance.
  • StartupFailure means startup threw. Inspect ExceptionTypeName and ExceptionMessage, then fix the service or host setup before changing deadlines.
  • ServiceNoncompletion after ignored cancellation means the execution task or stop path did not finish. Honor the stopping token, register a WithStoppingProbe, and inspect ExecutionState and the deadline flags.
  • HarnessDeadline means the outer safety bound expired; distinguish it from ShutdownDeadlineFired and only increase WithHarnessDeadline after removing the blocked work.
  • CleanupFailure means bounded cleanup threw after the primary outcome. CleanupNoncompletion means bounded best-effort cleanup StopAsync, pending lifecycle work, or disposal did not finish within the cleanup budget. Make cleanup cancellation-aware; use process isolation when a hard interruption boundary is required.
  • Scheduler-sensitive tests should assert probes and bounded state with practical deadline margins, not exact millisecond ordering. Repeat the scenario when investigating flakiness and keep host, service, and harness cancellation concepts separate.
Product 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 was computed.  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 was computed.  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 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.0 108 9/24/2026

Initial release of the deterministic hosted-service shutdown contract harness.