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.
Middleware is nested control flow around later work
A middleware delegate receives the current request and a delegate representing the rest of the pipeline. Work before awaiting that next delegate runs on the inbound path; work afterward runs while the nested call returns. The portable companion demonstrates that structure with ordinary asynchronous functions and a list. It prints outer-in, inner-in, work, inner-out and outer-out joined by separators. No HttpContext or web host is fabricated. The external project then records the equivalent structure through real ASP.NET middleware and an endpoint, making the framework boundary an observable second experiment rather than an implied feature of the console.
Read the completed request trace separately
The outer middleware creates a request-local list only for /order and stores it in HttpContext.Items. The inner middleware appends its entry, then the endpoint appends endpoint and returns handled. As control unwinds, the inner and outer middleware append their exit markers. Only after the outer marker is present does the project publish its completed trace to a small development observer. A subsequent /trace request returns outer-in>inner-in>endpoint>inner-out>outer-out. The observer path is excluded from recording so reading the trace does not replace it with a trace of the inspection itself.
The outbound path may follow response commitment
The fact that code runs after await next does not mean it can still change response status or headers. The endpoint may already have started writing the response. Use the appropriate response callbacks when a header must be supplied before commitment, and inspect the response-started state when designing failure handling. This lesson records its final sequence outside the response body precisely so it need not pretend the endpoint can serialize future exit markers. A real request's body and a completed diagnostic observation have different timing boundaries even when they refer to the same logical pipeline.
The observer has a deliberately narrow teaching contract
The list in HttpContext.Items belongs to one request, but the final observer value is shared application state. The sample is explicitly Development-only and expects sequential requests; it is not a reliable concurrent audit log, request history or correlation store. Two overlapping requests could replace the last-value observation in either completion order. That limitation does not change the nested call structure, but it matters when interpreting evidence. Production tracing should use appropriate scoped or correlated telemetry instead of retaining a mutable shared last-result field and assuming it identifies the request a user intended to inspect.
Practice: reverse the two registered wrappers
Swap the registration order of the two middleware delegates while also ensuring that whichever wrapper runs first creates the request-local list. Predict the new inbound and outbound sequence before running the real project. Keep the endpoint marker unchanged. A useful hint is to draw nested function calls rather than reading registration order as though every statement ran only once from top to bottom. Preserve the observer's excluded path and sequential request setup so changing registration order is the only cause of a different trace. The portable functions can independently illustrate the same nesting transformation.
Practice: introduce a deliberate short circuit
Add a controlled branch in the outer middleware that returns a fixed response without invoking next for a separate path. State which inner and endpoint markers must be absent in that case, and provide a separate completed observation for it. Then test the original /order route again. This exercise distinguishes returning from the current middleware from awaiting the next delegate and later returning. Do not combine the short circuit with an exception experiment or header mutation; each has a separate contract and can otherwise conceal why later work did not execute.
Use order to explain behavior rather than memorize a list
A pipeline review should identify which component selects endpoints, which components need that selection, where authentication and authorization run, and where a response can begin. This lesson isolates nested entry and exit so later middleware requirements can be understood causally. The complete project uses genuine ASP.NET delegates and a small observer, while the companion supplies a deterministic control-flow trace. Neither establishes a universal ordering for all middleware. Consult each component's requirements, keep short circuits explicit, and retain request-specific data in an owner that can distinguish concurrent operations instead of promoting a classroom observer into application infrastructure.
- Inbound registration order and outbound unwinding order are different observations.
- Code after next may run after response headers or body have already started.
Middleware order and unwind — 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.
- Call /order, then /trace sequentially; do not use this shared single-value observer for concurrent auditing.
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<TraceState>(); var app = builder.Build();
if (!app.Environment.IsDevelopment()) throw new InvalidOperationException("This trace observer is development-only");
app.Use(async (context, next) => {
if (context.Request.Path != "/order") { await next(context); return; }
var trace = new List<string> { "outer-in" }; context.Items["trace"] = trace;
await next(context); trace.Add("outer-out"); context.RequestServices.GetRequiredService<TraceState>().Last = string.Join(">", trace);
});
app.Use(async (context, next) => {
if (context.Request.Path != "/order") { await next(context); return; }
var trace = (List<string>)context.Items["trace"]!; trace.Add("inner-in"); await next(context); trace.Add("inner-out");
});
app.MapGet("/order", (HttpContext context) => { ((List<string>)context.Items["trace"]!).Add("endpoint"); return TypedResults.Text("handled"); });
app.MapGet("/trace", (TraceState state) => TypedResults.Text(state.Last));
app.Run();
public sealed class TraceState { public string Last { get; set; } = "none"; }
Expected result
GET /order returns handled. A subsequent GET /trace returns outer-in>inner-in>endpoint>inner-out>outer-out. Observer is development-only and intended for sequential requests.
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;
var trace = new List<string>();
Func<Task> work = () => { trace.Add("work"); return Task.CompletedTask; };
Func<Task> inner = async () => { trace.Add("inner-in"); await work(); trace.Add("inner-out"); };
trace.Add("outer-in"); await inner(); trace.Add("outer-out"); Console.WriteLine(string.Join(">", trace));
C# practice output
outer-in>inner-in>work>inner-out>outer-out
The return path unwinds in reverse order; response headers may already be committed then.