Skip to main content

The Middleware Pipeline: Every Request's Journey – Web API | Lesson 67

Published / updated

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 how Run and Map differ 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​

MethodDoesExample
app.Use(...)Adds a middleware that can call the next oneLogging, timing
app.Run(...)Adds a final middleware that never calls a next oneA fixed reply at the end of a branch
app.Map("/path", ...)Starts a separate branch for paths that begin with /pathA health-check branch
app.UseXyz()A built-in middleware, added through an extension methodUseHttpsRedirection()

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:

OrderMiddlewareWhy there
1Exception handling (Lesson 74)Must wrap everything to catch every error
2HTTPS redirectionRedirect before doing any real work
3Static files, if anyServe files without the cost of the rest
4Routing, CORSDecide the endpoint; allow websites on other addresses to call the API
5Authentication, then authorizationWho is calling, then are they allowed (Module 12)
6Endpoints: 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​

src/SchoolApp.Api/Middleware/RequestTimingMiddleware.cs
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​

src/SchoolApp.Api/Program.cs
// 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.csKindWhat it does to a request
app.UseRequestTiming()Custom middlewareStarts 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 middlewareSends plain HTTP requests to HTTPS when an HTTPS port exists
app.UseAuthorization()Built-in middlewareChecks permissions; nothing is protected yet (Module 12)
app.MapControllers()EndpointsRuns 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​

Terminal 2
curl.exe -i http://localhost:5080/api/school
curl.exe -i http://localhost:5080/api/nothing
Output
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:

Output
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​

  1. 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.
  2. Forgetting await next(context). Every request stops at that middleware. Fix: call next, unless you mean to short-circuit.
  3. Changing headers after the response has started. It throws. Fix: set them before calling next, or in OnStarting.
  4. 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.
  5. 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​

From 16 years of .NET 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.
  • RequestTimingMiddleware timed 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:

Output
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-assignment/067-library-middleware.cs
// 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​

  1. In what order do middleware steps see the response?

    1. The same order as the request
    2. The reverse order: the first one sees it last
    3. Random order
    4. Only the last one sees it
    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.

  2. What happens if a middleware does not call next?

    1. The app crashes
    2. The request stops there: a short-circuit
    3. The next step runs anyway
    4. The response is empty only for GET
    Show answer

    Answer: B. The request stops there: a short-circuit The stock-taking middleware answered 503 and the controller never ran.

  3. Why is UseRequestTiming first in Program.cs?

    1. It must be first to compile
    2. So its time includes every step below it
    3. So it runs only once
    4. Because it short-circuits
    Show answer

    Answer: B. So its time includes every step below it Placed last, it would time only the action.

  4. Why use OnStarting to set X-Response-Time-ms?

    1. It is faster
    2. Headers cannot change after the response has started
    3. It runs after the response ends
    4. It is required for logging
    Show answer

    Answer: B. Headers cannot change after the response has started OnStarting runs just before the headers are sent.

  5. Which belongs in middleware?

    1. The rule that fees cannot exceed the balance
    2. Logging every request
    3. Calculating report cards
    4. Mapping entities to DTOs
    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.

See the example · Permalink

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.)

See the example · Permalink

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.

See the example · Permalink

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().

See the example · Permalink

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.

See the example · Permalink

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.

See the example · Permalink

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.

See the example · Permalink

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.

See the example · Permalink

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.

See the example · Permalink

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.