Middleware

By the end of this lesson you'll understand how an ASP.NET Core request flows through the middleware pipeline — why ordering matters, how a step can let a request pass or short-circuit it, and how to write your own middleware, filters, and attributes for cross-cutting concerns like logging, auth, and error handling.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

The middleware pipeline is like the airport security checkpoints a passenger passes through: ticket check, then bag scan, then passport control, then the gate. Each checkpoint can wave you through, modify you (make you take your belt off), or stop you completely. The order is fixed — you can't reach the gate before passport control. And on the way back out you pass the same posts in reverse. An ASP.NET request flows through middleware exactly like that: in through each step, to the handler at the gate, then back out through each step in reverse.

A cross-cutting concern is a job that applies to lots of requests rather than to one feature — logging, authentication, compression, error handling, CORS. You don't want to copy that code into every controller. Middleware is where it lives: a chain of small components that each get a turn at the request before (and after) your handler runs.

Each component is handed next — a delegate representing the rest of the pipeline. The component runs some code, optionally calls next to pass the request along, and then runs more code once the response comes back. Calling next makes it a passthrough ; not calling it short-circuits the request.

ASP.NET can't run in this in-browser sandbox, so the ASP.NET examples below are read-only with their console output shown in // ✅ Expected output comments. The 🎯 Your Turn exercises model the exact same pipeline idea in plain C# that does run here, so you can feel the in/out flow for yourself.

1. The Pipeline & Why Order Matters

A request flows down through each middleware in the order you register them, hits the terminal handler, then flows back up in reverse. The code before await next() runs on the way in; the code after it runs on the way out. That nesting is why a step registered first wraps everything after it — so error-handling and logging go first, and authentication goes before anything that needs to know who the user is. Read this worked example and trace the output.

2. Use, Run, Map & Short-Circuiting

There are a few verbs for building the pipeline. app.Use adds a passthrough step that receives next and should call it. app.Run adds a terminal step with no next — it ends the pipeline. app.Map (and MapWhen / UseWhen ) branches the pipeline based on the path or a predicate. And any step can short-circuit : by simply not calling next (e.g. returning a 403), it stops the request before it reaches the handler.

3. Build a Pipeline Yourself

Middleware is really just a chain of responsibility : each step holds a reference to the next and decides whether to call it. You can model that in plain C# with delegates — and unlike the ASP.NET examples, this code runs right here. Fill in the three ___ blanks to wire a Logging step around an Auth step around the handler, then run it and watch the in/out order.

Now make a step short-circuit . The gate below checks for an API key; when it's missing it should print the blocked message and stop — without calling the handler. Fill in the one blank so the request never reaches the handler.

4. Custom Middleware, Filters & Attributes

For anything beyond a one-liner, write a middleware class : its constructor captures next (a RequestDelegate ) and its InvokeAsync(HttpContext) runs per request. Filters are more targeted — they run only around MVC controller actions, so use them for concerns scoped to controllers (model validation, auditing) rather than every static file or health check. A custom attribute is filter logic you attach declaratively, like [RequireApiKey] , keeping security out of your business code.

Middleware sees every request, including static files, health checks, and requests that never reach a controller. It only knows about the raw HttpContext — paths, headers, status codes. Reach for it for truly global concerns: logging, error handling, CORS, compression, authentication.

Filters run inside MVC, after routing has chosen an action, so they know the controller, the action, and the bound model arguments. That makes them perfect for things like model validation or auditing a specific endpoint — work that only makes sense once you know which action is about to run.

Rule of thumb: if it should happen for requests that may never hit a controller, it's middleware. If it depends on the action or its model, it's a filter. In Minimal APIs, the equivalent of an action filter is IEndpointFilter , which supports DI and async cleanly.

Q: What's the difference between app.Use and app.Run ?

Use is a passthrough that gets a next delegate and should call it so later steps run. Run is terminal — it has no next and ends the pipeline, so nothing registered after it ever executes.

The request short-circuits there. Anything registered after that step — including your controllers — never runs. Sometimes that's exactly what you want (a 401 gate); when it isn't, it looks like the request hung or returned nothing.

