What are filters in ASP.NET Core (authorization, resource, action, exception, result), in which order do they run, and how are they different from middleware?
17 reports in the question index (8 linked below): Hexaware (4), Publicis Sapient, Capgemini, TraviYo, Excellon Software, Colan Infotech, Infosys and others, in screening and first technical rounds.
Say this first · about 30 seconds
Filters are code that MVC runs before and after particular steps of handling a request, so things like auditing, validation or turning exceptions into error responses are written once instead of in every action.
There are five kinds, in this order. Authorization filters first, a check that the request may run. Then resource filters, which can answer from a cache. Then the parameters get filled in, and action filters run just before and after the action. Exception filters handle a throw, and result filters wrap writing the response.
The difference from middleware is scope and knowledge. Middleware runs for every request and sees the HTTP request; after routing it can see which endpoint was chosen, but not the arguments or the result. Filters run only for controller actions and know the action, its arguments and its result. So cross-cutting HTTP things like HTTPS, CORS and authentication go in middleware, and action-aware things go in filters.
You register a filter globally in the controller options, or as an attribute on a controller or an action. One trap I've seen: a filter registered as a single instance is shared by every request, so a field in it got overwritten by parallel requests and the audit log recorded the wrong approver.
Make it yours · UDC Finance payments API (invented example)
Situation: every night between 01:00 and 03:00 New Zealand time, the FIS core runs its , which posts the day's payments to the contracts. Payments that our API posted during that window sometimes landed half-processed, and fixed them by hand the next morning.
What I did: an action filter attribute, [BlockDuringFisBatch], on the four actions that post money. During the window it sets context.Result to 503 Service Unavailable ("try again later"), so the action never runs. Reads stay open all night. UTC is the world clock time with no time zone; the filter converts it to New Zealand time, so daylight saving is handled.
Result: no more half-posted payments, and no manual fixes. The UDC portal shows "payments open again at 3 am" when it gets the 503.
What I'd say: a filter fits because only some actions need it and it must stop the action before it runs: it sets the result, and MVC skips the action.
The code, simplified
Invented to show the shape, in C#. Tap Next: a payment at 12:30 UTC (01:30 in New Zealand's summer), inside the window.
UDC.sln · where these 3 files sit
Controllers/
Filters/
Diagram · step 112:30 UTC: the portal posts a payment. MVC reaches the action filter first.
UDC portalposts payment
Batch filteraction filter
Post actionpayments
POST payments
PaymentsController.cs · line 3 · 1 of 2The attribute puts the filter on this action only. You write [BlockDuringFisBatch]: C# lets you leave off the ending of the class name.
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
Filters are code that MVC runs before and after particular steps of handling a request, so things like auditing, validation or turning exceptions into error responses are written once instead of in every action.
There are five kinds, in this order. Authorization filters first, a check that the request may run. Then resource filters, which can answer from a cache. Then the parameters get filled in, and action filters run just before and after the action. Exception filters handle a throw, and result filters wrap writing the response.
The difference from middleware is scope and knowledge. Middleware runs for every request and sees the HTTP request; after routing it can see which endpoint was chosen, but not the arguments or the result. Filters run only for controller actions and know the action, its arguments and its result. So cross-cutting HTTP things like HTTPS, CORS and authentication go in middleware, and action-aware things go in filters.
You register a filter globally in the controller options, or as an attribute on a controller or an action. One trap I've seen: a filter registered as a single instance is shared by every request, so a field in it got overwritten by parallel requests and the audit log recorded the wrong approver.
Check your answer: tick what you said
You said 0 of 8
Then add your example: UDC Finance payments API (invented example)
Situation: every night between 01:00 and 03:00 New Zealand time, the FIS core runs its , which posts the day's payments to the contracts. Payments that our API posted during that window sometimes landed half-processed, and fixed them by hand the next morning.
What I did: an action filter attribute, [BlockDuringFisBatch], on the four actions that post money. During the window it sets context.Result to 503 Service Unavailable ("try again later"), so the action never runs. Reads stay open all night. UTC is the world clock time with no time zone; the filter converts it to New Zealand time, so daylight saving is handled.
Result: no more half-posted payments, and no manual fixes. The UDC portal shows "payments open again at 3 am" when it gets the 503.
What I'd say: a filter fits because only some actions need it and it must stop the action before it runs: it sets the result, and MVC skips the action.
See it
Middlewareerrors · sign-in · [Authorize]
GET contracts/7
user: priya
Authorization filters1st: may this run?
Resource filters2nd: wrap the rest
Model binding"7" → int id
Action filtersAuditFilter
ActionGet(7)
Exception filtersonly if it throws
Result filtersaround writing JSON
1/9A dealer app asks for contract 7. Middleware runs first, for every request.Inside MVC, authorization filters run first. [Authorize] was already checked by middleware.Resource filters wrap the rest. A cache (saved answers) could reply here and skip it all.Model binding turns "7" from the address into int id = 7.Action filter, before: logs "priya called Get". It can see id = 7.The action runs and returns Ok(contract): a result, not yet written.Back in the action filter, after: it sees the result and could change it.Had the action thrown, exception filters would run now instead.Result filters wrap writing the JSON. Then resource filters and middleware finish.
In plain words
Filter: a class MVC calls at a fixed point while it handles a request for a controller action. It's how you write "do this around every action" once.
Authorization filters run first and decide if the action may run at all. It exists for a custom permission check you write yourself as a filter. The built-in [Authorize] is checked earlier, by the authorization middleware ().
Resource filters run next and wrap everything after them, including model binding. A response cache can answer here and skip the rest.
Model binding (not a filter) turns the address, query string and body into the action's parameters ().
Action filters run just before and just after the action, with its arguments and its result in hand. The most common kind you write: auditing, timing, extra validation.
Exception filters run only when controller creation, model binding, an action filter or the action throws. They can turn the exception into a response, for example a 404 for "contract not found".
Result filters run just before and after the result is written to the response (here: the JSON). If an exception filter produced the response, they don't run.
Short-circuit: a filter can set context.Result itself. MVC then skips the action and the remaining filters of that stage, and sends that result (result filters still run if an action filter set it).
Where you register a filter: globally (options.Filters.Add<T>(), every action), or as an attribute (a label in square brackets above a class or method) on a controller or an action. Here options is the settings object that AddControllers(options => ...) hands you, and <T> means "of this class", for example Add<AuditFilter>(). Global ones run outside controller ones, which run outside action ones.
Middleware vs filters: middleware runs for every request (files, health checks, controllers) and sees the HTTP request; after routing it can see which endpoint was chosen, but not the arguments, model state or result. Filters run only for MVC actions and know the action, its arguments and its result.
Audit record: a line that says who did what and when, kept for BMW FS's auditors. A maker-checker rule means the person who creates a change can't also approve it.
The senior answer · about 2 minutes
What filters are for
Filters are MVC's hooks around an action, for code that every action needs but that isn't its own job: an audit record on every change, timing, a check that a contract belongs to the dealer, or mapping a domain exception to a status code. You write it once and register it globally, per controller or per action.
The order, and what each one sees
Authorization filters run first; in practice [Authorize] is checked even earlier, by the authorization middleware, so I almost never write one. Resource filters wrap everything after them, including model binding, which is why a response cache lives there. Then model binding fills the parameters, and action filters run just before and after the action, with the arguments and the result in hand.
If the action or an action filter throws, exception filters run instead of the normal path. Result filters wrap writing the response, and they're skipped when an exception filter produced it. Any filter can short-circuit by setting context.Result: we block payment posts during the FIS core's nightly batch that way, and the action never runs.
Middleware or filter?
Middleware runs for every request, health checks and static files included, and sees the HTTP context. After routing it can see which endpoint was chosen and its attributes, but not the bound arguments, model state or the action's result. So HTTPS, CORS, authentication, request logging and the global exception handler are middleware.
A filter only runs for controller actions but knows which action it is, its arguments and its result. So auditing a contract change, or turning into a 404, is a filter. One trap: an exception filter doesn't see errors from middleware, resource or result filters or JSON serialization (turning the C# object into JSON text), so the real safety net is with ProblemDetails, and exception filters only map specific domain errors.
What I watch for
How a filter is registered decides its lifetime. Added by type, MVC creates one per request and DI fills its constructor. Added as an instance with , one object serves every request (like a singleton: one object shared by the whole app), so a field in it is shared. That bit us at BMW FS: the approval audit recorded the wrong approver under load, which is the incident in Deep. Our rule now: filters keep no per-request state in fields, and a test fires parallel approvals.
Every filter, in order
The real output of a test app with one filter of each kind logging a line, for a normal call and then for an action that throws.
HTTPall requests
Early1 and 2
Action filter3, 5
Action4
Exc. or result6 or 7
1 authorization, 2 resource
1/6In. Middleware (HTTP), authorization filter (1), then resource filter before (2).Around the action. Binding, action filter before (3), the action (4), action filter after (5).Result. Result filter (7) wraps writing the JSON. Then resource after (8), middleware after.New request: the action throws. 1 to 4 as before; the after-method sees the exception (5).The exception filter runs (6) and sets a 409 result. Result filters are skipped; resource after (8).New request: writing the JSON throws. Too late for exception filters; middleware can catch it.
The story
Before
BMW FS's contracts API has about forty actions that change a contract: approve, fund, change the payment date. (An action is one method of a controller class that answers one kind of request. A controller is a class that groups the actions for one area, like contracts. MVC is the part of ASP.NET Core that sends each request to the right action: .) Every change must leave an audit record, a line saying who did what and when, which auditors check. In the first version every action started with the same few lines that wrote that record, copied by hand.
Approve()audit lines + work
Fund()audit lines + work
Move datework only
1/4Before: each action writes its own audit record, with the same lines copied in.A new action was added without the copy. Its changes left no audit record.Fix: one filter, registered once. MVC runs it around every action.Every action, old or new, now gets the audit record. Nobody has to remember.
Why it had to change
Code that every action needs, but that isn't the action's own job, is called cross-cutting: auditing, timing, turning errors into a standard response. Copied into each action, it gets forgotten or changed in one place and not the others.
How it works now
A filter is a class that MVC calls at a fixed point while it handles a request for a controller action (a public method that answers a request). You write it once and register it for every action, for one controller, or for one action. There are five kinds, and each runs at its own point. Middleware is the similar idea one level out: code that every HTTP request passes through before MVC even starts (,). The main diagram follows one request through all of them.
What we gained
The audit lines live in one class, every action is covered, and the actions only do their own work. The rest of the card shows where each kind of filter runs and when to pick a filter over middleware.
In the lending API
The audit filter: written once, run around every action; the diagram above shows where.
Only the key lines are shown; Deep walks the whole file.
Lending.sln · where this file sits
src/BmwFs.Contracts.Api/Filters/
⋯ 8 lines in Deep⋯ 3 lines in Deep
AuditFilter.cs · line 1 · 1 of 3An action filter: is an with two methods, one MVC calls just before the action, one just after. (The other method, run after the action, is in the full view.)
The code, file by file
The BMW FS contracts API: start-up, a controller, and the audit filter. Tap Next to walk one request.
Lending.sln · where these 3 files sit
src/BmwFs.Contracts.Api/
Controllers/
Filters/
Diagram · step 1A dealer app asks for contract 7. Middleware runs first, for every request.
Middlewareerrors · sign-in · [Authorize]
GET contracts/7
user: priya
Authorization filters1st: may this run?
Resource filters2nd: wrap the rest
Model binding"7" → int id
Action filtersAuditFilter
ActionGet(7)
Exception filtersonly if it throws
Result filtersaround writing JSON
Program.cs · line 3 · 1 of 6Registers MVC. options => { ... }: a small function .NET calls once to fill in MVC's settings.
What they ask next
The follow-ups interviews really ask. Answer out loud, then tap to check.
ReportedWhat is the difference between middleware and filters?
Middleware is part of the ASP.NET Core pipeline: it runs for every request and sees the HTTP request and response; after routing it can see which endpoint was chosen, but not the arguments or the result. Filters are part of MVC: they run only when a controller action handles the request, and they know which action, its arguments, the model state (the result of checking the bound parameters) and the result. Use middleware for things every request needs (HTTPS, CORS, authentication, global error handling, request logging), filters for things that depend on the action (auditing changes, mapping a domain exception, validation rules for some actions).
Middleware: every request
UseExceptionHandler
UseAuthentication
UseAuthorization
MVC (only for controllers):
authorization filters
resource filters
model binding
action filters → action
exception filters (on error)
result filters → JSON
ReportedWhat are the filter types, and in which order do they run?
Authorization, resource, action, exception, result. Authorization first; resource filters wrap model binding and everything after; action filters wrap the action; exception filters only when controller creation, model binding, an action filter or the action throws; result filters wrap writing the response. Old ASP.NET MVC 5 had authentication filters too and no resource filters.
How do you register a filter, and how does it get services from DI?
Globally: options.Filters.Add<AuditFilter>() in . On a controller or action: an attribute, either a class deriving from , or [ServiceFilter(typeof(AuditFilter))] (gets it from DI, so it must be registered) or [TypeFilter(typeof(AuditFilter))] (creates it, filling the constructor from DI). Filters.Add<T>() and [TypeFilter] create a filter per request; [ServiceFilter] uses the lifetime you registered; Filters.Add(new AuditFilter()) adds one shared instance, which must hold no per-request state. Each way side by side: .
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
ReportedWhat is the difference between middleware and filters?
Middleware is part of the ASP.NET Core pipeline: it runs for every request and sees the HTTP request and response; after routing it can see which endpoint was chosen, but not the arguments or the result. Filters are part of MVC: they run only when a controller action handles the request, and they know which action, its arguments, the model state (the result of checking the bound parameters) and the result. Use middleware for things every request needs (HTTPS, CORS, authentication, global error handling, request logging), filters for things that depend on the action (auditing changes, mapping a domain exception, validation rules for some actions).
Middleware: every request
UseExceptionHandler
UseAuthentication
UseAuthorization
MVC (only for controllers):
authorization filters
resource filters
model binding
action filters → action
exception filters (on error)
result filters → JSON
ReportedWhat are the filter types, and in which order do they run?
Authorization, resource, action, exception, result. Authorization first; resource filters wrap model binding and everything after; action filters wrap the action; exception filters only when controller creation, model binding, an action filter or the action throws; result filters wrap writing the response. Old ASP.NET MVC 5 had authentication filters too and no resource filters.
How do you register a filter, and how does it get services from DI?
Globally: options.Filters.Add<AuditFilter>() in . On a controller or action: an attribute, either a class deriving from , or [ServiceFilter(typeof(AuditFilter))] (gets it from DI, so it must be registered) or [TypeFilter(typeof(AuditFilter))] (creates it, filling the constructor from DI). Filters.Add<T>() and [TypeFilter] create a filter per request; [ServiceFilter] uses the lifetime you registered; Filters.Add(new AuditFilter()) adds one shared instance, which must hold no per-request state. Each way side by side: .
How do you stop the action from running in a filter?
Set context.Result in the before-method of an action or resource filter (or in an authorization filter). MVC skips the action and the remaining filters of that stage, and sends that result (result filters still run if an action filter set it). Throwing an exception instead is the wrong tool: it's slower and ends up as a 500.
Exception filter or exception middleware?
For the global safety net, middleware: with ProblemDetails (and an in .NET 8 and later) catches errors from everything after it in the pipeline, as long as the response hasn't started. Exception filters see errors from the action, the action filters, controller creation and model binding, but not from result execution or middleware, so I use one only to map specific domain exceptions, like to 404. The global handler is its own card: .
In which order do global, controller and action filters of the same type run?
On the way in: global, then controller, then action. On the way out: the reverse, like layers of an onion. A filter that implements can set ; lower numbers run first (on the way in), and with equal numbers, global runs first, then controller, then action.
Should a filter implement or ?
Async when the filter awaits something, like a database call. has one method that gets next: code before await next() runs before the action, code after it runs after, and a local variable carries state between them, which also avoids the shared-field bug. Implement one of the two, not both: MVC only calls the async one.
Do minimal APIs have filters?
Yes, endpoint filters since .NET 7, for minimal APIs (endpoints written in Program.cs with no controller): , added with .AddEndpointFilter<T>() on a route. They wrap the handler like an action filter, with its arguments. MVC's five kinds are for controllers only. Worked example: .
Production bugs, and how to tell them
Each one: what you see, why, the fix in code, and the line to say.
A serialization error skipped our exception filter: empty 500s
HTTP/1.1 500 Internal Server Error
Content-Length: 0
System.Text.Json.JsonException: A possible
object cycle was detected.
Cause
The team's turned exceptions into a ProblemDetails body (the standard error body) with a correlation id (an id that ties one request's log lines together). An action returned an EF Core entity (a class mapped to a database table) whose dealer pointed back to its contracts, a loop. The JSON writer threw while writing the response, during the result step. Exception filters only cover controller creation, model binding, action filters and the action, so the filter never saw it: the client got an empty 500 with nothing to quote to support.
Fix
Return a small DTO (a class shaped for the response) instead of the entity, which removes the loop. And move the safety net to middleware: with ProblemDetails catches errors from anywhere in the pipeline. The exception filter stays only for mapping our own domain exceptions. How to write that handler, with a correlation id: .
Diagram · step 1Before: the action returns the entity. No exception yet, so the exception filter isn't called.
MVCaction
Filterours
JSONwriter
Catchouter
Ok(contract entity)
Before · ContractsController.cs · line 5 · 1 of 1Returns the entity itself: dealer -> contracts -> dealer, a loop. Writing it as JSON throws.
Say it
Exception filters only see errors from the controller, binding, action filters and the action. A JSON serialization error happens later, so ours never ran. Global error handling now lives in UseExceptionHandler, and filters only map domain exceptions.
Every call to one controller failed with 500 after a refactor
System.InvalidOperationException: No service
for type 'AuditFilter' has been registered.
Cause
A developer moved from global to one controller with [ServiceFilter(typeof(AuditFilter))] (typeof(X) is the class X itself as a value). asks the DI container for the filter, but nobody registered it there, so every call threw.
Fix
Register it (builder.Services.AddScoped<AuditFilter>()), or use [TypeFilter(typeof(AuditFilter))], which creates it without a registration and only uses DI for its constructor. A test calls one action of every controller at start-up.
Lending.sln · where these 2 files sit
Controllers/
Before · DealersController.cs · line 1 · 1 of 1Gets from DI: it must be registered.
Say it
ServiceFilter gets the filter from the DI container, so the filter itself must be registered. We registered it, and a calls every controller once.
Production incident: The approval audit trail recorded the wrong approver
P2 · serious, has a workaroundBMW FScame with a deployContracts API (ASP.NET Core on App Service)
Composed, not a real event at BMW FS. The mechanism is real ASP.NET Core, checked by a run on .NET 10: a filter added with Filters.Add(new ...) is one object shared by every request, so a field set in its before-method can be overwritten by a parallel request before its after-method reads it. Times, counts, names, ticket and PR numbers are composed.
The story in 5 lines
On Mon 12 Oct BMW FS's monthly report flagged 14 contract approvals where the approver was the same person who created the contract, which the rules forbid.
The credit officers named in the audit trail said they never approved those contracts. request logs showed someone else had made each call.
Release 5.1 had changed how was registered: from by type (one per request) to one shared instance. The filter kept the approver in a field, and parallel approvals overwrote it.
An the same evening registered it by type again. The fix: no fields at all, an async filter with a local variable, a parallel-approvals test, and 45 audit rows rebuilt from request logs.
The rule since: filters keep no per-request state in fields, and the audit trail is compared with request logs every night.
Who's who (5 people)
You
.NET developer on the vendor team (L3). Owns the fix.
Tech lead
Vendor senior. Runs the bridge call, reviews every PR.
L2 pod
Vendor support. Runs queries, deploys with approval.
BMW FS compliance analyst
Owns the maker-checker report and the audit trail.
BMW FS IT change manager
Approves the emergency change.
Tap a step. The map lights the place where it happens.
Credit officersapprove contracts
Contracts APIApprovalAuditFilter
Azure SQLApprovalAudit table
Compliancemaker-checker report
You + tech leadL3: our code
App Insightsrequest logs, KQL
Azure DevOpsPR, tests, release
Staging slotthe next build waits
1/8 Detect
The report: 14 contracts approved by their own maker.
Mon 12 Oct, 10:00 ET, US Eastern (19:30 IST)BMW FS compliance analyst, then the L2 pod
What: the monthly maker-checker report (the person who creates a contract change can't also approve it) flags 14 contracts where the audit trail shows the same person as maker and approver.
How we saw it: the analyst asks two of those credit officers. Both say they never approved those contracts. opens a ticket with the vendor team.
Why it hid: every audit row looked valid: a real officer, a real contract, the right time. Only a rule that compares people noticed anything.
In theory
The audit trail is checked against an independent record of who called the API.
In practice
Nobody compared them. A monthly report found 14 of the 45 wrong rows by luck: the rest named a different, innocent officer.
Try it yourself · 10 minutes, on your Mac
dotnet new webapi -n FilterLab. Write an action filter that stores a request header in a field in and prints it in .
Register it with options.Filters.Add(new AuditFilter()), and add an action that waits 200 ms for one id and 20 ms for another.
Send two requests 50 ms apart with different headers: the slow one prints the fast one's header.
Change to with a local variable and repeat: each prints its own.
You should see
A filter added as an instance is shared by all requests, so a field in it is shared too. Local variables, or a filter created per request, keep each request's data its own.
The senior move
"Filters, like singletons, must not keep per-request data in fields. I use an async filter with locals, and I test audit code with parallel requests."
SourcesSourced: filter instances added with Filters.Add(instance) are reused across requests, while Filters.Add<T>() creates them per request (ASP.NET Core docs, "Filters": filter scopes and dependency injection, ); is a standard control in lending operations. Checked with a real run on .NET 10: two parallel requests through a shared filter instance logged the wrong user; an with a local logged both correctly. Composed: BMW FS's systems, dates, counts, names, ticket and numbers and the .
Traps
Don't sayFilters and middleware are the same thing.SayMiddleware runs for every request and sees only HTTP. Filters run only for controller actions and know the action, its arguments and its result.
Don't sayAn exception filter catches every error in the app.SayOnly errors from controller creation, model binding, action filters and the action. Not middleware, resource or result filters, or writing the response. Use for everything else.
Don't sayStore the start time or the user in a field of the filter.SayA filter added as an instance is shared by all requests. Keep state in local variables () or (a dictionary that lives for one request).
Don't sayTo stop the action, throw from the filter.SaySet context.Result. MVC skips the action and the remaining filters of that stage, and sends that result.
Traps
What should you say instead? Tap to check.
Don't sayFilters and middleware are the same thing.
Say instead
Middleware runs for every request and sees only HTTP. Filters run only for controller actions and know the action, its arguments and its result.
Don't sayAn exception filter catches every error in the app.
Say instead
Only errors from controller creation, model binding, action filters and the action. Not middleware, resource or result filters, or writing the response. Use for everything else.
Don't sayStore the start time or the user in a field of the filter.
Say instead
A filter added as an instance is shared by all requests. Keep state in local variables () or (a dictionary that lives for one request).
Don't sayTo stop the action, throw from the filter.
Say instead
Set context.Result. MVC skips the action and the remaining filters of that stage, and sends that result.
Live coding
Easy first, then harder. Set a timer and type it before you look.
Task 1
Question index: Musteasy · 10 minutes
Write an action filter that logs which user called which action, and register it globally so every controller gets it.
Which interface is called only when the action throws?
How do you check the exception's type and use it in one step?
What must you set so MVC knows the error is dealt with?
Show a solution
Lending.sln · where these 2 files sit
Filters/
Diagram · step 2MVC calls the exception filter with the exception.
ActionGet(88)
Exc. filterNotFoundFilter
Clientdealer app
throw: not found
Program.cs · line 3 · 1 of 1Global: any action that throws this exception gets a 404.
What the interviewer checks
that handles only and leaves other exceptions alone.
with status 404, a title and the detail.
Sets context.Result and context.ExceptionHandled = true.
Knows it misses errors from middleware, result filters and JSON writing; is the safety net.
With AI tools
Interviewers now ask how AI changes this. Assistants write a filter in seconds, and two mistakes show up again and again in what they generate: a field that holds per-request state in a filter added with Filters.Add(new ...), and an exception filter offered as "global error handling". I check every generated filter for how it's registered and what it stores, and keep the global error handling in .