Introduction
In ASP.NET Core MVC, controllers are the traffic cops of your application — they receive HTTP requests, call services or databases, and decide what comes back: an HTML page, JSON, a redirect, or an error. Actions are the individual methods inside a controller (like Index, Create, Delete).
If MVC is the bookstore layout from Article 4, the controller is the assistant who handles each customer request. This lesson teaches every important IActionResult type, action parameters (route, query, form), GET vs POST, a full Student CRUD for ShopNest Academy, and a gentle intro to action filters — exactly what TCS and Infosys interviewers expect from 0–2 year candidates.
After this article you will
- Define controllers and actions with correct naming conventions
- Return View, Json, Redirect, NotFound, and other result types confidently
- Bind route, query, and form parameters to action methods
- Implement List, Details, Create, Edit, Delete for students
- Explain GET vs POST and when to use action filters
Prerequisites
- Article 4 — MVC Architecture
- ShopNest.Web MVC project running
- Basic understanding of HTTP methods
What is a Controller?
Level 1 — Analogy
A controller is like a bank teller window. Customers (browsers) don't enter the vault (database) themselves. They hand a slip (HTTP request) to the teller (controller action). The teller validates the slip, talks to the vault clerk (service layer), and gives back cash or a receipt (HTML/JSON/redirect).
Level 2 — Technical
Controllers inherit from Controller (MVC with views) or ControllerBase (API-only, no view support). They are discovered by convention: class name ends with Controller, lives in Controllers/ folder.
// File: Controllers/StudentsController.cs
public class StudentsController : Controller
{
// Each public method is an "action"
public IActionResult Index() { ... }
}
Controller base class features
View(),Json(),RedirectToAction()helper methodsModelState— validation errors from formsUser,HttpContext,Request,ResponseTempData,ViewData,ViewBagfor passing data to views
IActionResult return types (with examples)
| Return type | When to use | Example |
|---|---|---|
ViewResult / View() | Render Razor HTML | return View(model); |
JsonResult / Json() | Return JSON (AJAX/API-style from MVC) | return Json(students); |
RedirectToActionResult | PRG pattern after POST | return RedirectToAction(nameof(Index)); |
NotFoundResult | 404 — resource missing | return NotFound(); |
BadRequestResult | 400 — invalid input | return BadRequest(ModelState); |
ContentResult | Plain text / custom content | return Content("OK", "text/plain"); |
FileResult | Download file | return File(bytes, "application/pdf", "report.pdf"); |
StatusCodeResult | Custom HTTP status | return StatusCode(429); |
ActionResult vs IActionResult: Use ActionResult<T> in APIs when you want typed response bodies with automatic 400 on invalid model. IActionResult is the non-generic interface — flexible for MVC returning different result types from one action signature pattern.
Common misconceptions
❌ MYTH: Every action must return View().
✅ TRUTH: POST actions typically RedirectToAction after save (Post-Redirect-Get) to prevent duplicate form submissions.
❌ MYTH: Controllers should contain all business logic.
✅ TRUTH: Keep controllers thin — delegate to services; controllers orchestrate HTTP only.
Action parameters — route, query, form
// Route: /Students/Details/42 → id = 42 (conventional routing)
public IActionResult Details(int id) { ... }
// Query: /Students/Search?term=rahul&page=2
public IActionResult Search(string term, int page = 1) { ... }
// Form POST: [FromForm] on complex model
[HttpPost]
public IActionResult Create([FromForm] Student student) { ... }
// Body JSON (more common in API controllers):
[HttpPost]
public IActionResult Create([FromBody] StudentDto dto) { ... }
Model binding maps HTTP data to C# parameters automatically. Use [FromRoute], [FromQuery], [FromForm], [FromBody] when you need to be explicit (Article 10 covers binding in depth).
GET vs POST in controllers
| Method | Safe? | Typical use | Attribute |
|---|---|---|---|
| GET | Yes (read-only) | List, Details, Edit form display | [HttpGet] (default) |
| POST | No (changes state) | Create, Edit save, Delete confirm | [HttpPost] |
Never use GET for delete or payment — browsers and crawlers can trigger GET accidentally. Use POST with anti-forgery token ([ValidateAntiForgeryToken]).
Hands-on: Student CRUD (ShopNest Academy)
Student model
// File: Models/Student.cs
using System.ComponentModel.DataAnnotations;
public class Student
{
public int Id { get; set; }
[Required, StringLength(100)]
public string FullName { get; set; } = string.Empty;
[Required, EmailAddress]
public string Email { get; set; } = string.Empty;
[Range(18, 60)]
public int Age { get; set; }
public string Course { get; set; } = "ASP.NET Core";
}
In-memory service (EF Core in Article 13)
public interface IStudentService
{
IReadOnlyList<Student> GetAll();
Student? GetById(int id);
void Add(Student student);
void Update(Student student);
bool Delete(int id);
}
public class StudentService : IStudentService
{
private static readonly List<Student> _students = new()
{
new() { Id = 1, FullName = "Rahul Sharma", Email = "rahul@example.com", Age = 22, Course = "Full Stack .NET" },
new() { Id = 2, FullName = "Priya Patel", Email = "priya@example.com", Age = 21, Course = "Cloud Azure" }
};
private static int _nextId = 3;
public IReadOnlyList<Student> GetAll() => _students;
public Student? GetById(int id) => _students.FirstOrDefault(s => s.Id == id);
public void Add(Student s) { s.Id = _nextId++; _students.Add(s); }
public void Update(Student s)
{
var i = _students.FindIndex(x => x.Id == s.Id);
if (i >= 0) _students[i] = s;
}
public bool Delete(int id)
{
var s = GetById(id);
if (s is null) return false;
_students.Remove(s);
return true;
}
}
StudentsController — full CRUD
// File: Controllers/StudentsController.cs
public class StudentsController : Controller
{
private readonly IStudentService _students;
public StudentsController(IStudentService students) => _students = students;
// LIST — GET /Students
public IActionResult Index() => View(_students.GetAll());
// DETAILS — GET /Students/Details/5
public IActionResult Details(int id)
{
var student = _students.GetById(id);
return student is null ? NotFound() : View(student);
}
// CREATE (form) — GET /Students/Create
[HttpGet]
public IActionResult Create() => View(new Student());
// CREATE (save) — POST /Students/Create
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(Student student)
{
if (!ModelState.IsValid) return View(student);
_students.Add(student);
return RedirectToAction(nameof(Index));
}
// EDIT — GET /Students/Edit/5
[HttpGet]
public IActionResult Edit(int id)
{
var student = _students.GetById(id);
return student is null ? NotFound() : View(student);
}
// EDIT — POST /Students/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(int id, Student student)
{
if (id != student.Id) return BadRequest();
if (!ModelState.IsValid) return View(student);
_students.Update(student);
return RedirectToAction(nameof(Details), new { id = student.Id });
}
// DELETE — GET confirmation
[HttpGet]
public IActionResult Delete(int id)
{
var student = _students.GetById(id);
return student is null ? NotFound() : View(student);
}
// DELETE — POST confirm
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public IActionResult DeleteConfirmed(int id)
{
if (!_students.Delete(id)) return NotFound();
return RedirectToAction(nameof(Index));
}
}
Register in Program.cs: builder.Services.AddSingleton<IStudentService, StudentService>();
Create view snippet (form with Tag Helpers)
@model Student
<form asp-action="Create" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="mb-3">
<label asp-for="FullName" class="form-label"></label>
<input asp-for="FullName" class="form-control" />
<span asp-validation-for="FullName" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Save student</button>
</form>
Action filters — intro
Filters run before or after actions — logging, caching, authorization. Example: log every student admin action.
public class LogActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
var name = context.ActionDescriptor.DisplayName;
Console.WriteLine($"Executing: {name}");
}
public void OnActionExecuted(ActionExecutedContext context) { }
}
// Register globally in Program.cs:
builder.Services.AddControllersWithViews(options =>
options.Filters.Add<LogActionFilter>());
Full filter pipeline (authorization, resource, exception) is covered in Article 26.
Interview questions
Q1: What is the difference between Controller and ControllerBase?
A: Controller adds view-related helpers; ControllerBase is lighter for APIs.
Q2: What is IActionResult?
A: Interface representing an HTTP response — View, Json, Redirect, etc.
Q3: Why RedirectToAction after POST?
A: Post-Redirect-Get prevents duplicate submissions on browser refresh.
Q4: What is ModelState?
A: Dictionary of validation errors from Data Annotations and model binding.
Q5: GET vs POST?
A: GET reads; POST creates/updates — POST is not idempotent or cacheable.
Q6: Purpose of [ValidateAntiForgeryToken]?
A: CSRF protection — validates hidden token matches cookie.
Summary
- Controllers handle HTTP; actions are public methods returning IActionResult
- Use View, Json, Redirect, NotFound, BadRequest appropriately
- Bind route/query/form parameters; validate with ModelState
- Student CRUD demonstrates full MVC lifecycle on ShopNest
- Filters cross-cut logging and auth around actions
Previous: MVC Architecture
Next: Routing in ASP.NET Core
FAQ
How many actions should one controller have?
Group by feature — StudentsController with Index, Details, Create, Edit, Delete is standard. Split if a controller exceeds ~10 actions or mixed concerns.
Can one action return both View and Json?
Same method can return different IActionResult types based on conditions — e.g. Json for AJAX requests and View for full page loads — but separate actions are cleaner when possible.