Action Methods — Return Types, Parameters and HTTP Verbs

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
    }
}
Note: Without [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.
Tip: Always use 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.
Warning: Always add [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.

🧠 Test Yourself

A POST action calls return View(model) when ModelState.IsValid is false. What happens to the validation error messages?