MVC Controllers and Views
|
This section documents ASP.NET Core on .NET 10 (LTS), the current release — the minimal hosting model, the middleware pipeline, dependency injection, Minimal APIs, MVC & Razor Pages, Blazor with the current render modes, SignalR and gRPC, EF Core, ASP.NET Core Identity and policy-based authorization, output caching, rate limiting, and Native-AOT-aware building — as described by the official documentation at Microsoft Learn, which is the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. .NET ships a major release every November and its APIs continue to evolve: the examples here target .NET 10 / C# 14. This section’s bibliography lists the reference material consulted while preparing these pages. |
MVC separates a request handler (the controller), the data it builds (the model), and how that data is rendered (the view). Views are written with Razor syntax.
Registering MVC
builder.Services.AddControllersWithViews(); // MVC with Razor views
// builder.Services.AddControllers(); // API controllers only, no views -- see
// xref:web/aspnet/core/web-api-controllers.adoc[Web API Controllers]
// builder.Services.AddRazorPages(); // Razor Pages -- see
// xref:web/aspnet/core/razor-pages.adoc[Razor Pages]
var app = builder.Build();
app.MapControllers(); // attribute-routed actions
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}"); // conventional route
Controllers and action results
Derive from Controller for view support (ControllerBase is enough for pure APIs — see
Web API Controllers). Action methods return IActionResult:
public sealed class ProductsController(IProductService products) : Controller
{
[HttpGet]
public async Task<IActionResult> Index()
=> View(await products.ListAsync()); // renders Views/Products/Index.cshtml
[HttpGet("products/{id:int}")]
public async Task<IActionResult> Details(int id)
{
var p = await products.FindAsync(id);
return p is null ? NotFound() : View(p);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(ProductInput input)
{
if (!ModelState.IsValid) return View(input);
await products.AddAsync(input);
return RedirectToAction(nameof(Index));
}
}
Common results: View, PartialView, Ok, NotFound, BadRequest, RedirectToAction, Redirect, File,
Json, Content. See
Handle requests with controllers and
Model Binding and Validation for how input above gets
populated and validated.
Conventional vs. attribute routing
| Style | Notes |
|---|---|
Conventional ( |
One or a few route templates cover many controllers/actions by
convention ( |
Attribute ( |
Each action declares its own template; needed for REST-shaped URLs, and required once any action on a controller uses attribute routing. |
[Route("catalog/[controller]")] // [controller] token expands to "Products"
public sealed class ProductsController : Controller
{
[HttpGet("{id:int}")] // -> catalog/Products/{id}
public IActionResult Details(int id) => ...;
}
A controller mixes styles only by accident — pick one per controller. See Routing to controller actions and Routing for the underlying endpoint-routing model.
ViewData, ViewBag, and TempData
| Mechanism | Notes |
|---|---|
|
A |
|
The same dictionary, exposed as |
|
Survives exactly one subsequent request (backed by session or cookies) — the standard place for a post/redirect/get success message. |
public IActionResult Create(ProductInput input)
{
// ...
TempData["Message"] = "Product created.";
return RedirectToAction(nameof(Index));
}
@if (TempData["Message"] is string msg)
{
<div class="alert alert-success">@msg</div>
}
Session state
Beyond TempData’s one-redirect scope, `ISession gives a controller a server-side, per-user dictionary keyed
by a session cookie — useful for small amounts of state that must survive across several requests (a shopping
cart id, a multi-step wizard’s progress) without round-tripping through the client on every request.
| Mechanism | Notes |
|---|---|
|
Server-side per-user dictionary keyed by a cookie; backed by
|
Cookies |
|
Query string / route / hidden field |
Stateless; preferred where practical over any server- or cookie-held state. |
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(o => o.IdleTimeout = TimeSpan.FromMinutes(20));
// ...
app.UseSession();
public IActionResult AddToCart(int productId)
{
var cart = HttpContext.Session.GetString("Cart") ?? "";
HttpContext.Session.SetString("Cart", cart + $",{productId}");
return RedirectToAction(nameof(Index));
}
ISession values are byte[] at the storage layer — use the GetString/SetString/GetInt32/SetInt32
extension methods for simple values, or serialize your own type. See
App state (session and app state) in
ASP.NET Core.
See Pass data to views.
Areas
Areas partition a large MVC app into feature folders, each with its own controllers, views, and (optionally) route prefix:
Areas/
Admin/
Controllers/UsersController.cs
Views/Users/Index.cshtml
[Area("Admin")]
public sealed class UsersController : Controller { }
app.MapControllerRoute("admin", "admin/{controller=Home}/{action=Index}/{id?}",
defaults: new { area = "Admin" });
View discovery
For View() with no name, MVC looks for Views/{Controller}/{Action}.cshtml, then
Views/Shared/{Action}.cshtml; areas prepend Areas/{Area}/Views/… to that search. Passing an explicit
name (View("SomeView")) or a ~/-rooted path (View("~/Views/Custom/Special.cshtml")) overrides discovery.
Razor class libraries (RCLs) can contribute views that participate in the same search, ahead of the app’s own
Views/Shared. See Views in ASP.NET Core MVC.