All articles

ASP.NET Core MVC Request Life Cycle Explained

Follow one request through Kestrel, middleware, routing, filters, and result execution in ASP.NET Core MVC, with a logging middleware that makes the pipeline visible.

0 · log in to like, save & follow Share on LinkedIn Share on X
ASP.NET Core MVC Request Life Cycle Explained

When you type a URL and hit enter, a lot happens between Kestrel accepting the connection and Razor flushing HTML back to the browser. Understanding that journey — the ASP.NET Core MVC request life cycle — is what turns "it works" into "I know why it works", and it is the fastest way to debug middleware ordering issues, filters that never fire, or routes that match the wrong action.

ASP.NET Core MVC Request Life Cycle Explained

This walkthrough follows one request end to end through a .NET 10 MVC application, with a logging middleware that makes the pipeline visible. All code is verified against the companion repository.

The big picture

Every request flows through the same stages, in the same order:

  1. Kestrel accepts the HTTP connection and builds an HttpContext.
  2. The middleware pipeline runs in registration order — each component can act before and after the rest of the pipeline.
  3. Routing (UseRouting) matches the URL to an endpoint — for MVC, that's a controller action.
  4. Filters wrap the action: authorization → resource → model binding → action filters → the action itself → result filters.
  5. The action executes and returns an IActionResult.
  6. The result executes — for ViewResult, Razor renders the view into the response body.
  7. The response unwinds back through the middleware in reverse order.

The key mental model: middleware is an onion, and MVC (endpoints + filters + action) is the core of that onion.

Making the pipeline visible

The easiest way to internalize the flow is to log it. This middleware wraps everything after it, so it sees the request on the way in and the finished response on the way out:

public class RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
{
    public async Task InvokeAsync(HttpContext context)
    {
        var sw = System.Diagnostics.Stopwatch.StartNew();
        logger.LogInformation("--> {Method} {Path} entering MVC pipeline", context.Request.Method, context.Request.Path);
        await next(context);
        sw.Stop();
        logger.LogInformation("<-- {Method} {Path} responded {Status} in {Ms} ms",
            context.Request.Method, context.Request.Path, context.Response.StatusCode, sw.ElapsedMilliseconds);
    }
}

Register it in Program.cs — the position matters. Anything registered before it will not be logged; anything after it will:

var app = builder.Build();

app.UseHttpsRedirection();
app.UseRouting();

app.UseMiddleware<RequestTimingMiddleware>();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

A request to /Products/Boom (an action that throws, wrapped by an exception filter) produces this log — note how the middleware brackets everything, and the filters run inside:

--> GET /Products/Boom entering MVC pipeline
[audit] executing ProductsController.Boom args=
[audit] executed ProductsController.Boom result=(null)
[exception-filter] Demo failure inside an action.
<-- GET /Products/Boom responded 500 in 0 ms

Stage by stage

1. Kestrel and HttpContext

Kestrel parses the raw HTTP request into an HttpContext — request headers, body stream, connection info, and an empty response. Everything downstream reads and mutates this one object. There is no HttpContext.Current in ASP.NET Core; the context is passed explicitly, which is why it is safe under async.

2. The middleware pipeline

Each Use... call adds a component. Order is the number one source of subtle bugs:

  • UseHttpsRedirection before everything that generates URLs.
  • UseStaticFiles (or MapStaticAssets in .NET 9+) early, so images and CSS never touch MVC.
  • UseRouting before anything that needs to know the matched endpoint.
  • UseAuthentication / UseAuthorization between routing and the endpoint execution.

A component can short-circuit the pipeline by not calling next(context) — that is exactly how the static files middleware serves a CSS file without MVC ever running.

3. Routing

UseRouting performs URL matching and stores the chosen endpoint on the context; the terminal Map... call executes it. The conventional route pattern {controller=Home}/{action=Index}/{id?} means /Products/Detail/2 resolves to ProductsController.Detail(2). Attribute routes ([HttpGet("/products/{id:int}")]) take precedence over conventional routes and are matched in the same phase.

4. Filters

Filters run inside the action invocation, in a fixed order:

Filter stage Runs Typical use
Authorization filters first [Authorize]
Resource filters around model binding caching, short-circuiting
Action filters around the action logging, validation
Exception filters on unhandled exception error shaping
Result filters around result execution response headers

Model binding happens between resource filters and action filters: route values, query string, and form fields are converted into your action's parameters, and validation attributes populate ModelState.

5. Action and result execution

The action returns an IActionResult — a description of the response, not the response itself. View() returns a ViewResult; nothing has rendered yet. Result execution is the step that runs Razor, serializes JSON, or writes the redirect header. This split is what makes result filters and unit testing practical: you can assert on the returned ViewResult.Model without rendering HTML.

6. The response unwinds

After the result executes, control returns up the middleware chain in reverse. That is where our timing middleware logs the status code and elapsed time — and where response-compression or exception-handling middleware do their after-the-fact work.

Why this matters in practice

  • A filter "not firing" usually means the request short-circuited earlier — a 304 from a resource filter, a redirect from middleware, or a static file.
  • UseAuthorization before UseRouting throws at startup in modern templates, but custom pipelines that manipulate endpoints manually can still get the order wrong.
  • Middleware cannot see MVC's model state or action results — if you need those, you want a filter, not middleware. Conversely, filters cannot see requests that never reach MVC.
  • Performance work starts with the pipeline: the earlier you can answer a request (static files, output caching, rate limiting), the less you pay.

Summary

One request, one HttpContext, one trip through the onion: middleware in order, routing picks the endpoint, filters wrap the action, the action describes the response, result execution writes it, and the response unwinds back out. Keep the logging middleware from this post in your toolbox — dropping it into an unfamiliar codebase and reading the log is the quickest way to see what that app's pipeline actually does.

The complete working project is in the companion repository, including the audit and exception filters that appear in the log output above.

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.