Roadmap › ASP.NET Core MVC (and Razor Pages) › Module: MVC Basics
Lessons in this course (15)
2. How ASP.NET Core MVC works inside — from Kestrel to HTML
Intermediate★ Must knowLesson 2 of 15
Phase 1 — Startup (once):
WebApplication.CreateBuildercreates the generic host: loads configuration (appsettings.json → appsettings.{Env}.json → user secrets → environment variables → command line), sets up logging, creates theIServiceCollectionand configures Kestrel as theIServer.- You register services;
builder.Build()creates the rootIServiceProvider. Use...calls compose the middleware pipeline into a singleRequestDelegate(each middleware wraps the next).MapControllerRoute/MapControllersregisters endpoint data sources: at startup MVC discovers controllers (public classes ending inControllerin the app's assemblies — the application parts), builds anActionDescriptorfor every action (routes, HTTP methods, filters, parameters) and turns them into endpoints.app.Run()starts hosted services and Kestrel, which listens on the configured ports.
Phase 2 — Each request:
text
Kestrel (socket → HTTP parser) ──► HttpContext created (Request, Response, Features, RequestServices = new DI SCOPE)
│
▼ Middleware pipeline (RequestDelegate chain)
ExceptionHandler ─► HSTS/HttpsRedirection ─► Static assets? (serve & short-circuit)
─► UseRouting: EndpointRoutingMiddleware matches URL + HTTP method against all endpoints
(a compiled DFA matcher), stores the chosen Endpoint + route values in HttpContext
─► UseAuthentication: reads cookie/JWT → HttpContext.User
─► UseAuthorization: reads [Authorize] metadata from the selected endpoint → allow / 401 / 403
─► Endpoint middleware: invokes the endpoint's RequestDelegate
│
▼ MVC: ControllerActionInvoker (ResourceInvoker) — the filter pipeline
1. Authorization filters
2. Resource filters (before) ← e.g. caching can short-circuit here
3. Controller created by the controller activator (constructor dependencies resolved from the request scope)
4. Model binding: value providers (route, query, form, headers, body via input formatters e.g. System.Text.Json)
+ model binders → action parameters; then validation (DataAnnotations/IValidatableObject) → ModelState
5. Action filters (OnActionExecuting)
6. ACTION METHOD executes → returns IActionResult (ViewResult, JsonResult, RedirectResult...)
7. Action filters (OnActionExecuted)
8. Exception filters (if the action/filters threw)
9. Result filters (OnResultExecuting)
10. Result execution:
ViewResult → IViewEngine (RazorViewEngine) finds the compiled view → creates ViewContext
→ runs _ViewStart → view's ExecuteAsync writes HTML (tag helpers run, partials/view components render)
→ layout renders with @RenderBody → HTML written to the response body
ObjectResult/JsonResult → output formatter (System.Text.Json) serialises to the body
11. Result filters (OnResultExecuted), Resource filters (after)
◄─ response flows back through middleware (they can still act after next()) ◄─
Kestrel writes the response bytes; the request's DI scope is disposed (scoped services like DbContext disposed)Key internals to remember:
| Piece | What it really is |
|---|---|
HttpContext | Per-request object: Request, Response, User, Items, Features, RequestServices |
| Middleware | Func<RequestDelegate, RequestDelegate> — each calls next(context) or short-circuits |
| Endpoint | A RequestDelegate + metadata (route pattern, [Authorize], CORS policy, action descriptor) |
| Routing | Two steps: match in UseRouting, execute in the endpoint middleware — middleware between them (auth, CORS) can read the selected endpoint |
| Controller lifetime | A new controller instance per request (transient), created from the request's DI scope |
| Scoped services | One instance per request (DbContext) — disposed at the end of the request |
| Model binding | Value providers + model binder providers ([FromRoute], [FromQuery], [FromForm], [FromBody], [FromServices], [FromHeader]) |
| Razor views | Compiled at build time by the Razor SDK into classes deriving from RazorPage<TModel> inside the app assembly (runtime compilation only if you add the package for development) |
| Tag helpers | Classes executed while the view renders, turning <input asp-for> into HTML |
| Output | IActionResult.ExecuteResultAsync writes to HttpContext.Response.Body (a pipe to Kestrel) |
Writing your own middleware shows the pipeline idea:
csharp
app.Use(async (context, next) =>
{
var sw = Stopwatch.StartNew(); // runs on the way IN
await next(context); // everything after this middleware runs here
sw.Stop(); // runs on the way OUT
app.Logger.LogInformation("{Path} took {Ms} ms → {Status}", context.Request.Path, sw.ElapsedMilliseconds, context.Response.StatusCode);
});Interview tip: Asked "explain the request life cycle", answer in this order: Kestrel → HttpContext + DI scope → middleware (exception, HTTPS, static files, routing, authentication, authorization) → endpoint → MVC filters (authorization, resource, model binding + validation, action, exception, result) → action → result execution (view engine / formatter) → back out through middleware → scope disposed.