Q: When should I use a filter instead of middleware?

Use middleware for global concerns that apply to every request (logging, errors, CORS). Use a filter when the logic depends on the chosen MVC action or its model — like validating a specific endpoint's input — because filters run after routing.

Q: Why does the order I register middleware matter so much?

Each step wraps everything registered after it, in and out like nested boxes. So error handling must be first to catch downstream throws, and authentication must come before any step that needs to know who the user is.

Q: Are custom attributes the same as filters?

They're filters in a friendlier wrapper. A class like RequireApiKeyAttribute implements a filter interface (e.g. IAuthorizationFilter ) and inherits Attribute , so you can attach it declaratively with [RequireApiKey] .

No blanks this time — just a brief and an outline. Using the Step helper provided, build the classic middleware chain in plain C#: a Logging step wrapping an Auth step wrapping the handler, and print the in/out order. Run it and check your output against the expected lines in the comments.

Practice quiz

In what order does a request flow through the middleware pipeline relative to the response?

  • In and out in the same order
  • Randomly
  • In through each step, then out in reverse order
  • Only inward

Answer: In through each step, then out in reverse order. A request flows down through each middleware to the handler, then back up in reverse — nesting like Russian dolls.

In passthrough middleware, when does code before 'await next()' run?

  • On the way in (request)
  • On the way out (response)
  • Never
  • Only on errors

Answer: On the way in (request). Code before await next() runs on the way in; code after it runs on the way out.

What is the difference between app.Use and app.Run?

  • Use is terminal; Run is passthrough
  • They are identical
  • Run runs first always
  • Use is a passthrough that gets next(); Run is terminal with no next

Answer: Use is a passthrough that gets next(); Run is terminal with no next. Use is a passthrough that receives and should call next(); Run is terminal and ends the pipeline.

What does 'short-circuiting' the pipeline mean?

  • Calling next() twice
  • Not calling next(), so the request stops before the handler
  • Throwing an exception
  • Reordering middleware

Answer: Not calling next(), so the request stops before the handler. By not calling next() (e.g. returning a 403), a step stops the request before it reaches the handler.

Why must exception-handling middleware be registered first?

  • So its try/catch wraps every later step and can catch downstream throws
  • It runs fastest
  • It needs the user identity
  • Order doesn't matter

Answer: So its try/catch wraps every later step and can catch downstream throws. A step only protects what comes after it, so error handling must be first to wrap and catch downstream exceptions.

What do app.Map and app.MapWhen do?

  • End the pipeline
  • Add authentication
  • Branch the pipeline based on the path or a predicate
  • Log requests

Answer: Branch the pipeline based on the path or a predicate. Map branches by path; MapWhen/UseWhen branch on any predicate, giving requests their own sub-pipeline.

What happens to middleware registered after app.Run?

  • It runs first
  • It never executes — Run is terminal
  • It runs in parallel
  • It catches exceptions

Answer: It never executes — Run is terminal. app.Run ends the pipeline, so anything registered after it never runs.

How does a custom middleware class receive the rest of the pipeline?

  • As a static field
  • It doesn't
  • Via a global variable
  • Its constructor captures next (a RequestDelegate); InvokeAsync runs per request

Answer: Its constructor captures next (a RequestDelegate); InvokeAsync runs per request. A middleware class takes next (RequestDelegate) in its constructor and implements InvokeAsync(HttpContext) per request.

When should you use a filter instead of middleware?

  • For static files
  • When the logic depends on the chosen MVC action or its model
  • For every request including health checks
  • Never

Answer: When the logic depends on the chosen MVC action or its model. Filters run after routing, inside MVC, so use them when the logic needs the action or its bound model; use middleware for global concerns.

Why should you not inject a scoped service into a middleware constructor?

  • It's faster as a field
  • Scoped services don't exist
  • The middleware is a singleton; accept scoped services as InvokeAsync parameters instead
  • It causes infinite loops

Answer: The middleware is a singleton; accept scoped services as InvokeAsync parameters instead. Middleware is constructed once (singleton), so take per-request scoped services as InvokeAsync parameters to get a per-request instance.