Learn what a controller is, how action methods handle requests, how parameters arrive from the URL, what the Controller base class gives you, and how to keep controllers small and focused.

Bishal Biswas
Self Learner
What a controller does
A controller is a C# class that receives requests and decides what to send back. It is the “C” in MVC and the traffic officer of your app.
Each public method in a controller is an action. One action handles one kind of request: show the product list, show one product, save a new product, and so on.
using Microsoft.AspNetCore.Mvc;
namespace ShopApp.Controllers;
public class ProductsController : Controller
{
public IActionResult Index()
{
return View();
}
}
Three things make this a controller:
- It lives in the
Controllersfolder (a convention, not a requirement). - Its name ends with
Controller. - It inherits from
Controller, which gives it a toolbox of helper methods.
Visiting /Products or /Products/Index runs the Index action.
The naming rule
The Controller suffix is required and it is dropped from URLs. That gives you this mapping:
ProductsController handles /Products/...
OrdersController handles /Orders/...
AccountController handles /Account/...
Name controllers after the thing they manage, usually as a plural noun. A controller called DoStuffController is a sign that something is off.
Actions and parameters
An action can take parameters. The framework fills them in from the request, a feature called model binding. With the default route, the third part of the URL becomes a parameter named id:
// GET /Products/Details/5 -> id = 5
public IActionResult Details(int id)
{
return Content($"You asked for product {id}");
}
Values from the query string bind by name too:
// GET /Products/Search?term=mouse&maxPrice=50
public IActionResult Search(string term, decimal maxPrice)
{
return Content($"Searching for {term} under {maxPrice}");
}
The parameter names must match the names in the URL. Matching is case insensitive, so ?Term=mouse also works.
If a value is missing, the parameter gets its default: 0 for int, null for string. Make a parameter nullable, such as int? page, when missing is a normal case.
Return types
Almost every action returns IActionResult. It is an interface that means “some kind of response”. View(), Content(), RedirectToAction() and NotFound() all return something that implements it, so one action can choose between them:
public IActionResult Details(int id)
{
var product = FindProduct(id);
if (product is null)
{
return NotFound();
}
return View(product);
}
The next topic goes through every result type. For now, remember View() for pages and IActionResult as the return type.
Which methods become actions
Every public method in a controller is an action, even if you did not mean it to be. A helper method like this would be reachable at /Products/FindProduct:
public Product? FindProduct(int id) { /* ... */ }
Fix it by making the method private, or by marking it with [NonAction]:
[NonAction]
public Product? FindProduct(int id) { /* ... */ }
Private is the better choice in almost every case.
What the Controller base class gives you
Inheriting from Controller gives you helper methods to build responses:
View() // render a view
View("Custom") // render a differently named view
View(model) // render a view with data
RedirectToAction("Index") // go to another action
RedirectToAction("Details", new { id = 5 })
Redirect("https://example.com") // go to any URL
Content("plain text") // send text
Json(new { ok = true }) // send JSON
File(bytes, "application/pdf") // send a file
NotFound() // 404
BadRequest() // 400
StatusCode(500) // any status code
It also gives you properties that describe the current request:
Request // the incoming request: headers, query string, cookies
Response // the outgoing response
HttpContext // everything about the current request and response
User // who is logged in
ModelState // validation results for bound parameters
ViewData // a dictionary passed to the view
ViewBag // the same dictionary with a friendlier syntax
TempData // data that survives one redirect
Url // builds URLs to actions
You will use each of these in later topics. ViewData, ViewBag and TempData are covered when you learn to pass data to views.
Controller versus ControllerBase
There are two base classes:
ControllerBasehas the request handling basics, and it is what Web API controllers use.Controllerinherits fromControllerBaseand adds view support:View(),PartialView(),ViewData,ViewBagandTempData.
For MVC websites, always inherit from Controller.
Controllers get their tools through the constructor
Actions often need something: a database, a logger, an email sender. You do not create these inside the controller. You ask for them in the constructor and the framework hands them over. This is dependency injection:
public class ProductsController : Controller
{
private readonly ILogger<ProductsController> _logger;
public ProductsController(ILogger<ProductsController> logger)
{
_logger = logger;
}
public IActionResult Index()
{
_logger.LogInformation("Product list requested");
return View();
}
}
The ILogger service is registered by default. Your own services get registered in Program.cs, and there is a full topic on how that works.
A new controller is created for every request
The framework creates a fresh instance of your controller for each request and throws it away afterwards. That has a consequence: fields on a controller do not remember anything between requests.
public class CounterController : Controller
{
private int _count = 0;
// Always shows 1. Every request gets a brand new controller.
public IActionResult Index() => Content($"Count: {++_count}");
}
Anything that must survive between requests belongs in a database, a cache, or session state, all of which come later.
Keep controllers thin
A good action reads like a short to do list: get input, ask a service to do the work, pick a response.
public IActionResult Index()
{
var products = _productService.GetAll();
return View(products);
}
Business rules, database queries and calculations belong in other classes. A controller that is hundreds of lines long is hard to read and hard to test. Keep it as the traffic officer, not the whole city.
Quick recap
- A controller is a class ending in
Controllerthat inherits fromController. Its public methods are actions. - The suffix is dropped in URLs:
ProductsControllerhandles/Products/.... - Action parameters are filled from the URL and query string by name. Missing values get defaults, so use nullable types when a value is optional.
- Return
IActionResultand pick a result with helpers such asView(),RedirectToAction()andNotFound(). - Mark helper methods
privateor[NonAction]so they do not become reachable URLs. - Controllers get their dependencies through the constructor, and a new controller instance is created for every request.
// Controllers/ProductsController.cs
// A complete controller with several actions and a private helper.
using Microsoft.AspNetCore.Mvc;
using ShopApp.Models;
namespace ShopApp.Controllers;
public class ProductsController : Controller
{
// Stand in for a database until we add EF Core later in the course.
// 'static' so the list is shared, because a new controller is
// created for every request.
private static readonly List<Product> Products = new()
{
new Product { Id = 1, Name = "Keyboard", Price = 49.99m },
new Product { Id = 2, Name = "Mouse", Price = 19.99m },
new Product { Id = 3, Name = "Monitor", Price = 199.99m }
};
private readonly ILogger<ProductsController> _logger;
// The framework supplies the logger. This is dependency injection.
public ProductsController(ILogger<ProductsController> logger)
{
_logger = logger;
}
// GET /Products or /Products/Index
public IActionResult Index()
{
_logger.LogInformation("Listing {Count} products", Products.Count);
return View(Products); // Views/Products/Index.cshtml
}
// GET /Products/Details/2 (the route's {id?} segment)
public IActionResult Details(int id)
{
Product? product = FindProduct(id);
if (product is null)
{
return NotFound(); // 404
}
return View(product); // Views/Products/Details.cshtml
}
// GET /Products/Search?term=mo&maxPrice=100 (query string values)
public IActionResult Search(string? term, decimal? maxPrice)
{
IEnumerable<Product> results = Products;
if (!string.IsNullOrWhiteSpace(term))
{
results = results.Where(p =>
p.Name.Contains(term, StringComparison.OrdinalIgnoreCase));
}
if (maxPrice is not null)
{
results = results.Where(p => p.Price <= maxPrice);
}
return View("Index", results.ToList()); // reuse the Index view
}
// GET /Products/Cheapest
public IActionResult Cheapest()
{
Product cheapest = Products.MinBy(p => p.Price)!;
// Send the visitor to the Details page of that product
return RedirectToAction("Details", new { id = cheapest.Id });
}
// Private, so it is NOT reachable as /Products/FindProduct
private static Product? FindProduct(int id)
{
return Products.FirstOrDefault(p => p.Id == id);
}
}
// Models/Product.cs
namespace ShopApp.Models;
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public decimal Price { get; set; }
}
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready