Lab — Identify Middleware Ordering Problems
Learning objective: Inspect a deliberately misordered ASP.NET Core middleware pipeline, predict its request/response behavior, identify the architectural boundary that is broken, repair the order, validate the changed behavior, and explain why the correction works.
Difficulty: Beginner
Pattern classification: Canonical pattern
Prerequisites: Complete Middleware Ordering Changes Behavior. Run the Middleware Ordering Changes Behavior sample before starting the repair exercise.
Middleware order is executable architecture.
The same components can produce different behavior when their registration order changes because earlier middleware can wrap later behavior, short-circuit requests, establish request state, or modify responses on the way back out.
The diagnostic flow for this lab is:
Incorrect Order
↓
Predict Behavior
↓
Observe Request / Response
↓
Identify Broken Boundary
↓
Encode the Intended Behavior in a Test
↓
Reorder Middleware
↓
Validate
↓
Explain Why Behavior Changed
The goal is not to memorize a universal middleware list.
The goal is to learn how to prove what a particular position means.
Part 1 — Establish the Baseline
From the repository root, run the focused sample tests:
dotnet test samples/middleware-ordering-changes-behavior/MiddlewareOrderingChangesBehavior.Tests/MiddlewareOrderingChangesBehavior.Tests.csproj
The existing tests establish three observable facts:
- Request traversal and response unwinding occur in opposite directions.
- A correctly placed exception boundary can normalize a downstream demonstration failure.
- A fault that occurs before the exception boundary is entered remains outside that custom boundary.
Now inspect these files:
samples/middleware-ordering-changes-behavior/
├── MiddlewareOrderingChangesBehavior/
│ ├── MiddlewareOrderDemo.cs
│ └── Program.cs
└── MiddlewareOrderingChangesBehavior.Tests/
└── MiddlewareOrderTests.cs
Focus first on the deliberately incorrect branch in MiddlewareOrderDemo.Configure:
UseFaultProbe(app, observe);
UseExceptionBoundary(app, observe);
UseTrace(app, "outer", observe);
Do not repair it yet.
Part 2 — Predict Before You Run
For the incorrect pipeline, predict what will happen for each request before executing it.
Fill in a table like this in your notes:
| Request | Expected status/result | First observable event | Will the custom exception boundary handle it? | Will the endpoint run? |
|---|---|---|---|---|
/ |
? | ? | ? | ? |
/fault |
? | ? | ? | ? |
Use the two-direction model:
Request
↓ registration order
Middleware
↓
Endpoint
↑
Middleware
↑ reverse unwind
Response
Remember that middleware which throws or short-circuits before calling next prevents later middleware from being entered.
Before running anything, answer:
- Which middleware is outermost in the incorrect pipeline?
- Which middleware is never entered when
/faultthrows immediately? - Can a downstream exception handler catch an exception thrown before that handler is entered?
- For
/, why can the pipeline still appear healthy even though the ordering defect exists?
Part 3 — Observe the Incorrect Behavior
Run the deliberately incorrect pipeline:
dotnet run --project samples/middleware-ordering-changes-behavior/MiddlewareOrderingChangesBehavior/MiddlewareOrderingChangesBehavior.csproj -- --PipelineMode=incorrect --urls http://127.0.0.1:5080
In another terminal, make a normal request:
curl -i http://127.0.0.1:5080/
Compare the console trace with your prediction.
The normal request can still reach the endpoint because the fault probe calls the next middleware when the path is not /fault.
Now trigger the demonstration failure:
curl -i http://127.0.0.1:5080/fault
The hosting server may ultimately produce a 500 response.
That alone does not prove the sample's custom exception boundary handled the failure.
Use the event trace to answer the more important question:
Did
exception-boundary:handledoccur?
For the deliberately incorrect order, the important causal sequence is:
/fault request
↓
Fault probe entered first
↓
Fault probe throws before calling next
↓
Exception boundary is never entered
↓
Custom handler cannot normalize that failure
The architectural defect is therefore not "the application returned 500."
The defect is:
The failure-producing middleware sits outside the custom exception boundary that is expected to normalize it.
Part 4 — Make a Disposable Repair Copy
Do not change the canonical teaching sample in your working tree for the exercise.
Copy the sample to a disposable directory beside the repository.
PowerShell
Copy-Item -Recurse `
samples/middleware-ordering-changes-behavior `
../MiddlewareOrderingLab
Bash
cp -R samples/middleware-ordering-changes-behavior ../MiddlewareOrderingLab
The copied test project keeps its relative project reference, so the pair remains runnable together.
Run the copied tests before changing anything:
dotnet test ../MiddlewareOrderingLab/MiddlewareOrderingChangesBehavior.Tests/MiddlewareOrderingChangesBehavior.Tests.csproj
They should pass in the copied baseline.
Part 5 — Encode the Target Behavior Before Repairing the Order
In the copied MiddlewareOrderTests.cs, replace the test named:
IncorrectOrder_LeavesEarlierFaultOutsideExceptionBoundary
with this repair-target test:
[Fact]
public async Task RepairedOrder_CatchesFaultInsideExceptionBoundary()
{
List<string> events = [];
RequestDelegate pipeline =
MiddlewareOrderDemo.Build(
correctOrder: false,
events.Add);
DefaultHttpContext context =
CreateContext("/fault");
await pipeline(context);
Assert.Equal(
StatusCodes.Status500InternalServerError,
context.Response.StatusCode);
Assert.Contains(
"exception-boundary:handled",
events);
}
Run the copied tests again:
dotnet test ../MiddlewareOrderingLab/MiddlewareOrderingChangesBehavior.Tests/MiddlewareOrderingChangesBehavior.Tests.csproj
The new repair-target test should fail because correctOrder: false still builds the defective sequence.
That failure is useful.
You have changed the test from:
Prove the defect exists
to:
Prove the repaired boundary works
Now the code must earn the new expectation.
Part 6 — Repair the Middleware Boundary
Open the copied MiddlewareOrderDemo.cs.
Change only the deliberately incorrect branch.
Your constraint is:
The exception boundary must be entered
before the fault-producing middleware can throw.
Do not copy the corrected branch mechanically.
Reason from the dependency:
Exception boundary
↓ must wrap
Fault-producing middleware
Reorder the calls until the custom exception boundary encloses the fault probe.
Leave the trace middleware in a position you can explain.
Run the copied tests:
dotnet test ../MiddlewareOrderingLab/MiddlewareOrderingChangesBehavior.Tests/MiddlewareOrderingChangesBehavior.Tests.csproj
The repair-target test should now pass.
If it does not, inspect the event list rather than moving middleware at random.
A successful repair should make this sequence reachable:
exception-boundary:request
↓
fault-probe:throw
↓
exception-boundary:handled
↓
exception-boundary:response
The endpoint should still not execute for /fault.
The difference is that the failure now occurs inside the boundary that owns its normalization behavior.
Part 7 — Explain Why the Behavior Changed
Write a short explanation using these terms:
- registration order;
- request traversal;
next;- wrapping;
- short-circuit or early failure;
- response unwinding;
- exception boundary.
A strong explanation should be able to complete this statement:
Moving the exception handler earlier changed behavior because ...
Avoid explanations such as:
"ASP.NET Core wants the exception handler first."
That describes a remembered rule, not the architecture.
The useful explanation is causal:
Earlier registration
↓
Boundary enters request first
↓
Boundary awaits downstream next
↓
Downstream fault occurs inside try/catch
↓
Boundary can normalize the failure
Part 8 — Diagnose Other Ordering Boundaries
Exception handling is only one form of order-dependent behavior.
For each scenario below, identify the broken dependency or coverage requirement before proposing a reorder.
Scenario A — Authentication and Authorization
app.UseAuthorization();
app.UseAuthentication();
Question:
If authorization expects an authenticated
ClaimsPrincipal, which middleware produces that state and which middleware consumes it?
The dependency is:
Authentication
↓ produces principal
Authorization
↓ consumes principal
If authorization is endpoint-specific, routing metadata is another dependency that must already be available.
Scenario B — Request Logging and a Short-Circuiting Limiter
Rate limiter
↓
Request logging
↓
Endpoint
Suppose the operational requirement is:
Every rejected request must appear in the same request log.
A request rejected before logging is reached cannot satisfy that requirement.
Moving request logging earlier increases coverage:
Request logging
↓
Rate limiter
↓
Endpoint
That does not make earlier logging universally correct.
If the requirement intentionally excludes noisy rejected traffic from that log, later placement may be reasonable.
Scenario C — Security Headers and an Earlier Response Producer
Static files
↓
Security headers
↓
Application endpoints
Suppose the requirement is:
Static-file responses must receive the same selected security headers.
A static-file response can short-circuit before the security-header middleware is reached.
The coverage dependency then points toward:
Security headers
↓
Static files
↓
Application endpoints
Again, define the response-coverage requirement first rather than memorizing the position.
Scenario D — Endpoint-Specific Rate Limiting
An endpoint-specific limiter needs routing metadata.
That gives the dependency:
Routing
↓
Endpoint metadata available
↓
Endpoint-specific rate limiting
A purely global limiter may have no such dependency and can legitimately run earlier.
This is why "Where does rate limiting go?" does not have one universal answer.
Part 9 — Build a Dependency Table
Choose a real or sample ASP.NET Core pipeline and classify at least five middleware concerns using this shape:
| Concern | Produces | Consumes | Must wrap | Coverage requirement |
|---|---|---|---|---|
| Example: Authentication | ClaimsPrincipal |
Credentials/services | — | Requests requiring authenticated identity |
| ? | ? | ? | ? | ? |
| ? | ? | ? | ? | ? |
| ? | ? | ? | ? | ? |
| ? | ? | ? | ? | ? |
Good candidates include:
- forwarded headers;
- request logging;
- exception handling;
- security headers;
- routing;
- CORS;
- rate limiting;
- authentication;
- authorization;
- endpoint execution.
For each row, answer:
What observable behavior would change if this component moved across the thing it depends on or is expected to wrap?
That question is more durable than memorizing a framework-specific list.
Part 10 — Compare with the Working Reference
After completing the repair, inspect the fuller NetCoreApplicationTemplate pipeline:
Do not treat the working template's sequence as a universal answer key.
Instead, choose three neighboring components and explain the dependency or coverage goal that justifies their relative order.
If you cannot explain why two components have to be ordered relative to each other, determine whether that relationship is actually required or merely conventional.
Completion Criteria
You have completed the lab when you can demonstrate all of the following:
- You predicted the incorrect pipeline's behavior before running it.
- You distinguished a host-generated
500from the sample's custom exception boundary handling the failure. - You identified why the fault probe sits outside the intended exception boundary.
- You changed a test so the desired repaired behavior failed before the code was changed.
- You reordered the copied pipeline so the custom boundary handles the downstream failure.
- The copied focused tests pass after the repair.
- You can explain request traversal and response unwinding without relying on a memorized middleware list.
- You can identify producer/consumer dependencies such as authentication before authorization.
- You can explain coverage tradeoffs for request logging and security headers.
- You can explain why endpoint-specific rate limiting may require a different position from global rate limiting.
- You can justify a middleware reorder in terms of observable behavior, dependency, wrapping, or coverage.
The architectural invariant should now be visible:
Middleware position
↓
What state is available?
What behavior is wrapped?
What can short-circuit first?
Which responses unwind through this component?
↓
Observable application behavior
Optional Extension — Add a Short-Circuit Probe
In the disposable copy, add middleware that returns 418 I'm a teapot without calling next for /short-circuit.
Then write a test that proves:
Short-circuit middleware reached
↓
Endpoint event absent
↓
Only middleware that already entered the request can participate in response unwinding
Move the short-circuit middleware one position at a time and record which events disappear.
This makes pipeline reachability observable without introducing authentication, routing, or external infrastructure.
Related Content
- Middleware Ordering Changes Behavior — architecture explanation and ordering dependency model.
- Middleware Ordering Changes Behavior sample — runnable corrected and deliberately incorrect pipelines.
- ASP.NET Core learning area — broader application-architecture learning path.
- NetCoreApplicationTemplate — fuller working ASP.NET Core reference specimen.
Read it. Run it. Question it. Improve it.