.NET and ASP.NET Core: the path of one request

How the .NET runtime and the ASP.NET Core host start, where settings and secrets come from, why middleware order decides safety, how Minimal APIs route and bind a request with code generated at build time, how a mediator hands it to one handler, and how errors, rate limits, CORS and CSRF are handled - step by step, with pictures.

In short

The {{brand}} server is one ASP.NET Core application on .NET 10. The built-in web server, Kestrel, reads each request and hands it to a pipeline of middleware: small steps that each check or prepare one thing, in a fixed order. At the end, routing picks one small function, a Minimal API endpoint, whose parameter-reading code was generated at build time. It sends one command to one handler, and any exception thrown by a handler becomes the right HTTP status in one central place.

After this lesson you can Explain how an ASP.NET Core app starts and where its settings come from, put middleware in a safe order and say why, build a Minimal API endpoint with a handler and clean error answers, and explain rate limiting, CORS and CSRF in an interview.

Words you need

The path of one request, end to end

Follow one request - a browser saving a note - from the network to the answer. Every ASP.NET Core app has these stages; only the middleware in the middle differs from app to app.

Routing reads the first line, binding reads the JSON body, and middleware reads the headers.

The .NET runtime and the host

The JIT lets the same compiled file run on Windows, Linux and different CPUs, and optimize for the real workload.

async and await are what let one server handle thousands of requests. While a request waits for the database, its thread goes back to the thread pool and serves someone else; the request continues on any free thread when the answer arrives.

Pass the CancellationToken down, so work stops when the client goes away.

Every service in dependency injection has a lifetime: how long one instance lives and who shares it.

Never let a singleton hold a scoped service A singleton that keeps a scoped service (a captive dependency) keeps one request's database context alive for the whole life of the app and shares it between requests. ASP.NET Core in Development refuses this when scope validation is on.

The middleware pipeline, and why order matters

Middleware is a chain of nested calls. Each step does some work, calls next, and runs again when the inner steps return - like the layers of an onion. A step that does not call next short-circuits: the request goes no deeper, and the answer travels back out through the steps it already passed.

Step 2, the exception handler, can only catch what happens inside it, which is why it sits near the outside.

One place for the order Write the order once, in one place, with the reason beside every line. Nobody should have to search the code to learn what runs before an endpoint.

The options in the market

What {{brand}} chose, and why

{{brand}} has well over a thousand endpoints. With Minimal APIs, each route is one tiny class in the folder of its use case, beside its request, command and handler: a vertical slice. The build-time generator keeps start-up fast at that size. A small own mediator gives every use case exactly one handler, with no third-party library at the center of the product.

When another choice is right A team of Java experts, or a company standard on Spring, should not switch for this. A small service with five routes does not need a mediator - call the service. A public API that leans on many filters and validation conventions may be happier with controllers.

Configuration: providers, order and secrets

ASP.NET Core reads settings from a list of configuration providers and merges them into one tree of keys such as Mail:Host. When two providers set the same key, the one added later wins. That one rule explains nearly every surprise with settings.

The options pattern turns a section into a typed class. Instead of reading strings everywhere, a class asks for IOptions<MailOptions> and gets numbers as numbers and defaults in one place.

Check settings at start, not on the first customer's request The built-in way is options validation with ValidateOnStart. {{brand}} uses its own readiness checks: each reads one setting and answers ready, warning or blocker. Outside local development a blocker - a missing signing key, for example - stops the process at once, with a message naming the setting to fix.

A vault provider can reload on a timer, but a value already copied into an IOptions<T> object stays as it was, so a rotated secret usually needs a restart.

Endpoint routing and Minimal APIs

Endpoint routing works in two halves. Early in the pipeline, the matcher picks the endpoint; at the very end, the endpoint runs. Between the two, every middleware can read the chosen endpoint's metadata - its rate-limit policy, its body limit, whether it allows signed-out callers. A WebApplication adds both halves for you, around your own middleware.

Model binding and the request delegate generator

If binding fails, for example a bad GUID or invalid JSON in the body, the framework answers 400 before your function runs.

Something must turn those rules into code that runs on every request: read the route value, parse the GUID, read and deserialize the body, resolve the service, call your function and write the result. That code is the request delegate. By default ASP.NET Core builds it at runtime from expression trees, when the endpoint is first used. The request delegate generator writes the same code at build time as plain C#, and the compiler swaps your MapGet call for it with interceptors.

What the generator changes in practice Cold start. A big API used to build hundreds of delegates during its first requests, and many test hosts starting at once queued for the compiler. Moving that work to build time cut a long test run from tens of minutes to under a minute, and a new deployment's first visitor no longer waits either.

The rules it brings Write the handler where it is mapped - a method group or a lambda - so the generator can see it. A record bound with [AsParameters] that has an optional text or array field must use init properties, not a positional constructor: the generator writes typeof(string?), which C# refuses (error CS8639). Bound properties have no default values, so optional numbers are nullable and the handler applies the default.

Native AOT compiles a whole app to machine code ahead of time: a tiny start-up time and memory use, but no runtime code generation, and only the code a trimmer can prove is used. The generator removes one big obstacle. Native AOT also needs the rest of the app to avoid reflection, and some libraries, such as EF Core, still treat it as experimental. The faster cold start is the part that pays off for most apps today.

The mediator: one command, one handler

In CQRS style, a use case that changes data is a command and one that only reads is a query. Each is a small record that names its result type. A mediator finds the one handler registered for that record's type and calls it. The endpoint depends on the mediator alone, so it never grows a list of services.

Cross-cutting work without behaviors Libraries like MediatR wrap handlers in behaviors for logging or validation. {{brand}}'s mediator has none on purpose: logging, limits and access run in middleware and endpoint metadata, which see every request, whether or not it reaches a handler.

Errors: from exception to status code and problem body

A handler that finds a problem throws a typed exception: not found, conflict, invalid input. Nothing catches it on the way up. Near the top of the pipeline, ASP.NET Core's exception handler middleware calls the registered IExceptionHandler, which maps the type to a status and writes the body. Endpoints therefore contain no try/catch at all. Gates and limiters - sign-in, the cross-site check, the rate limiter, body and time limits - write their own answers early, before any handler runs.

Why 404 for hidden things If a stranger asks for record 42 and gets 403, they have learned that record 42 exists. Answering 404 for both missing and hidden records gives an attacker nothing to count or guess with.

Rate limiting: a budget per caller

A rate limiter gives each caller a budget of requests per time window and answers 429 Too Many Requests with a Retry-After header when it runs out. ASP.NET Core has one built in: a middleware plus named policies, each choosing an algorithm and a partition key - whose budget this request spends.

The counters live in memory A framework's built-in limiter usually counts in the process's own memory. For one strict budget shared by many servers use a shared store such as Redis, or limit at the edge.

CORS and CSRF explained

CORS protects people, not servers CORS is enforced by browsers only. curl, scripts and other servers ignore it entirely. It decides which web pages may read your API's answers with a visitor's cookies; it never replaces authentication, and on its own it does not stop CSRF.

{{brand}} follows the layered approach described above: a hardened session cookie plus a second check on writes. Pick the layers that suit your own app. State-changing work must never hang on a GET, because browsers and crawlers follow links with cookies.

Try it yourself: a tiny API that shows the pipeline

In half an hour you can see every idea of this lesson working, with the free .NET SDK and curl. Watch how the order of two lines changes what you see.

Common mistakes

Interview questions

Related