GET and POST Actions

A search form sends a GET request with a query string; a form that saves data sends a POST. Pair a GET action that shows the form with a POST action that saves it, redirect afterward, and protect it with an anti-forgery token.

Journeyman · ASP.NET Core · Razor · C# · Updated

After this lesson you can

  • Write a search form that sends a GET request, and the action that answers it
  • Write a GET action that shows a form and a POST action that saves it, then redirects
  • Protect a POST form with an anti-forgery token

Key takeaways

  • Use GET for searches and filters, and POST for anything that changes data, because a GET must be safe to repeat.
  • A search form sends a GET: its inputs' names and values become the query string, which fills the action's parameters by name.
  • A form that saves needs two actions with the same name: an [HttpGet] one shows the form, and an [HttpPost] one saves it.
  • After a successful POST, return a redirect, not a view, so Refresh doesn't send the POST again. That's Post-Redirect-Get.
  • Put [ValidateAntiForgeryToken] on every POST action, and write POST forms with the form tag helper so the token is there.

So far, every request in NorthwindWeb has been the browser asking for a page. Forms let the user send something: a search, or a new product. How the form sends it depends on what it's for.

Two methods

Every request has an HTTP method. Two matter for forms:

GET POST
Means "Give me something" "Here's something to save"
The form's values travel In the address, as a query string In the request's body, out of sight
Can be bookmarked or shared Yes No
Use it for Searches and filters Adding, changing or deleting data

The rule: if the request changes data, use POST. A GET must be safe to repeat, because browsers and search engines repeat them freely.

A search form: GET

A search changes nothing, so it's a GET. Add a Search action to ProductsController. (ProductDao already has Search(string text), from Example: a Product DAO.)

// GET /Products/Search?name=sauce
public IActionResult Search(string? name)
{
    ViewData["SearchText"] = name;
    List<Product> products = string.IsNullOrWhiteSpace(name) ? new List<Product>() : productDao.Search(name);
    return View(products);
}

And its view, Views/Products/Search.cshtml:

@model List<Product>
@{
    ViewData["Title"] = "Search";
    string? searchText = ViewData["SearchText"] as string;
}

<h1>Search Products</h1>

<form method="get" asp-action="Search">
    <input type="search" name="name" value="@searchText" placeholder="Part of a name">
    <button type="submit">Search</button>
</form>

@if (!string.IsNullOrWhiteSpace(searchText))
{
    <p>@Model.Count products match "@searchText".</p>
    <ul>
        @foreach (Product product in Model)
        {
            <li><a asp-action="Details" asp-route-id="@product.ProductId">@product.ProductName</a></li>
        }
    </ul>
}

Type sauce and click Search. The browser goes to /Products/Search?name=sauce: the input's name became the query string's name, which fills the action's name parameter. The page shows:

<p>2 products match "sauce".</p>
<ul>
    <li><a href="/Products/Details/65">Louisiana Fiery Hot Pepper Sauce</a></li>
    <li><a href="/Products/Details/8">Northwoods Cranberry Sauce</a></li>
</ul>

The search text goes back into the box (value="@searchText"), and the address can be bookmarked or shared.

A form that saves: GET, then POST

Adding a product takes two actions with the same name: one shows the empty form, the other receives it. The attributes [HttpGet] and [HttpPost] tell ASP.NET Core which is which. (ProductDao already has Add(Product product). Add a GetCategories() method, and a Category model with CategoryId and CategoryName.)

// GET /Products/Create: shows the empty form
[HttpGet]
public IActionResult Create()
{
    ViewData["Categories"] = productDao.GetCategories();
    return View(new Product());
}

// POST /Products/Create: saves the form, then shows the new product
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(Product product)
{
    int newId = productDao.Add(product);
    return RedirectToAction("Details", new { id = newId });
}
  1. The user goes to /Products/Create: a GET, so the first action shows the form.
  2. They fill it in and click the button. The form has method="post", so the browser sends a POST to the same address, and the second action receives the product. (How the form's fields become a Product is the next lesson.)
  3. The action saves it, and redirects to the new product's details page.

Why redirect: Post-Redirect-Get

After a successful POST, don't return a view. Return a redirect. The browser then makes a fresh GET for the details page, and that GET is what's on screen.

Without it, the page on screen would be the POST's answer, and pressing Refresh would send the POST again: a second, identical product. With the redirect, Refresh just reloads the details page. This habit is called Post-Redirect-Get.

RedirectToAction("Details", new { id = newId }) builds the address the same way the tag helpers do: /Products/Details/78, for example.

The anti-forgery token

A POST changes data, so you want to be sure it came from your form, not from another website that tricks a signed-in user's browser into posting to yours. That trick is called cross-site request forgery, and ASP.NET Core blocks it with an anti-forgery token:

  • A form tag helper with method="post" adds a hidden field with a secret value to the form:

    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8ABPDCrs9LdN..." />
    
  • [ValidateAntiForgeryToken] on the POST action checks that the value came back. If it's missing or wrong, the action never runs, and the browser gets 400 Bad Request.

Put [ValidateAntiForgeryToken] on every POST action, and write POST forms with the form tag helper (asp-action), so the token is always there.

Check yourself

3 questions to try on the lesson page.

So what

GET to ask, POST to change; a GET action to show a form and a POST action to save it; a redirect after saving, and a token to prove the form is yours. Every form in every MVC app you build follows this shape.

Key terms

Cheat sheets