Controllers and Actions

This section documents ASP.NET MVC 5.3.x, ASP.NET Web API 2.2, ASP.NET Web Pages 3, OWIN/Katana, SignalR 2, and ASP.NET Identity 2 — all running on .NET Framework 4.8.1 — the System.Web-hosted MVC framework, its routing, Razor views, HTML helpers, model binding, filters, and the OWIN-based authentication/Identity stack — as described by the official documentation at Microsoft Learn (plus Web API, Web Pages, SignalR, and Identity), which are the reference these pages are written and verified against.

This is not ASP.NET Core MVC. ASP.NET MVC 5 is the pre-Core, System.Web-hosted MVC framework; it is functionally frozen and receives only security fixes. For the current, cross-platform MVC framework see MVC Controllers and Views under ASP.NET Core (Blazor).

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

This page documents ASP.NET MVC 5.3.x on .NET Framework 4.8.1 — not ASP.NET Core MVC, whose equivalent types are Controller/ControllerBase returning IActionResult.

Controller and ControllerBase

System.Web.Mvc.ControllerBase is the abstract root implementing IController; System.Web.Mvc.Controller extends it with everything action methods actually use — ActionResult-returning helper methods (View, PartialView, Json, Redirect, …​), ViewBag/ViewData/TempData, and access to the current HttpContextBase.

public class ProductsController : Controller
{
    private readonly IProductRepository _repository;

    public ProductsController(IProductRepository repository) => _repository = repository;

    public ActionResult Index() => View(_repository.GetAll());
}

Public methods on a Controller are actions by default unless marked [NonAction]; the constructor above takes a dependency that a custom IControllerFactory must supply (see The MVC Pattern and Request Life Cycle).

Action selectors

[HttpGet]
public ActionResult Edit(int id) => View(_repository.Find(id));

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(Product model)
{
    if (!ModelState.IsValid) return View(model);
    _repository.Update(model);
    return RedirectToAction("Index");
}

[ActionName("Delete")]                 // exposed as /Products/Delete, method name stays DeleteConfirmed
[HttpPost]
public ActionResult DeleteConfirmed(int id) { _repository.Remove(id); return RedirectToAction("Index"); }

[NonAction]
public Product LoadForEditing(int id) => _repository.Find(id);   // never routable

[AcceptVerbs(HttpVerbs.Get | HttpVerbs.Head)]
public ActionResult Details(int id) => View(_repository.Find(id));

[HttpGet]/[HttpPost]/[HttpPut]/[HttpDelete] implement ActionMethodSelectorAttribute; [ActionName] decouples the routable action name from the CLR method name (commonly used for Edit/Delete GET-vs-POST overload pairs); [NonAction] removes a public method from consideration entirely; [AcceptVerbs] predates the per-verb attributes and accepts a bitwise combination of HttpVerbs.

Parameters and defaults

Action parameters are populated by model binding from route data, the query string, and posted form values (in that precedence — see Model Binding and Validation); a C# default parameter value is used when no matching value is found:

public ActionResult Index(int page = 1, string sort = "name") => View(_repository.Page(page, sort));

The ActionResult family

Every built-in result derives from the abstract ActionResult and implements ExecuteResult(ControllerContext):

Result Returned by / purpose

ViewResult

View() — renders a .cshtml view through the view engine.

PartialViewResult

PartialView() — renders a partial view with no layout.

JsonResult

Json(data, JsonRequestBehavior.AllowGet) — serializes to JSON; GET requests are blocked by default (JsonRequestBehavior.DenyGet) to prevent JSON hijacking via <script src>.

RedirectResult

Redirect(url) — an HTTP 302 to a literal URL.

RedirectToRouteResult

RedirectToAction(…​) / RedirectToRoute(…​) — a 302 built from route values.

ContentResult

Content(text, contentType) — raw text/HTML/XML with an explicit content type.

FileResult

File(bytes/stream/path, contentType) — a file download (FileContentResult, FileStreamResult, FilePathResult).

HttpStatusCodeResult

new HttpStatusCodeResult(403) — an arbitrary status code with no body.

HttpNotFoundResult

HttpNotFound() — shorthand for a 404.

EmptyResult

The default when an action returns void; writes nothing.

public ActionResult Summary(int id)
{
    var product = _repository.Find(id);
    if (product == null) return HttpNotFound();
    return Json(new { product.Id, product.Name, product.Price }, JsonRequestBehavior.AllowGet);
}

Custom results are straightforward — subclass ActionResult and override ExecuteResult (a CSV export, for example); see The MVC Pattern and Request Life Cycle for where ExecuteResult sits in the pipeline.

ViewBag vs. ViewData vs. TempData

  • ViewData — a ViewDataDictionary (string-keyed, object values) passed from controller to view for the current request only; requires casting on read.

  • ViewBag — a dynamic wrapper over the same ViewData dictionary; no casting, but no compile-time checking either.

  • TempData — a TempDataDictionary that survives exactly one subsequent request (the classic redirect-then-display-a-message pattern after a POST). It is backed by an ITempDataProvider — SessionStateTempDataProvider by default, i.e. Session under the hood — and each key is marked for removal the moment it is read ("one-read" semantics); Keep()/Peek() opt out of that removal when a value must survive an extra hop.

public ActionResult Create(Product model)
{
    _repository.Add(model);
    TempData["Message"] = "Product created.";      // survives the following redirect
    return RedirectToAction("Index");
}

public ActionResult Index()
{
    ViewBag.Message = TempData["Message"];          // reading here removes it from TempData
    return View(_repository.GetAll());
}

Async actions

Action methods can be async Task<ActionResult>; AsyncControllerActionInvoker (the default since MVC 4) handles both synchronous and asynchronous actions on the same Controller base class — there is no separate AsyncController requirement in MVC 5:

public async Task<ActionResult> Details(int id)
{
    var product = await _repository.FindAsync(id);
    return product == null ? (ActionResult)HttpNotFound() : View(product);
}

See Caching and Performance for why "async all the way" matters under IIS’s thread pool.

Request/Response/Server/User via HttpContextBase

ControllerBase exposes the current request through HttpContextBase (the testable abstraction over System.Web.HttpContext, introduced specifically so controllers can be unit tested without an ASP.NET runtime — see Testing and Diagnostics):

public ActionResult WhoAmI()
{
    string ip = Request.UserHostAddress;          // HttpRequestBase
    bool authenticated = User.Identity.IsAuthenticated;  // IPrincipal
    string root = Server.MapPath("~/App_Data");    // HttpServerUtilityBase
    Response.Cache.SetCacheability(HttpCacheability.NoCache);
    return Content(ip);
}