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
<PackageReference Include="KeelMatrix.ShutdownSpec" Version="0.1.0" />
<PackageVersion Include="KeelMatrix.ShutdownSpec" Version="0.1.0" />
<PackageReference Include="KeelMatrix.ShutdownSpec" />
paket add KeelMatrix.ShutdownSpec --version 0.1.0
#r "nuget: KeelMatrix.ShutdownSpec, 0.1.0"
#:package KeelMatrix.ShutdownSpec@0.1.0
#addin nuget:?package=KeelMatrix.ShutdownSpec&version=0.1.0
#tool nuget:?package=KeelMatrix.ShutdownSpec&version=0.1.0
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
StartupNoncompletionmeansStartAsyncor the readiness probe exceeded the startup deadline. Check the readiness probe and startup path before increasing the deadline.FactoryNoncompletionmeans 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.ServiceNoncompletionmeans stop or an observable execution task did not finish before the applicable shutdown/outer deadline.StopCompletedcan be true when a customStopAsyncreturns early; inspect the execution state and exact deadline provenance.StartupFailuremeans startup threw. InspectExceptionTypeNameandExceptionMessage, then fix the service or host setup before changing deadlines.ServiceNoncompletionafter ignored cancellation means the execution task or stop path did not finish. Honor the stopping token, register aWithStoppingProbe, and inspectExecutionStateand the deadline flags.HarnessDeadlinemeans the outer safety bound expired; distinguish it fromShutdownDeadlineFiredand only increaseWithHarnessDeadlineafter removing the blocked work.CleanupFailuremeans bounded cleanup threw after the primary outcome.CleanupNoncompletionmeans bounded best-effort cleanupStopAsync, 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 | 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 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. |
-
.NETStandard 2.0
- Microsoft.Extensions.Hosting (>= 10.0.12)
-
net8.0
- Microsoft.Extensions.Hosting (>= 10.0.12)
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.