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 runtime contract
ExecutionContext flow and continuation-context selection are different mechanisms. ConfigureAwait false does not erase AsyncLocal data, while ExecutionContext.SuppressFlow changes capture for work scheduled while suppression is active. The portable companion demonstrates only the first distinction using a completed task and ambient root value. The desktop project performs real Task.Run scheduling under a short suppression scope, restores that scope before awaiting, and inspects the worker's ambient value. This narrower split avoids claiming browser OS-thread scheduling evidence and prevents the thread-affine restoration token from being carried across an arbitrary asynchronous continuation.
Trace the portable program
In the portable sequence, awaiting an already completed task with ConfigureAwait false leaves the ambient root value present and does not enable suppression. Output is ambient:root and suppressed:False. The external work is scheduled while flow is suppressed, so its AsyncLocal value is absent and the chosen fallback is none. After suppression has been restored and the task awaited, the parent still reads root and suppression is false. Its output is worker:none, parent:root, and suppressed:False. A common wrong answer expects ConfigureAwait false to behave like the suppression scope or awaits inside that scope without respecting restoration ownership.
Execute the actual desktop lab
The desktop suppression scope surrounds only Task.Run scheduling. Its restoration token is disposed before any await, on the creating thread, and the queued work receives no captured ambient root value. After the task completes, the parent still has its own value. This is real execution-context capture behavior. ConfigureAwait false addresses whether an await should use an available continuation context or scheduler; it does not by itself suppress AsyncLocal propagation and does not force a new physical thread. In particular, an already completed await can continue synchronously, as the portable companion deliberately demonstrates without making a thread-switch claim.
Pitfalls and boundary limits
Do not carry AsyncFlowControl across an arbitrary await and assume restoration can safely occur on whichever thread resumes. Do not interpret suppression as clearing the calling thread's current ambient values either; it changes flow capture for subsequently queued work. UI affinity and ambient correlation are separate requirements. A library might avoid continuation-context capture while still correctly carrying tracing data, and a deliberately detached background task may need a different propagation policy. The sample chooses one bounded scheduling scope and owns its task. It does not recommend discarding every request's context or launching unobserved background operations as a universal optimization.
Practice one: isolate behavior
First read the parent's AsyncLocal value inside the suppression scope before scheduling work and predict that root is still present there. Hint: suppressed capture is not immediate erasure of the current slot. Then schedule a second task after restoration and compare its inherited value with the suppressed task's fallback. Retain and await both owned tasks rather than letting their output ordering become a scheduling guess. Keep the parent observations separate from each worker's result so the experiment cannot confuse one flow's local value with another's capture policy.
Practice two: design and verify
For another exercise, define a correlation policy for a deliberately detached background operation and decide which data should be copied explicitly, flowed automatically, or omitted. Hint: suppressing all context is broader than removing one application field. Keep the suppression scope synchronous and own every scheduled task's completion and failures. In a real UI or custom-scheduler environment, test continuation affinity independently from ambient-value propagation instead of treating ConfigureAwait false as either a guaranteed thread-pool hop or an instruction to remove all context from the program.
Review the evidence
Review context-related code by separating ambient state, scheduling-time capture, await continuation policy, and physical restoration ownership. The portable two-line output establishes that ConfigureAwait false does not erase root or enable suppression. The external three-line output establishes one actually suppressed queued operation with a restored parent. These are different mechanisms with different lifetimes. Choose them according to a concrete propagation contract, and avoid replacing that contract with slogans such as no context or always another thread. Correctness depends on what is captured, where it is restored, and who awaits the work. An already completed await is especially poor evidence of any particular rescheduling behavior.
Suppressed flow differs from ConfigureAwait — desktop runtime lab
Set up the SDK project
- Save both files at their exact paths in a fresh owned directory.
- Use a .NET 10 SDK, then run dotnet run --project RuntimeLab.csproj. This project intentionally uses C# 13 syntax.
- Workers and waits are bounded; a timeout or a violated invariant fails the process. No benchmark or NativeAOT publication is implied by an ordinary run.
Download complete SDK project ↗
Project files
Program.cs
Download file ↗using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Reflection;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
var local = new AsyncLocal<string?>();
local.Value = "root";
Task<string> work;
using (ExecutionContext.SuppressFlow()) { work = Task.Run(() => local.Value ?? "none"); }
Console.WriteLine($"worker:{await work}");
await Task.CompletedTask.ConfigureAwait(false);
Console.WriteLine($"parent:{local.Value}");
Console.WriteLine($"suppressed:{ExecutionContext.IsFlowSuppressed()}");
RuntimeLab.csproj
Download file ↗<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>13.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>false</PublishAot>
</PropertyGroup>
</Project>
Expected result
Exact behavioral stdout on the stated desktop lab: worker:none parent:root suppressed:False Variable diagnostics, where emitted, go to stderr and must not be treated as portable timing or allocation guarantees.
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.Collections.Concurrent;
using System.Reflection;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
var local = new AsyncLocal<string?>();
local.Value = "root";
await Task.CompletedTask.ConfigureAwait(false);
Console.WriteLine($"ambient:{local.Value}");
Console.WriteLine($"suppressed:{ExecutionContext.IsFlowSuppressed()}");
C# practice output
ambient:root suppressed:False
ConfigureAwait(false) is not ExecutionContext suppression. The desktop project suppresses flow only while scheduling Task.Run and restores it before awaiting.
Further reading
Interview questions for this lesson
- Explain: Why does ConfigureAwait(false) not have the same effect as ExecutionContext.SuppressFlow?
- Trace: Trace the completed-await companion and identify which suppression operation it does not perform.
- Design: How would you schedule deliberately detached work with a precise context and ownership policy?