The Middleware Pipeline: Every Request's Journey – Web API | Lesson 67
Introduction
Between Kestrel receiving a request and a controller handling it, the request passes through a chain of steps. Each step can look at the request, change it, stop it, or let it continue, and then do more work on the way back.
That chain is the middleware pipeline, and you built a short one in Program.cs in Lesson 66.
This lesson explains how it works and adds a custom step to SchoolApp.Api: one that times every
request.
By the end of this lesson, you will be able to:
- Explain what middleware is and how a request moves through the pipeline.
- Use
app.Use, and explain howRunandMapdiffer from it. - Order middleware correctly, and explain why order matters.
- Short-circuit a request in middleware.
- Write a middleware class and register it with an extension method.
Why this matters
Logging, error handling, HTTPS, CORS, authentication, and rate limiting are all middleware. Modules 11 and 12 add most of them, and each must go in the right place, or it silently does nothing.
Middleware is also where cross-cutting work belongs. Timing every request in one middleware is ten lines; doing it inside every action would be hundreds of lines, and easy to forget in new ones.
The concept
Example: what middleware is
A middleware is one step in the pipeline. It receives the request's HttpContext, which holds the
request and the response, and a reference to the next step.
Think of a visitor to the school. The gate guard writes down the arrival time, reception checks the visitor's pass, and then an office handles the business. On the way out, the visitor passes reception and the guard again, and the guard notes the leaving time.
Each middleware runs its "before" code, calls the next one, waits for it to finish, then runs its
"after" code. The first middleware in Program.cs is the first to see the request and the last to see
the response.
Example: Use, Run, and Map
| Method | Does | Example |
|---|---|---|
app.Use(...) | Adds a middleware that can call the next one | Logging, timing |
app.Run(...) | Adds a final middleware that never calls a next one | A fixed reply at the end of a branch |
app.Map("/path", ...) | Starts a separate branch for paths that begin with /path | A health-check branch |
app.UseXyz() | A built-in middleware, added through an extension method | UseHttpsRedirection() |
app.Run() with no arguments, at the end of Program.cs, is different: it starts the server. The
Run(...) that takes a delegate adds a final pipeline step.
Example: order matters
Middleware runs in the order it is added. Microsoft's documentation gives the usual order:
| Order | Middleware | Why there |
|---|---|---|
| 1 | Exception handling (Lesson 74) | Must wrap everything to catch every error |
| 2 | HTTPS redirection | Redirect before doing any real work |
| 3 | Static files, if any | Serve files without the cost of the rest |
| 4 | Routing, CORS | Decide the endpoint; allow websites on other addresses to call the API |
| 5 | Authentication, then authorization | Who is calling, then are they allowed (Module 12) |
| 6 | Endpoints: MapControllers() | Finally, run the action |
Put authorization before authentication, and it checks a caller nobody has identified yet, so every protected request fails. Put exception handling last, and it never sees the exceptions thrown before it. The code compiles either way, which is why order mistakes are dangerous.
Example: where the endpoints really run
In apps built with WebApplication, as SchoolApp.Api is, MapControllers() does not put the
controllers at that line. It only registers them; ASP.NET Core runs the endpoints at the very end of
the pipeline, after every Use... call.
A quick experiment confirmed it: a middleware added after MapControllers() still printed its
"before" line ahead of the controller. So what matters is the order of the Use... calls among
themselves, not where Map... sits. Keeping MapControllers() last is still the convention, because
it makes the file read in the order things happen.
Example: middleware and filters
ASP.NET Core also has filters, which run inside the controller part of the pipeline (Module 12). A filter knows which controller and action will run; middleware only sees the HTTP request.
Use middleware for things every request needs, even requests no controller handles, such as timing or security headers. Use filters for things tied to actions, such as checking a model or caching one action's result.
Example: short-circuiting
A middleware can stop a request by writing a response and not calling next. This is called
short-circuiting. The steps after it never run, but the steps before it still run their "after"
code.
Authentication does this when a token is missing: it answers 401 and the controller never runs. The
homework builds one that pauses book issues during stock-taking.
Apply it to the SMS
Step 1: a timing middleware
using System.Diagnostics;
namespace SchoolApp.Api.Middleware;
// Lesson 67: a middleware that times every request. It runs code before the rest of the pipeline,
// calls the next step with await _next(context), and runs more code after it.
public class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestTimingMiddleware> _logger;
public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
Stopwatch timer = Stopwatch.StartNew();
// Headers must be set before the response starts, so this runs just before it does.
context.Response.OnStarting(() =>
{
context.Response.Headers["X-Response-Time-ms"] = timer.ElapsedMilliseconds.ToString();
return Task.CompletedTask;
});
await _next(context); // everything after this middleware: other middleware, then the controller
_logger.LogInformation("{Method} {Path} responded {StatusCode} in {ElapsedMs} ms",
context.Request.Method, context.Request.Path, context.Response.StatusCode, timer.ElapsedMilliseconds);
}
}
// Lesson 67: app.UseRequestTiming() reads better in Program.cs than app.UseMiddleware<...>().
public static class RequestTimingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestTiming(this IApplicationBuilder app)
{
return app.UseMiddleware<RequestTimingMiddleware>();
}
}
This is a convention-based middleware: a class with a constructor that takes the next
RequestDelegate, and an InvokeAsync(HttpContext) method. ASP.NET Core creates it once, at startup,
and calls InvokeAsync for every request.
The logger arrives through the constructor. That is dependency injection, which Lesson 69 explains; for now, it simply works.
Example: before and after next
InvokeAsync starts a Stopwatch, then await _next(context) runs everything after this middleware,
including the controller. When that returns, the response is complete, so the middleware logs the
method, path, status code, and time.
The extension method UseRequestTiming() hides UseMiddleware<RequestTimingMiddleware>(), so
Program.cs reads like the built-in middleware does.
Example: setting a header in time
Headers are sent before the body. Once the controller starts writing JSON, the headers are already on their way, and changing them throws "Headers are read-only, response has already started".
context.Response.OnStarting(...) registers code that runs just before the headers are sent. That is
the last safe moment to add X-Response-Time-ms.
Step 2: add it to the pipeline
// Lesson 66: the start of SchoolApp.Api. Program.cs has two halves:
// 1. Register services (the "builder" part), 2. build the app and describe how requests are handled.
using SchoolApp.Api.Middleware;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Services: things the app can use. AddControllers finds every controller class in this project.
builder.Services.AddControllers();
builder.Services.AddOpenApi(); // describes the API as an OpenAPI document (Lesson 71)
WebApplication app = builder.Build();
// The request pipeline: what happens to every request, in this order (Lesson 67).
// Each Use... adds one middleware; a request goes down the list, and the response comes back up.
app.UseRequestTiming(); // first, so its time includes everything below it
if (app.Environment.IsDevelopment())
{
app.MapOpenApi(); // /openapi/v1.json, only while developing
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers(); // send each request to the matching controller action
app.Run(); // start the web server and wait for requests
UseRequestTiming() comes first, so its stopwatch includes every step below it. Placed last, just
before MapControllers(), it would time only the action.
Example: SchoolApp.Api's pipeline today
| Line in Program.cs | Kind | What it does to a request |
|---|---|---|
app.UseRequestTiming() | Custom middleware | Starts the clock; logs the result on the way back |
app.MapOpenApi() | Endpoint (Development only) | Answers /openapi/v1.json with the API description |
app.UseHttpsRedirection() | Built-in middleware | Sends plain HTTP requests to HTTPS when an HTTPS port exists |
app.UseAuthorization() | Built-in middleware | Checks permissions; nothing is protected yet (Module 12) |
app.MapControllers() | Endpoints | Runs the matching controller action, at the end of the pipeline |
Modules 10 to 12 add exception handling, authentication, and rate limiting to this table, each in its own place.
Step 3: run and send requests
curl.exe -i http://localhost:5080/api/school
curl.exe -i http://localhost:5080/api/nothing
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Wed, 07 Oct 2026 14:33:58 GMT
Server: Kestrel
Transfer-Encoding: chunked
X-Response-Time-ms: 1
{"name":"NexCoding Public School","currentAcademicYear":"2026-27","serverTimeUtc":"2026-10-07T14:33:58.9838512Z"}
HTTP/1.1 404 Not Found
Content-Length: 0
Date: Wed, 07 Oct 2026 14:33:58 GMT
Server: Kestrel
X-Response-Time-ms: 0
And in the API's terminal:
info: SchoolApp.Api.Middleware.RequestTimingMiddleware[0]
GET /api/school responded 200 in 94 ms
info: SchoolApp.Api.Middleware.RequestTimingMiddleware[0]
GET /api/school responded 200 in 1 ms
info: SchoolApp.Api.Middleware.RequestTimingMiddleware[0]
GET /api/nothing responded 404 in 0 ms
Example: what the log shows
The very first request took 94 ms: ASP.NET Core prepares many things on first use. The next identical request took 1 ms. When you measure an API, always ignore the first request, as Lesson 61 did with queries.
The 404 was timed and logged too. Middleware sees every request, including ones no controller
handles, which makes it the right place for logging, metrics, and error handling.
Common mistakes
- Wrong order. Authorization or exception handling in the wrong place does nothing. Fix: follow the standard order, and comment why each step is where it is.
- Forgetting
await next(context). Every request stops at that middleware. Fix: call next, unless you mean to short-circuit. - Changing headers after the response has started. It throws. Fix: set them before calling
next, or in
OnStarting. - Business logic in middleware. Rules about students or fees end up far from the code that uses them. Fix: middleware for cross-cutting concerns only.
- Slow work in every request. A database call in middleware runs for every request, even
404s. Fix: keep middleware cheap, or limit it to paths that need it.
In real projects
On a school portal, every teacher suddenly got "401 Unauthorized" the morning after a release, although their sign-in worked and no permission had changed.
During a cleanup of Program.cs, someone had sorted the lines, and app.UseAuthorization() now came
before app.UseAuthentication(). Authorization checked each request before anyone had read the
teacher's token, so it saw an anonymous caller every time.
We swapped the two lines back, added a test that signs in and calls a protected endpoint, and wrote the reason for each position in comments. That test has caught the same mistake once since.
Best practices
- Keep the pipeline order standard: exceptions, HTTPS, routing, authentication, authorization, endpoints.
- Use middleware for concerns that apply to every request: logging, timing, errors, security headers.
- Write reusable middleware as a class, with a
UseXyz()extension method. - Short-circuit as early as possible for requests that will be refused anyway.
- Comment the reason for each middleware's position.
Recap
- Middleware is a chain of steps that every request passes through, in order.
- Each step runs code before and after calling the next step.
- The first middleware sees the request first and the response last.
- Order decides behaviour; the wrong order compiles but fails silently.
- Short-circuiting answers without calling the next step.
RequestTimingMiddlewaretimed and logged every SchoolApp.Api request.
Homework
Library module task: In a single-file library API, add two middleware steps with app.Use. The
first prints a line before and after the rest of the pipeline.
The second pauses book issues for stock-taking: POST /api/book-issues gets 503 without reaching
the controller. Send a GET and a POST, and read the order of the lines.
A correct result looks like:
1 in: GET /api/books
2: passed on
BooksController.Get runs
1 out: 200
1 in: POST /api/book-issues
2: stock-taking, request stopped here
1 out: 503
Show the solution
// Library homework, Lesson 67: two middleware steps that show the pipeline's order, and one that
// stops issue requests during stock-taking without ever reaching a controller.
// Run with: dotnet run library-assignment/067-library-middleware.cs
#:sdk Microsoft.NET.Sdk.Web
#:property PublishAot=false
using Microsoft.AspNetCore.Mvc;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders(); // keep the console for this homework's own lines
builder.Services.AddControllers();
WebApplication app = builder.Build();
bool stockTaking = true; // the library is counting its books today
// 1. Runs first on the way in, and last on the way out.
app.Use(async (context, next) =>
{
Console.WriteLine($"1 in: {context.Request.Method} {context.Request.Path}");
await next(context);
Console.WriteLine($"1 out: {context.Response.StatusCode}");
});
// 2. Short-circuits: during stock-taking, issue requests get 503 here and never go further.
app.Use(async (context, next) =>
{
if (stockTaking && context.Request.Path.StartsWithSegments("/api/book-issues"))
{
Console.WriteLine("2: stock-taking, request stopped here");
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
await context.Response.WriteAsJsonAsync(new { error = "Issuing is paused for stock-taking." });
return; // no call to next: the controller never runs
}
Console.WriteLine("2: passed on");
await next(context);
});
app.MapControllers();
app.Run("http://localhost:5067");
[ApiController]
[Route("api/books")]
public class BooksController : ControllerBase
{
[HttpGet]
public string[] Get()
{
Console.WriteLine(" BooksController.Get runs");
return ["Wings of Fire", "The Jungle Book", "Malgudi Days"];
}
}
[ApiController]
[Route("api/book-issues")]
public class BookIssuesController : ControllerBase
{
[HttpPost]
public IActionResult Issue()
{
Console.WriteLine(" BookIssuesController.Issue runs");
return StatusCode(StatusCodes.Status201Created);
}
}
curl.exe -i -X POST http://localhost:5067/api/book-issues sends the issue request. The
BookIssuesController line never prints: the second middleware answered first. The first middleware's
"out" line still prints, because the short-circuit returned to it.
Quiz
In what order do middleware steps see the response?
Show answer
Answer: B. The reverse order: the first one sees it last Each step runs its after code once the steps below it return.
What happens if a middleware does not call next?
Show answer
Answer: B. The request stops there: a short-circuit The stock-taking middleware answered 503 and the controller never ran.
Why is UseRequestTiming first in Program.cs?
Show answer
Answer: B. So its time includes every step below it Placed last, it would time only the action.
Why use OnStarting to set X-Response-Time-ms?
Show answer
Answer: B. Headers cannot change after the response has started OnStarting runs just before the headers are sent.
Which belongs in middleware?
Show answer
Answer: B. Logging every request Middleware is for concerns that apply to every request.
Interview questions
Questions are grouped by experience level. Click a question to see a model answer. All questions: Web API interview questions.
Fresher (0-2 yrs)
F What is middleware in ASP.NET Core? concept
A step in the request pipeline. Each middleware receives the HttpContext and the next step; it can
run code before calling next, stop the request, or run code after the rest of the pipeline returns.
Logging, HTTPS redirection, authentication, and error handling are all middleware.
F What is the difference between app.Use and app.Run(delegate)? concept
app.Use adds a middleware that receives the next step and normally calls it. app.Run with a
delegate adds a terminal step that never calls a next one. app.Map starts a separate branch for a
path prefix. (app.Run() with no arguments, at the end of Program.cs, starts the server.)
F What does short-circuiting mean in the middleware pipeline? concept
A middleware writes the response itself and does not call the next step, so the rest of the pipeline, including the controller, never runs. The earlier middleware still run their "after" code. Authentication answering 401, or a stock-taking check answering 503, are examples.
Mid (3-5 yrs)
M How do you write a custom middleware class? coding
A class whose constructor takes RequestDelegate next (plus singleton services such as a logger) and
which has public async Task InvokeAsync(HttpContext context) that calls await _next(context).
Register it with app.UseMiddleware<T>(), usually through an extension method such as
app.UseRequestTiming().
M Why can't a middleware add a header after calling next, and what do you do instead? coding
Once the action starts writing the body, the headers have been sent, and changing them throws
"Headers are read-only, response has already started". Set headers before calling next, or register
context.Response.OnStarting(...), which runs just before the headers go out.
M What is the usual order of the built-in middleware, and why? concept
Exception handling first, so it wraps everything; then HTTPS redirection, static files, routing and CORS, authentication, authorization, and finally the endpoints. Each step needs what the earlier ones did: authorization needs the user that authentication found.
Senior (6-9 yrs)
S After a cleanup of Program.cs, every signed-in user gets 401. What would you look at first? scenario
The middleware order: UseAuthorization() placed before UseAuthentication() checks requests before
any token has been read, so every caller looks anonymous. Restore the order, document why each
middleware sits where it does, and add a test that signs in and calls a protected endpoint.
How to answer: Go from symptom to the pipeline, explain the dependency between the two steps, and add the regression test so the order is protected.
S In an app built with WebApplication, does it matter where MapControllers() appears relative to app.Use calls? concept
Not for when the endpoints run: MapControllers() only registers them, and WebApplication runs the
endpoint middleware at the end of the pipeline, after every Use... call. A middleware added after
MapControllers() still runs before the controller. The order of the Use... calls among themselves
still matters.
How to answer: Explain the implicit routing and endpoint middleware WebApplication adds, describe how you would verify it, and note that keeping Map calls last is still the readable convention.
Architect / Lead (10+ yrs)
A How do you decide between middleware, filters, and code in the action for a cross-cutting concern? design
Middleware for concerns that apply to every HTTP request, even unmatched ones: timing, error handling, security headers, rate limiting. Filters for concerns tied to actions and their metadata: validation, per-action caching, action-level authorization. The action only for logic specific to that endpoint. Keep business rules out of all three, in the core layer.
How to answer: Give the scope each mechanism sees, a concrete example for each, and the cost of putting a concern at the wrong level, such as running database work for every 404.
Resources
- Code for this lesson: git tag
lesson-67-middleware(code repository coming soon) - Files:
src/SchoolApp.Api/Middleware/RequestTimingMiddleware.cs,src/SchoolApp.Api/Program.cs,library-assignment/067-library-middleware.cs - Video: coming soon
What's next
Requests now reach controllers through a timed pipeline. Next: the first real SMS endpoint, a student from SchoolDb, written as a controller and as a minimal API side by side. Lesson 68: Controllers vs minimal APIs
Want live sessions, code reviews, and interview preparation? Join the next batch at Sahasra Technologies.