Action methods are the public methods on a controller that handle HTTP requests. By convention, any public method on a controller class is an action method. An action method receives input through model binding (route values, query strings, form fields), performs work (calls services, validates data), and returns an IActionResult that tells ASP.NET Core what response to generate. Understanding the full vocabulary of action method signatures and return types lets you handle every request scenario cleanly.
Action Method Signatures
public class PostsController(IPostService service) : Controller
{
// ── GET — no parameters ────────────────────────────────────────────────
// GET /posts
public async Task<IActionResult> Index()
{
var posts = await service.GetAllAsync();
return View(posts);
}
// ── GET — route parameter ──────────────────────────────────────────────
// GET /posts/details/42
public async Task<IActionResult> Details(int id)
{
var post = await service.GetByIdAsync(id);
return post is null ? NotFound() : View(post);
}
// ── GET — optional query string parameters ─────────────────────────────
// GET /posts?page=2&pageSize=10&search=asp.net
public async Task<IActionResult> Search(
int page = 1,
int pageSize = 10,
string search = "")
{
var results = await service.SearchAsync(search, page, pageSize);
return View(results);
}
// ── GET — display the create form ─────────────────────────────────────
[HttpGet]
public IActionResult Create()
=> View(new CreatePostViewModel());
// ── POST — handle form submission ─────────────────────────────────────
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(CreatePostViewModel model)
{
if (!ModelState.IsValid)
return View(model); // re-display form with validation errors
await service.CreateAsync(model.ToRequest());
TempData["Success"] = "Post created successfully!";
return RedirectToAction(nameof(Index));
}
// ── POST — delete with PRG pattern ────────────────────────────────────
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id)
{
await service.DeleteAsync(id);
TempData["Success"] = "Post deleted.";
return RedirectToAction(nameof(Index));
}
// ── Action returning JSON (for AJAX) ──────────────────────────────────
[HttpGet]
public async Task<IActionResult> GetTags(string prefix)
{
var tags = await service.SearchTagsAsync(prefix);
return Json(tags); // returns application/json
}
}
[HttpGet] or [HttpPost] attributes, an action method responds to any HTTP verb. This is fine for simple GET-only actions but can cause issues if a POST request accidentally triggers a GET action. Best practice: use [HttpGet] explicitly for read-only actions and always use [HttpPost] for state-changing actions. For actions that have both GET (show form) and POST (process form) variants with the same name, both [HttpGet] and [HttpPost] attributes are needed to distinguish them — the same name can exist twice as long as the HTTP verb differs.nameof() in RedirectToAction() calls: RedirectToAction(nameof(Index)) instead of RedirectToAction("Index"). Using nameof() means if you rename the action method, the compiler catches the broken redirect as an error at build time rather than a 404 at runtime. The string “Index” would silently become a broken redirect after an action rename; nameof(Index) becomes a compile error immediately.[ValidateAntiForgeryToken] to every POST action method that processes form submissions. Without it, your form endpoints are vulnerable to CSRF attacks. Alternatively, add the AutoValidateAntiforgeryTokenAttribute as a global filter in Program.cs to protect all POST/PUT/DELETE actions automatically (with an [IgnoreAntiforgeryToken] opt-out for any action that genuinely should not require it, like webhook endpoints).Action Method Return Types
| Return Type | Use When | Example |
|---|---|---|
| IActionResult | Multiple possible result types | View() or NotFound() |
| ViewResult | Always returns a view | return View(vm); |
| Task<IActionResult> | Async with multiple results | await service.Get() |
| string | Simple text response | return “hello”; |
| void / Task | No response body (rare) | Background fire-and-forget |
Common Mistakes
Mistake 1 — Two actions with the same name and no HTTP verb attribute (ambiguity)
❌ Wrong — framework cannot determine which Create() to call:
public IActionResult Create() { } // GET — no attribute
public IActionResult Create(Model m) { } // POST — no attribute — ambiguous!
✅ Correct — annotate both: [HttpGet] on the first, [HttpPost] on the second.
Mistake 2 — Not re-displaying the form when ModelState is invalid
❌ Wrong — redirecting away on validation failure loses ModelState and error messages:
if (!ModelState.IsValid) return RedirectToAction(nameof(Create)); // errors lost!
✅ Correct — return View(model) to re-render the form with ModelState errors inline.