All articles

ASP.NET Core MVC Request Life Cycle Explained

An HTTP request in an ASP.NET Core MVC app on .NET 9 flows through the Kestrel web server into an ordered middleware pipeline, gets matched to a controller…

0 · log in to like, save & follow Share on LinkedIn Share on X

An HTTP request in an ASP.NET Core MVC app on .NET 9 flows through the Kestrel web server into an ordered middleware pipeline, gets matched to a controller action by endpoint routing, is bound and validated into method parameters, runs through action filters, executes the action, produces a result (a rendered view or JSON), and then travels back out through the same middleware in reverse. Understanding this order is what lets you place authentication, error handling, and logging correctly. In this article you will trace every stage and see the exact Program.cs ordering that makes it work.

ASP.NET Core MVC Request Life Cycle Explained

Where does the request start?

Every request first reaches Kestrel, the cross-platform web server built into ASP.NET Core. Kestrel (often sitting behind a reverse proxy such as Nginx or IIS) parses the raw HTTP bytes into an HttpContext and hands that context to the application. From this point on, everything is your code: a chain of middleware components that each get a chance to inspect, short-circuit, or modify the request and response.

This is the first big departure from the old ASP.NET MVC 5 pipeline. There is no System.Web, no Global.asax, no IHttpModule or IHttpHandler. In MVC 5 a request moved through nearly twenty HttpApplication events — BeginRequest, AuthenticateRequest, ResolveRequestCache, and so on — and you extended it by writing modules that subscribed to those events. Ordering was implicit and hard to reason about. In .NET 9 all of those event-based extension points collapse into one linear concept you configure by hand: middleware. What you gain is an explicit, top-to-bottom order you can read straight off Program.cs, and a server (Kestrel) that runs the same way on Windows, Linux, and macOS instead of being tied to IIS.

How the middleware pipeline works

The pipeline is an ordered list of components. Each middleware receives the HttpContext and a next delegate. It can run code before calling next (on the way in), call the next component, then run code after next returns (on the way out). This "Russian doll" nesting is why a request enters top-to-bottom and the response leaves bottom-to-top.

Order is not cosmetic — it is behavior. If you put static file handling after authentication, you force every image through the auth check. If you put exception handling last, it can't catch errors from the components above it. A typical .NET 9 order looks like this:

  1. Exception handling / developer exception page — outermost, so it wraps everything below.
  2. HTTPS redirection and HSTS.
  3. Static files — short-circuits for CSS, JS, and images before routing runs.
  4. Routing — decides which endpoint matches.
  5. Authentication then authorization — identify the user, then check permissions.
  6. Endpoint execution — runs the matched controller action.

Ordered middleware pipeline in Program.cs on .NET 9

Writing custom middleware

You can insert your own component anywhere in the chain. A conventional middleware is a small class with an InvokeAsync method. This one times each request and adds the elapsed milliseconds to a response header:

public class RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
{
    public async Task InvokeAsync(HttpContext context)
    {
        var start = Stopwatch.GetTimestamp();
        await next(context);   // hand off to the rest of the pipeline
        var elapsed = Stopwatch.GetElapsedTime(start);
        logger.LogInformation("{Method} {Path} took {Ms} ms",
            context.Request.Method, context.Request.Path, elapsed.TotalMilliseconds);
    }
}

The await next(context) call is the hinge: code before it runs on the way in, code after it runs on the way out. Register it with app.UseMiddleware<RequestTimingMiddleware>() early in the pipeline so it wraps everything you want to measure. If a middleware chooses not to call next — for example, a caching layer that already has the answer, or an auth gate that rejects the request — the pipeline short-circuits and the response starts unwinding immediately without ever reaching the endpoint. That short-circuiting ability is exactly why order matters: whichever component runs first gets the first chance to answer or reject.

From routing to a controller action

When execution reaches app.MapControllers() (or the classic {controller}/{action}/{id} convention set up with MapControllerRoute), endpoint routing has already run and selected an endpoint. Routing is split into two phases: the routing middleware matches the URL to an endpoint and stores it on the context, and the endpoint middleware at the end actually invokes it. Everything in between — auth, custom middleware — sees which endpoint was chosen and can act on its metadata.

