ASP.NET Core MVC (and Razor Pages)
In short: Model–View–Controller separates an app into three parts.
- The Model holds data, business rules and validation (entities, view models).
- The View renders UI from a model (Razor
.cshtml). - The Controller receives the request, works with services and models, and returns a result (view, redirect, JSON).
- Benefits: separation of concerns, testable controllers, parallel work on UI and logic, and clean URLs through routing.
In short: Kestrel receives the request, which flows through the middleware pipeline (exception handling, HTTPS, static files, routing, authentication, authorisation).
- Endpoint routing selects the controller action.
- The controller is created via DI, then the filter pipeline runs: authorisation filters → resource filters → model binding and validation → action filters → the action executes → result filters → result execution (the view engine renders the view with its layout and partials).
- Exception filters handle errors in that stage, and the response goes back out through the middleware.
In short: ViewData is a string-keyed dictionary and ViewBag is a dynamic wrapper over the same data.
- Both live only for the current request (controller → view) and need no casting with ViewBag.
TempDatapersists until it is read, typically across one redirect (PRG flash messages); it's stored in a cookie by default or in session.Keep()/Peek()retain values.- Prefer strongly typed view models over all three for real data.
In short: Conventional routing defines URL templates centrally ({controller=Home}/{action=Index}/{id?}) and is good for uniform MVC apps.
- Attribute routing puts templates on controllers/actions (
[Route("products/{id:int}")]), giving precise control, constraints and versioning; - it's required for
[ApiController]. - Both can coexist.
- Attribute routes are matched without the conventional patterns.
In short: The process of mapping HTTP request data (form fields, route values, query string, headers, body, files) to action parameters and model properties by name.
- Attributes
[FromForm],[FromQuery],[FromRoute],[FromHeader],[FromBody]and[FromServices]control the source. - After binding, validation runs and fills
ModelState.
In short: Data annotations ([Required], [StringLength], [Range], [EmailAddress], [RegularExpression], [Compare]) on models are checked after model binding, and the results go into ModelState.
- Check
ModelState.IsValidin the action and redisplay the view with errors. - Tag helpers emit
data-val-*attributes, so jQuery Unobtrusive Validation validates on the client too. - Custom rules use a
ValidationAttribute,IValidatableObjector FluentValidation. - Server-side validation is mandatory because the client side can be bypassed.
_Layout, _ViewStart and _ViewImports?In short: _Layout.cshtml is the master template (header, nav, footer) with @RenderBody() and optional @RenderSection.
_ViewStart.cshtmlruns before every view in its folder and usually setsLayout = "_Layout"._ViewImports.cshtmlholds shared directives —@using,@addTagHelper,@inject— for all views in that folder hierarchy.
In short: Use a partial view to reuse markup when the parent already has the data (e.g. a product card in a list).
- Use a view component when the widget needs its own logic and data from services, independent of the page's model (shopping cart badge, navigation menu, "latest posts").
- View components replace MVC 5 child actions and can be invoked as tag helpers (
<vc:cart />).
In short: Filters run code at specific stages of the MVC action pipeline: Authorization → Resource → (model binding) → Action → action → Action (after) → Result → result execution → Result (after).
- Exception filters catch unhandled exceptions from these stages.
- They can be applied globally, per controller or per action (in that order on the way in), and use DI via
ServiceFilter/TypeFilter. - Use cases: auth, logging, caching, validation, error handling.
In short: Middleware is part of the ASP.NET Core pipeline.
- It runs for every request, including static files and non-MVC endpoints, works with
HttpContextonly, and its order is set in Program.cs. - Filters are part of MVC.
- They run only for controller actions and Razor Pages, and can see MVC context such as
ModelState, action arguments andIActionResult. - Use middleware for cross-cutting HTTP concerns (logging, CORS, exception handling) and filters for action-specific concerns (validation, audit per action).
In short: Tag helpers are server-side C# classes that enhance HTML-looking elements via asp-* attributes (<input asp-for="Name" />, <a asp-action="Details" asp-route-id="5">).
- Compared with HTML helpers (
@Html.TextBoxFor), they look like plain HTML, give better IntelliSense, are friendlier to designers, and add features automatically (antiforgery tokens in forms, cache-busting withasp-append-version). - Custom tag helpers derive from
TagHelper.
In short: Use antiforgery tokens.
- The form tag helper injects a hidden
__RequestVerificationTokenfield paired with a cookie. - Validate with
[ValidateAntiForgeryToken]per action or globally withAutoValidateAntiforgeryTokenAttribute. - For AJAX, send the token in a header (
RequestVerificationToken) configured viaAddAntiforgery(o => o.HeaderName = ...). SameSitecookies add defence in depth.- APIs using JWT in the Authorization header aren't vulnerable in the same way.
RedirectToAction, Redirect and LocalRedirect?In short: All send a 302 by default (*Permanent variants send 301/308).
RedirectToAction("Details", new { id })builds the URL from routing.Redirect(url)goes to any URL, including external ones, which is risky with user input (open redirect).LocalRedirect(url)only allows local URLs and throws otherwise, so use it forreturnUrlafter login.
In short: After a successful POST, the server responds with a redirect (302/303) to a GET page instead of rendering a view directly.
- Refreshing the browser then repeats the harmless GET instead of resubmitting the form, which prevents duplicate orders or payments.
TempDatacarries the success message across the redirect.
In short: A way to partition a large MVC app into functional sections (Admin, Billing, Customer), each with its own Controllers, Views and Models folders.
- Controllers are marked
[Area("Admin")]and routes include{area:exists}. - Tag helpers use
asp-area.
In short: In production, app.UseExceptionHandler("/Home/Error") (shows a friendly error page, logs the exception) plus app.UseStatusCodePagesWithReExecute("/Error/{0}") for 404s.
- In development, the developer exception page is enabled by default.
- Exception filters handle MVC-specific cases. .NET 8 adds
IExceptionHandlerimplementations registered withAddExceptionHandler<T>(). - Always log with a correlation ID and never show stack traces to users.
In short: MVC apps usually use cookie authentication: after login (Identity or OpenID Connect via Entra ID), an encrypted auth cookie identifies the user on each request, and antiforgery tokens protect forms.
- Web APIs consumed by SPAs or mobile apps usually use JWT bearer tokens in the
Authorizationheader, validated statelessly (issuer, audience, signature, expiry). - In both, authorisation uses
[Authorize]with roles, claims or policies. - For SPAs, a BFF (Backend-for-Frontend) that keeps tokens server-side and uses cookies is now recommended for security.
In short: Attribute-based roles can't express "owner of this specific document".
- Load the resource, then call
IAuthorizationService.AuthorizeAsync(User, document, "EditPolicy"). - Write an
AuthorizationHandler<SameOwnerRequirement, Document>that succeeds whendocument.OwnerId == user idor the user is an admin. - Return
Forbid()on failure. - This also prevents IDOR attacks.
In short: Inject dependencies through interfaces and mock them (Moq/NSubstitute).
- Instantiate the controller, call the action, and assert on the
IActionResulttype and model: `var result = Assert.IsType(await controller.Details(5)); - Assert.IsType
(result.Model);`. - Set
controller.ControllerContextwith a fakeHttpContext/Userwhen needed. - For routing, filters and views end to end, use integration tests with
WebApplicationFactory.
In short: Defence in layers: (1) disable the button on submit with JavaScript;
- (2) use Post-Redirect-Get;
- (3) make the server idempotent — generate an order token (GUID) when rendering the form, store it in a hidden field, and enforce a unique constraint on it in the DB, so a second POST with the same token is rejected or returns the existing order.
- The DB unique constraint is the only fully reliable guard.
In short: It's an IDOR (Insecure Direct Object Reference) / broken access control.
- The action loads by ID without checking ownership.
- Fix: always scope queries by the current user (
db.Orders.Where(o => o.Id == id && o.CustomerId == userId)), or use resource-based authorisation. - Return 404 (not 403) to avoid revealing existence.
- Audit other endpoints, add integration tests for access control, and consider EF Core global query filters for multi-tenant data.
In short: That's an N+1 query problem: views or lazy loading trigger a query per item.
- Fix: load all needed data in the controller/service with one or a few queries using
Includeor, better, projections (Selectinto a view model). - Disable lazy loading.
- Cache rarely changing data (categories, menus) with
IMemoryCache/HybridCacheor the<cache>tag helper. - Add output caching for anonymous pages.
- Measure again with logging of EF queries or MiniProfiler.
In short: Use cache busting.
asp-append-version="true"on<script>,<link>and<img>adds a hash query string that changes when the file changes.- In .NET 9+,
MapStaticAssets()with.WithStaticAssets()fingerprints and compresses assets at build time with long-lived caching headers. - Bundlers (Vite/webpack) produce hashed file names.