← Home
.NETWeb APIReported by candidates

What is middleware, how does the request pipeline work, why does order matter, and how do you write custom middleware?

Asked at Capgemini, Hexaware, Infosys, EPAM, TCS, Wipro, Deloitte and NeoSOFT, often as the first .NET Core question of round one.

Say this first · about 30 seconds
Make it yours · BMW FS lending API (invented example)
The code, simplified

Invented to show the shape, in C#. Tap Next: a payment at 03:10 UTC is stopped; a read goes through.

BMW.sln · where these 2 files sit
  • src/Api/
    • Middleware/
Diagram · step 103:10 UTC: a dealer POSTs a payment. Authorization runs first: signed in, allowed.
Program.cs · line 4 · 1 of 3Enforces endpoints that require sign-in. The payments controller has [Authorize], so a stranger is turned away here, before the 503.

Fix later in your repo Example written for this card: invented, not from a real codebase.

Say it out loud, then check

Show the 30-second answer
  • Middleware is a small piece of code that every HTTP request passes through, one after another, on its way to the controller, and again on the way back.
  • The pipeline: Program.cs lists them in order. Each one gets the (this request and its response) and a next link to the one after it.
  • Use does its work, calls next, and can work again when the response comes back. Run ends the line: no next. Map branches by path.
  • Order matters because requests meet them in that order: exception handler first, then CORS (the browser's check on which other sites may call us), then authentication, then authorization.
  • Custom: a class whose constructor takes RequestDelegate next (the next middleware, as a function) and which has InvokeAsync(HttpContext), added with app.UseMiddleware<T>().
Check your answer: tick what you said

You said 0 of 10

Then add your example: BMW FS lending API (invented example)
  • Situation: our BMW FS lending API posts dealer payments into the FIS core. During FIS's nightly the core rejects writes, so dealers got a raw 500 error.
  • What I did: wrote . While the batch runs, it answers payment POSTs with 503 (service unavailable) and a Retry-After header that says when to try again. It never calls next: a short-circuit.
  • Where it sits: after authorization, so a payment (its controller has [Authorize]) reaches it only from a signed-in dealer, and before the controllers, so no payment work starts. Reads still pass.
  • What I'd say: middleware fits a rule that covers many endpoints and must run before any controller. Its place in Program.cs decides which requests it sees.

See it

1/11Before: every controller did the same jobs itself: catch errors, tag the logs, check the token.

In plain words

The senior answer · about 2 minutes
What it is
  • Middleware is a component in ASP.NET Core's request pipeline. Every HTTP request passes through each one in order, and each one can do some work, call the next one, and then do more work when the response comes back.
  • Each middleware wraps the next one. At start-up ASP.NET Core chains them into a single , a function that takes the , so each one holds a link to the next. In a class that link is the _next field, and you call it with await _next(context); people just say "call next".
  • adds a middleware that calls next. adds one that ends the pipeline. branches it by path. A middleware that answers without calling next short-circuits the rest, and authorization returning 401 is the everyday example.
Why order matters
  • The request meets the middleware in exactly the order they are added in Program.cs, and the response meets them in reverse. So the order is behaviour, not style.
  • The exception handler goes first, because it can only catch errors thrown by the middleware that come after it.
  • Authentication goes before authorization. Authentication works out who you are and puts that in ; authorization then checks what that user may do. Swap them and every protected call returns 401.
  • CORS goes after routing and before authentication. Before a browser page calls an API on another site with a token, the browser first sends a preflight: a request with no token, asking whether that site may call. If authorization runs first and every endpoint needs a signed-in user, the preflight gets 401 and the browser blocks the real call. I have seen that reach production; it was missed because Postman and integration tests never send a preflight.
Writing my own
  • For anything reused I write a class: a constructor that takes RequestDelegate next, an InvokeAsync(HttpContext) method, and one app.UseMiddleware<T>() line in Program.cs.
  • That class is built once for the whole app, so it behaves like a singleton. Scoped services, like a , go into 's parameters, never the constructor. Otherwise one request's object is trapped inside a long-lived one and shared by every request: a captive dependency.
  • If I add a response header, I register it with , a callback ASP.NET Core runs just before the headers go out. Once the body starts, the headers have already been sent, so setting one after next() throws.
In production
  • In our lending API, one custom middleware puts a correlation id on every log line and on the response, so a dealer's support ticket leads straight to one trace in .
  • Middleware runs on every request, so I keep it cheap, and I never log request bodies or tokens from it.
  • I test the order with an integration test through , because a unit test cannot see the pipeline. After a release moved below authorization and dealers got CORS errors, that test now sends a real preflight.

One request, every middleware, in and out

One real request through the lending API pipeline. Down the left column on the way in, then the right column on the way back up. The response fills in below.

1/11One real call: a BMW FS dealer posts a payment for loan 7781 from the dealer portal.

The story

  1. Before

    In .NET Framework, IIS, the Windows web server, ran every ASP.NET request through a fixed list of events. Shared code hooked into those events from Global.asax or an listed in web.config.

    1/5IIS ran every request through the same fixed events, for every app, in IIS's order.

  2. Why it had to change

    The order was hidden in IIS and web.config, and the app could not run outside IIS.

  3. How it works now

    ASP.NET Core made the pipeline an ordered list of functions in Program.cs. Each one gets the and a next to call. Kestrel, the web server built into the app, feeds requests into the top.

  4. What happens next

    The pipeline is now code you read top to bottom. The storyboard below follows one real payment through all of it.

  5. What we gained

    You read the order in one file and can test it. A shared job is written once instead of in 40 controllers. And the same app runs on Kestrel anywhere: , a container, Linux.

In the lending API

Program.cs runs once at start-up. The app.Use… lines build the pipeline: a request passes them top to bottom, the response bottom to top.

Only the key lines are shown; Deep walks the whole file.

Lending.sln · where this file sits
  • src/Api/
Program.cs · line 27 · 1 of 4First, so it wraps everything after it. An exception thrown anywhere below climbs back up here and becomes a 500 . Anything above this line is not covered.

The code, file by file

The BMW FS lending API: the pipeline in Program.cs, our custom middleware, and the controller at the end of the line. Tap a line to see where the request is.

Lending.sln · where these 3 files sit
  • src/Api/
    • Middleware/
    • Controllers/
Program.cs · line 4 · 1 of 12Lets the exception handler below answer with a JSON body (status, title, trace id) instead of an empty 500.

What they ask next

The follow-ups interviews really ask. Answer out loud, then tap to check.

Follow-up tree

Answer, then open the next level. That is the order an interviewer digs in.

Reporteda candidate was asked Intervieweran interviewer asks it

Production bugs, and how to tell them

Each one: what you see, why, the fix in code, and the line to say.

Production incident: Dealers could not submit credit applications

P1 · outage or data leakNavistar Financialcame with a deployCredit application API behind the dealer portal

Composed, not a real event at Navistar Financial. The mechanism is real and common: CORS placed after authorization breaks only browser calls, so every non-browser check stays green.

The story in 5 lines
  1. After Thursday night's release, Navistar dealers could not submit truck from the dealer portal. Every save showed a CORS error.
  2. The API was up: health checks, Postman and every integration test passed. showed a wall of 401s on OPTIONS requests.
  3. A that added had moved below . The browser's preflight carries no token, so it got 401 before CORS could answer.
  4. We swapped the back in 40 minutes, then shipped the right order with a test that sends a real preflight.
  5. Now an sends a preflight every 5 minutes, and Program.cs explains its order in a comment.
Who's who (5 people)
You
.NET developer on the vendor team (L3), also on the Navistar portal. Owns the fix.
Tech lead
Vendor senior. Runs the bridge call, reviews every PR.
L2 pod
Vendor support in Navi Mumbai. Runbooks, slot swaps with approval.
Navistar dealer support
First line for US truck dealers (L1).
Navistar IT manager
Approves the rollback and the fix's release.

Tap a step. The map lights the place where it happens.

1/8 Detect

Friday morning in the US: dealers can't submit . The browser shows a CORS error.

Fri 17 Oct, 07:40 CT = 18:10 ISTNavistar dealer support, then L2

  • US truck dealers start their day. Saving a spins, then shows "Something went wrong". Dealer support logs four tickets in 20 minutes.
  • The in Navi Mumbai picks them up at 18:10 IST. The health check alert is green and the API answers.
  • One dealer sends a screenshot of the browser's console. It shows the real message: a CORS error.
Browser console (the dealer's screenshot)
Access to XMLHttpRequest at 'https://api.nfc.example/api/credit-apps' from origin 'https://dealers.nfc.example' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
In theory

A check that behaves like a browser catches it minutes after the release swap.

In practice

The health check calls /health with no Origin header, so it passed all night.

Try it yourself · 10 minutes, on your Mac
  1. dotnet new webapi -n OrderLab. Add a CORS policy for http://localhost:4200, JWT bearer authentication, and a fallback policy that needs a signed-in user.
  2. Put app.UseCors() after app.UseAuthorization(). Run it and send the preflight from the Reproduce step with curl: 401.
  3. Move above . Same curl: 204 with an Access-Control-Allow-Origin header.
You should see

Moving one line turns 401 into 204. Nothing else changed.

The senior move

"When something works in Postman but fails in the browser, I check the preflight and the middleware order before I touch the CORS policy."

Sources Sourced: the browser's CORS error text (Chromium), Microsoft's middleware order guidance ( before and ), the CORS middleware answering a preflight with 204, and ( docs). Composed: Navistar's systems, every date, number and name, and the (standard columns).

Traps

Traps

What should you say instead? Tap to check.

Live coding

Easy first, then harder. Set a timer and type it before you look.

Task 1

Question index: Musteasy · 15 minutes

Write middleware that times every request, adds an X-Response-Time header, and logs a warning when a request takes longer than 1 second. Say where it goes in Program.cs.

Question index (Must, easy), derived from the middleware question; Microsoft Learn's custom middleware guide
You are given
Lending.sln · where this file sits
  • src/Api/
Stuck? Hints
  1. Start a before await _next(context) and read it after.
  2. Headers must be set before the body starts. Which hook lets you add one in time?
  3. What if the code below throws? Should the slow request still be logged?
  4. Place it so it times as much as possible, and so its log line carries the correlation id.
Show a solution
Lending.sln · where these 2 files sit
  • src/Api/
    • Middleware/
RequestTimingMiddleware.cs · line 12 · 1 of 5Built once by ; the logger is a singleton, so the constructor is the right place for it.
What the interviewer checks
  • Calls await _next(context) and measures around it.
  • Adds the header in , not after next().
  • Logs in a finally, so failed requests are timed too.
  • Uses a message template with named values ({Ms}), not string joining, so can filter by them.
  • Placed right after the correlation id middleware: it times nearly everything, including auth failures, and its warning carries the id.
  • Senior extra: the header shows time to first byte, the log shows the full time. Say which one you report.
  • Senior extra: ASP.NET Core 8+ already records the http.server.request.duration metric. In production you alert on that; this middleware adds the header for support tickets.
With AI tools

Interviewers now ask how you check AI-written code. Middleware is where AI drafts look right and fail at run time. Copilot drafted our timing middleware with the header set after await next(). It compiled and the unit test passed; the first real request logged "Headers are read-only". I moved it into and added an integration test. When AI suggests a Program.cs change, I read every moved Use… line as a behaviour change, because order mistakes do not show up as compile errors.

Glossary: every name, one line each →

My notes

Paste the NOTES block from your doubt chat here. The grader never reads notes; the coach reads "Still unclear".

Read moreWhere it was asked