Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.
The request lifetime must reach the operation doing work
A bound CancellationToken in a Minimal API handler represents HttpContext.RequestAborted. That gives the application a path for observing a request that is aborted or whose client disconnects, but cooperative downstream work must still receive and honor the token. Cancellation is not forced termination of arbitrary code. The portable companion checks a pre-canceled token in an ordinary operation and reports cancellation before successful completion. The external project instead passes the real request token into an actual delayed operation, allowing a loopback disconnect to test the framework boundary the console cannot represent.
Separate pre-cancellation from a disconnected client
The console cancels its source before calling Complete, so ThrowIfCancellationRequested prevents the completion counter from incrementing. It prints cancelled and completed:0. On a fresh server, /probe reports zero controlled cancellations. A client begins /wait, whose handler awaits a thirty-second delay with the bound token. Closing that client's connection causes the cooperative wait to cancel, and the guarded catch increments the probe. A later probe request reports one. The test waits for that observation rather than assuming that connection closure and server cancellation are instantaneous events on the same clock tick.
A token can span several different asynchronous boundaries
A real handler may pass RequestAborted through database calls, HTTP calls, stream reads or application work. Supplying the token only to an initial operation leaves later work outside the intended cancellation path. Conversely, some operations cannot safely be abandoned after a commit boundary and need an explicitly different lifetime or reconciliation policy. This example has only a delay and a probe update, so it makes no claim about those larger transactions. Keep the request and any resources alive until awaited operations finish; do not return a response while unowned work still borrows a request-scoped dependency.
Cancellation does not establish rollback or user intent
A canceled wait does not prove that a remote service did nothing, that previously written bytes were erased or that a user deliberately pressed a Cancel button. A timeout, server policy or connection failure can also end a request. The external catch is deliberately guarded by this operation's token state and records one narrow observation. Its counter is not a transaction log. When adapting the pattern, classify cancellation alongside other failure categories and decide what the caller must do after an uncertain remote outcome. Do not automatically retry a mutating action merely because the waiting request was canceled.
Practice: test normal completion separately
Reduce the delay to a small classroom duration and let one request complete normally. Predict its successful body and the unchanged cancellation count. Start a separate request and disconnect it before the delay completes, then poll the probe for the independent canceled outcome. Use separate request instances and specify ordering with observable completion rather than arbitrary long sleeps. The hint is that a successful completion counter and a cancellation counter answer different questions. Avoid a test that merely accepts either success or cancellation, because it would not establish either scenario's intended boundary.
Practice: forward cancellation into a second stage
Add a controlled two-stage operation in the external project, with an observable signal after the first stage and another cancellable wait in the second. Disconnect only after the first signal, and verify that the second stage does not complete successfully. Pass the same request lifetime into the work that should cooperate. Then deliberately omit that forwarding in a separate project copy to expose the missing boundary. Do not introduce a public slow service or a real purchase; controlled in-process stages can establish the ordering without confusing network uncertainty with application cancellation logic.
Describe the exact evidence supplied by the test
The portable run proves the operation respects pre-cancellation before completion. The external run proves a real Kestrel request's disconnect reaches the cooperative handler wait in this project. Neither proves automatic rollback, identical internal token values or the behavior of an unrelated remote adapter. Preserve those precise claims when adding timeouts or downstream calls. Own the cancellation source at the appropriate application boundary, pass tokens deliberately and await cleanup. A small deterministic probe is useful teaching evidence, while production reporting should distinguish abandoned requests, business rejection and operational failures without treating every interruption as the same event.
- Test pre-cancellation, normal completion and a controlled later cancellation independently.
- Forward the owning token to cooperative operations without promising rollback.
RequestAborted and cooperative work — real ASP.NET Core 10 project
Set up the SDK project
- Save all files into one new empty directory, preserving their relative paths. Install the official .NET 10 SDK if it is not already available.
- From that directory run: dotnet restore WebLab.csproj --source https://api.nuget.org/v3/index.json
- Start only on loopback: ASPNETCORE_ENVIRONMENT=Development dotnet run --project WebLab.csproj --no-launch-profile --urls http://127.0.0.1:5080
- Use another terminal for the requests below, then press Ctrl+C in this project’s terminal to stop its server.
- GET /probe, then run curl --max-time 1 http://127.0.0.1:5080/wait; its timeout disconnects the request. GET /probe again.
Download complete SDK project ↗
Project files
WebLab.csproj
Download file ↗<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Program.cs
Download file ↗var builder = WebApplication.CreateBuilder(args); builder.Services.AddSingleton<CancelProbe>(); var app = builder.Build();
app.MapGet("/wait", async (CancellationToken token, CancelProbe probe) => {
try { await Task.Delay(TimeSpan.FromSeconds(30), token); return TypedResults.Ok(new { completed = true }); }
catch (OperationCanceledException) when (token.IsCancellationRequested) { probe.Record(); throw; }
});
app.MapGet("/probe", (CancelProbe probe) => new { canceled = probe.Count }); app.Run();
public sealed class CancelProbe { private int count; public int Count => Volatile.Read(ref count); public void Record() => Interlocked.Increment(ref count); }
Expected result
A fresh GET /probe reports canceled=0. Disconnecting a GET /wait client interrupts its cooperative delay; subsequent /probe reports canceled=1. No remote mutation or rollback is demonstrated.
Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.
C# companion practice
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using var lifetime = new CancellationTokenSource(); lifetime.Cancel(); int completed = 0;
try { await Complete(lifetime.Token); completed++; } catch (OperationCanceledException) { Console.WriteLine("cancelled"); }
Console.WriteLine("completed:" + completed);
static Task Complete(CancellationToken token) { token.ThrowIfCancellationRequested(); return Task.CompletedTask; }
C# practice output
cancelled completed:0
The token reaches cooperative work; a canceled wait is not a transaction rollback.