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
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>().
Make it yours · 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.
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.
AuthorizationUseAuthorization
Our middlewareMaintenanceWindow
Controllersits next
dealer ✓
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 repoExample 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
Dealer portal
Loanscatch · log · token
Paymentscatch · log · token
Quotescatch · log
1/11Before: every controller did the same jobs itself: catch errors, tag the logs, check the token.Problem: 40 controllers, 40 copies. Quotes forgot the token check, so anyone could call it.Fix: each shared job becomes one middleware. Program.cs lines them up once, in order.A request comes in at the top. The exception handler wraps everything below it.Each middleware does its job, then calls next() to pass the request one step down.Authentication reads the token and records who is calling in .Authorization checks that this dealer may call this endpoint. Yes, so it calls next().The endpoint, your controller action, runs and writes the response.The response climbs back up in reverse order. Code written after next() runs now.No token: authentication leaves the user empty, so authorization answers 401. A short-circuit.The action throws. The error climbs back up until the exception handler turns it into a clean 500.
In plain words
Think of the doors at a BMW FS office. A first-aider stands at the entrance for anything that goes wrong inside. The front desk writes you in the logbook, security checks your badge, the floor door checks you may enter that floor, and then you reach the person you came to see. On the way out you pass the same doors in reverse.
Each door is a middleware: first-aider = exception handler, logbook = correlation id, badge = authentication, floor door = authorization. The person you visit is your controller. Program.cs decides the order of the doors.
A door can turn you away: no badge, no entry, and you never reach the desk. That is a short-circuit.
A job every visitor needs, like the badge check, sits in one door, instead of being repeated at every desk.
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.
Pipeline, top first
Middleware
In
Out
Exception
try {
catch: 500
Correlation
book header
close scope
Routing
pick route
–
Cors
allow header
–
AuthN
read token
–
AuthZ
Dealer ✓
–
Controller
save, 201
write → sent
1/11One real call: a BMW FS dealer posts a payment for loan 7781 from the dealer portal.Exception handler: wraps everything below in a try/catch. Nothing else to do yet.Correlation id: tags every log line with 9f3c and books the response header for later.Routing reads the URL and picks the endpoint. It does not run it yet, so later steps can check it.CORS: the portal is a different site. It is on our allowed list, so the reply will say so.Authentication checks the token's signature and expiry, then records the dealer in .Authorization: the endpoint needs the Dealer policy, and this user has the dealer role. Allowed.The action runs: the payment is recorded, its log line carries 9f3c, and it sets 201.Back up. The controller starts writing; just before the first byte, the booked header is added.Nothing was thrown, so the handler lets it pass. The portal gets 201 and the id for tickets.Same call, database down: the controller throws, the error climbs to the handler: a clean 500.
The story
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.
IISWindows web server
BeginBeginRequest
AuthAuthenticateRequest
Handleryour page or controller
1/5IIS ran every request through the same fixed events, for every app, in IIS's order.Shared code, like logging, hooked into an event with an .Which modules ran sat in web.config, not in code. You could not read the order in one place.And it all lived in System.Web, a library tied to IIS, so the app ran only on Windows.ASP.NET Core: the pipeline is plain code in Program.cs, in the order you write.
Why it had to change
The order was hidden in IIS and web.config, and the app could not run outside IIS.
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.
What happens next
The pipeline is now code you read top to bottom. The storyboard below follows one real payment through all of it.
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.
ReportedWhat is the difference between app.Use and app.Run?
adds a middleware that gets next: it can work before and after the rest of the pipeline, or stop early. adds a terminal one: no next, so nothing after it ever runs.
app.Use(async (ctx, next) =>
{
var sw = Stopwatch.StartNew();
await next(ctx); // or next(): same
app.Logger.LogInformation(
"{Ms} ms", sw.ElapsedMilliseconds);
});
app.Run(ctx =>
ctx.Response.WriteAsync("pong"));
app.UseAuthorization(); // never reached
ReportedWhy does middleware order matter? Give an example.
Requests meet them in exactly the Program.cs order. Exception handler first, so it catches everything below. Authentication before authorization, or every call is 401. CORS before both, or the browser's no-token preflight check gets 401.
ReportedMiddleware vs filters: what is the difference?
Middleware runs for every request and knows only the . Filters run only around MVC actions and can see the action, its arguments and the model state. App-wide jobs are middleware; rules for some actions are filters. Full answer: the filters card (Related).
Answer, then open the next level. That is the order an interviewer digs in.
Reporteda candidate was asked Intervieweran interviewer asks it
ReportedWhat is the difference between app.Use and app.Run?
adds a middleware that gets next: it can work before and after the rest of the pipeline, or stop early. adds a terminal one: no next, so nothing after it ever runs.
app.Use(async (ctx, next) =>
{
var sw = Stopwatch.StartNew();
await next(ctx); // or next(): same
app.Logger.LogInformation(
"{Ms} ms", sw.ElapsedMilliseconds);
});
app.Run(ctx =>
ctx.Response.WriteAsync("pong"));
app.UseAuthorization(); // never reached
app.Use: timergets next
GET /ping
timer: running
app.Run: "pong"no next: the end
app.Use: anything elseafter a Run
1/4: start the timer, then call next.: writes "pong" and returns. It has no next to call.Anything added after a is never reached.Back up in : the code after await next runs and logs the time.
It branches the pipeline by the start of the path. Requests to /internal/... go down the branch's own small pipeline and never return to the main line. branches on any condition; branches and then rejoins. Don't confuse it with or , which add endpoints, not branches.
Explain this lineProgram.cs ends with app.Run(). Is that the middleware?
No. With no arguments, app.Run() starts the web server and waits until the app stops. The terminal middleware is app.Run(context => ...), with a function. Same name, different job.
What is short-circuiting? Give real examples.
A middleware writes the response and does not call next, so nothing below runs. Everyday ones: authorization returning 401 or 403, CORS answering a browser's preflight, the rate limiter returning 429 (once is set; the default is 503), static files returning a file. That is also why the exception handler and request logging go above them: they must see every request.
ReportedWhy does middleware order matter? Give an example.
Requests meet them in exactly the Program.cs order. Exception handler first, so it catches everything below. Authentication before authorization, or every call is 401. CORS before both, or the browser's no-token preflight check gets 401.
After , before and . Before a cross-site call with a token, the browser first sends a preflight: an OPTIONS request with no token, asking which sites may call. Most secure APIs set a fallback policy (every endpoint needs a signed-in user), so if authorization runs first, the preflight gets 401 with no CORS headers, and the browser reports a CORS error while the API is up. Postman and integration tests never send preflights, so only real browsers fail.
Why is the exception handler first?
It wraps the rest of the pipeline in a try/catch. Anything thrown below it, in auth, our middleware or a controller, climbs back to it and becomes a 500 with a trace id, instead of a dropped connection or a stack trace. Anything above it is not covered. One limit: if the response has already started, it can only log. In .NET 8 the handling logic goes in an ; the global handler is its own card (Related).
ReportedHow does authentication work in a Web API?
The authentication middleware reads the Bearer token from the Authorization header. It checks that (Microsoft's sign-in service) signed it, that it was issued for this API, and that it has not expired, then puts the claims in . A request with no token is not rejected here; the user is just empty. Authorization then checks policies and roles against that user. The full flow, from login to refresh, is its own card (Related: JWT authentication).
ReportedMiddleware vs filters: what is the difference?
Middleware runs for every request and knows only the . Filters run only around MVC actions and can see the action, its arguments and the model state. App-wide jobs are middleware; rules for some actions are filters. Full answer: the filters card (Related).
Conventional middleware or : what is the difference?
A conventional class (constructor takes RequestDelegate next, method InvokeAsync(HttpContext)) is built once, so it behaves like a singleton; scoped services go in 's parameters. A class that implements is registered in the container and built per request with the lifetime you give it, so its constructor can take scoped services. I default to conventional, and use when the constructor needs something scoped.
// C# 12 primary constructor: db is a// constructor parameter.publicsealedclassAuditMiddleware(
ServicingDbContext db) : IMiddleware
{
publicasync Task InvokeAsync(
HttpContext ctx, RequestDelegate next)
{
await next(ctx);
// AuditRows: a DbSet<AuditRow> on our// context; AuditRow is a small record.
db.AuditRows.Add(newAuditRow(
ctx.Request.Path,
ctx.Response.StatusCode));
await db.SaveChangesAsync();
}
}
// Program.cs: per request, UseMiddleware// asks the container for a new one.
builder.Services.AddScoped<AuditMiddleware>();
app.UseMiddleware<AuditMiddleware>();
How do you use a scoped service, like a , in conventional middleware?
Add it as a parameter of after the . ASP.NET Core resolves it from the request's scope on every call. Never put it in the constructor.
Your logging middleware logs request headers and bodies. What can go wrong?
It copies secrets and personal data into logs that many people can read and that are kept for months: the Authorization header holds the dealer's token, and a payment body holds bank details. I log an allow-list only: method, path, status, duration, correlation id. If I need more, the built-in with an allow-list of headers, and never the body on money endpoints. Reading the body also needs , or the controller finds it empty.
How would you test a middleware?
Two levels. A unit test builds a , passes a fake next that records it was called, and checks what it can see without a server: that next ran and what was put on the context. callbacks only run when a real response starts, so the header is checked in the integration test: sends real requests through the real pipeline, the only way to test order too. For CORS that test sends an OPTIONS preflight with an Origin header and asserts the allow header comes back.
var ctx = newDefaultHttpContext();
var called = false;
RequestDelegate next = _ =>
{
called = true;
return Task.CompletedTask;
};
var mw = newCorrelationIdMiddleware(
next, NullLogger<
CorrelationIdMiddleware>.Instance);
await mw.InvokeAsync(ctx);
Assert.True(called);
Production bugs, and how to tell them
Each one: what you see, why, the fix in code, and the line to say.
Every protected call returns 401 after a refactor
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
Every [Authorize] endpoint, even with a valid token.
No exception in the logs.
Cause
sat above . When authorization checked the user, nobody had read the token yet, so was empty.
Fix
Swap the two lines. Add an integration test that calls one protected endpoint with a valid test token and expects 200.
Diagram · step 1A dealer calls with a valid token. It sits in the request; nobody has read it yet.
GET /loans/7781
token: in the request
UseAuthorizationchecks the user
UseAuthenticationreads the token
LoansControllerthe endpoint
Before · Program.cs · line 2 · 1 of 3A dealer calls with a valid token. It sits in the request; nobody has read it yet.
Say it
Authentication answers "who are you" and fills ; authorization answers "may you" using that user. Run them backwards and authorization always sees an anonymous caller, so everything is 401. It is silent: no exception, only 401s.
A header set after the response started
System.InvalidOperationException: Headers are read-only, response has already started.
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowHeadersReadOnlyException()
at Lending.Api.Middleware.CorrelationIdMiddleware.InvokeAsync(HttpContext context)
warn: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware
The response has already started, the error handler will not be executed.
Cause
The first version of our middleware set X-Correlation-ID after await _next(context). By then the controller had written the body, and headers always leave before the body.
Fix
Book the header with before calling next. ASP.NET Core runs that callback just before the headers are sent.
Diagram · step 1The middleware picks an id for this request: 9f3c.
Our middlewareCorrelationIdMiddleware
Before · CorrelationIdMiddleware.cs · line 4 · 1 of 3Same helper as in the code walk above: the caller's id, or this request's .
Say it
Headers travel before the body, so once the first byte of the body is sent they are locked. Code after next() runs too late to add one; is the hook for it. The client still got its 201, which is why this hid in the logs for a week.
A DbContext in a middleware's constructor
Development:
System.InvalidOperationException: Cannot resolve scoped service 'Lending.Infrastructure.Servicing.ServicingDbContext' from root provider.
Production (scope checks off), under load:
System.InvalidOperationException: A second operation was started on this context instance before a previous operation completed.
Cause
A middleware class is built once, when the pipeline is built, from the root provider. took the scoped in its constructor, so one context served every request. In Development the container's scope check () catches it at start-up; in Production that check is off by default, so it only shows under load.
Fix
Ask for the context as an parameter. ASP.NET Core fills those from the current request's scope on every call.
Diagram · step 2The context goes into a field. It now lives as long as the app: a captive dependency.
Root providerbuilds the middleware once
DealerStatusMiddlewareone for the app
_db
ServicingDbContextregistered: scoped
Before · DealerStatusMiddleware.cs · line 4 · 1 of 4A field lives as long as the object. This object lives as long as the app.
Say it
Conventional middleware behaves like a singleton. Its constructor parameters are resolved once, from the root; its parameters are resolved per request. So singletons go in the constructor and scoped services go in . It is the same captive dependency as in the .
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
After Thursday night's release, Navistar dealers could not submit truck from the dealer portal. Every save showed a CORS error.
The API was up: health checks, Postman and every integration test passed. showed a wall of 401s on OPTIONS requests.
A that added had moved below . The browser's preflight carries no token, so it got 401 before CORS could answer.
We swapped the back in 40 minutes, then shipped the right order with a test that sends a real preflight.
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.
Dealer browserportal on another site
Credit app APIApp Service, .NET 8
Desk → L2 podNavistar, then Navi Mumbai
FIS coreapplications land here
You + tech leadL3: our code
App Insightsrequests, 401s, KQL
Azure DevOpsPR, tests, release
Staging slotthe previous build waits
1/8 Detect
Friday morning in the US: dealers can't submit . The browser shows a CORS error.
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
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.
Put app.UseCors() after app.UseAuthorization(). Run it and send the preflight from the Reproduce step with curl: 401.
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."
SourcesSourced: 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
Don't sayMiddleware runs only on the way in.SayCode before next() runs on the way in; code after it runs on the way out, in reverse order.
Don't sayASP.NET Core sorts the middleware for you.SayThey run exactly in the order written. A wrong order compiles and fails at run time.
Don't sayPut the in the middleware's constructor.SayThe class is built once. Scoped services go in 's parameters.
Don't sayAdd the response header after await next().SayBy then the body has started and headers are locked. Use .
Don't sayFix a CORS error by allowing any origin.SayFirst check the order: CORS must run before authentication, or the preflight gets 401.
Traps
What should you say instead? Tap to check.
Don't sayMiddleware runs only on the way in.
Say instead
Code before next() runs on the way in; code after it runs on the way out, in reverse order.
Don't sayASP.NET Core sorts the middleware for you.
Say instead
They run exactly in the order written. A wrong order compiles and fails at run time.
Don't sayPut the in the middleware's constructor.
Say instead
The class is built once. Scoped services go in 's parameters.
Don't sayAdd the response header after await next().
Say instead
By then the body has started and headers are locked. Use .
Don't sayFix a CORS error by allowing any origin.
Say instead
First check the order: CORS must run before authentication, or the preflight gets 401.
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.
Start a before await _next(context) and read it after.
Headers must be set before the body starts. Which hook lets you add one in time?
What if the code below throws? Should the slow request still be logged?
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.