Once the action is selected, the MVC framework takes over with model binding. It maps route values, query string, form fields, headers, and the JSON body onto your action's parameters, converting strings to the target types using the registered value providers and model binders. A parameter like int id is pulled from the route, a complex [FromBody] OrderDto order is deserialized from JSON with System.Text.Json, and you can steer the source explicitly with attributes such as [FromQuery] or [FromForm]. Validation runs immediately after binding, evaluating data annotations such as [Required] and [Range] and populating ModelState. You inspect ModelState.IsValid inside the action, or let [ApiController] return a 400 automatically before the action ever runs.

Where do action filters run?

Filters are MVC's own pipeline that runs inside endpoint execution, and they fire in a defined sequence around the action:

  • Authorization filters run first and can short-circuit the whole thing.
  • Resource filters wrap model binding — useful for caching or short-circuiting before binding.
  • Action filters run immediately before and after the action method.
  • Exception filters handle unhandled exceptions from the action or later filters.
  • Result filters run around the execution of the result (for example, before and after the view renders).

Filters are the right place for cross-cutting concerns that need MVC context — logging the action name, validating ModelState in one spot, wrapping the action in a transaction, or shaping a consistent error response. You register them globally, per-controller with an attribute, or per-action. A minimal action filter that logs before and after execution:

public class LogActionFilter(ILogger<LogActionFilter> logger) : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext ctx) =>
        logger.LogInformation("Executing {Action}", ctx.ActionDescriptor.DisplayName);

    public void OnActionExecuted(ActionExecutedContext ctx) =>
        logger.LogInformation("Executed with {Result}", ctx.Result?.GetType().Name);
}

Executing the action and rendering the result

The action method runs and returns an IActionResult — commonly View(model), Json(data), Ok(), or NotFound(). The result is not the response itself; it is a description of what to produce. MVC then executes it: a ViewResult invokes the Razor view engine, which finds the .cshtml file, renders it with your model, and writes HTML to the response stream. A JsonResult serializes the object with System.Text.Json and sets the content type.

After the result executes, the response unwinds back up the pipeline. Result filters, then the endpoint middleware, then each outer middleware get their turn on the way out — this is where your timing middleware records the elapsed time and where response compression or security headers get applied. Because middleware is nested, the outermost component (your exception handler) is also the last to see the response, which is why it can catch and reshape errors thrown anywhere below it. Finally Kestrel writes the status line, headers, and body bytes back over the connection to the client, and the HttpContext is disposed.

The whole journey is deterministic: given the same registration order, a request always visits the same components in the same sequence in and the reverse sequence out. That predictability is the practical payoff of the .NET 9 model — you reason about the request life cycle by reading Program.cs top to bottom rather than tracing a web of framework events.

Key takeaways

  • Kestrel receives the request and builds the HttpContext; your app is a middleware pipeline over it.
  • Middleware order defines behavior: exception handling outermost, then HTTPS, static files, routing, auth, endpoints.
  • Endpoint routing matches the URL to a controller action before the endpoint middleware executes it.
  • Model binding fills action parameters and validation populates ModelState before the action runs.
  • Filters (authorization, resource, action, exception, result) run inside endpoint execution in that order.
  • The response travels back out through the pipeline in reverse — the "on the way out" half of each middleware.

Frequently asked questions

How is the .NET 9 MVC pipeline different from ASP.NET MVC 5?

MVC 5 ran on System.Web with IHttpModule/IHttpHandler and Global.asax events. .NET 9 replaces all of that with a single ordered middleware pipeline configured in Program.cs, hosted by Kestrel instead of IIS-only.

Does middleware order really matter?

Yes. Middleware runs in the exact order you register it. Placing authentication before routing, or static files after auth, changes which requests are checked and what gets served — order is functional, not stylistic.

What is the difference between middleware and action filters?

Middleware runs for every request across the whole app. Filters run only for MVC endpoints, inside endpoint execution, with access to model binding, ModelState, and the action result.

When does model validation happen?

Validation runs right after model binding and before the action executes. With [ApiController], an invalid ModelState returns a 400 automatically; in MVC controllers you check ModelState.IsValid yourself.

Enjoyed this article? Get the best GeeksArray articles in your inbox — once a week, no spam, unsubscribe anytime.

Comments (0)

Log in to join the conversation.

No comments yet — be the first to share your thoughts.