# 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 November 3, 2026

## 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*.)

```csharp
// 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`:

```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:

```html
<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`.)

```csharp
// 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:

  ```html
  <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

- [Form](https://www.ask-codey.com/glossary/form)
- [HTTP Method](https://www.ask-codey.com/glossary/http-method)
- [GET](https://www.ask-codey.com/glossary/get)
- [POST](https://www.ask-codey.com/glossary/post)
- [Post-Redirect-Get](https://www.ask-codey.com/glossary/post-redirect-get)
- Anti-Forgery Token

## Cheat sheets

- [GET and POST Actions](https://www.ask-codey.com/cheat-sheets/mvc-forms-get-and-post-actions-cheat-sheet)

Source: https://www.ask-codey.com/lessons/mvc-forms-get-and-post-actions
