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 (MapControllerRoute)

One or a few route templates cover many controllers/actions by convention ({controller}/{action}/\{id?}); simplest for classic "pages per controller" sites.

Attribute ([Route], [HttpGet("…​")])

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

ViewData["Key"]

A string-keyed dictionary, one request/response, object values require casting.

ViewBag.Key

The same dictionary, exposed as dynamic properties — syntactic sugar over ViewData.

TempData

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

ISession (AddSession + app.UseSession())

Server-side per-user dictionary keyed by a cookie; backed by IDistributedCache (in-memory by default, Redis/SQL Server for a farm).

Cookies

Response.Cookies.Append(…​) for small, client-visible state the browser should see and send back.

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.

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.

The MVC request flow

flowchart LR REQ([Request]) --> RT[Routing] RT --> CTL[Controller action] CTL --> MB[Model binding + validation] MB --> LOGIC[Action logic builds the model] LOGIC --> VR[View result] VR --> RAZOR[Razor view engine + layout] RAZOR --> RESP([HTML response])