Forms, Tag Helpers and Antiforgery

Tag helpers that bind inputs to a model, what arrives on post, where validation messages come from, and the antiforgery token.

Syntax<form asp-action="Register" method="post"> <input asp-for="Email" /> </form>

A tag helper is an attribute on an HTML element that the server understands. It looks like markup and runs as code while the view renders. The form tag helpers exist so that one C# class describes a form's fields and the HTML is generated from it.

A bound form

<input asp-for="Email" /> writes name="Email", id="Email", the current value, a type chosen from the property's type and attributes, and data-val-* attributes describing the rules. The longhand is the plain HTML it produces: <input type="email" id="Email" name="Email" value="@Model.Email" />. Same element, fewer places to mistype a name. <label asp-for="Email"></label> takes its text from [Display(Name = ...)], and <form asp-controller="Account" asp-action="Register" method="post"> works out the action URL from the route table.

On post

Because each input's name matches a property name, model binding fills a fresh instance of the class from the posted fields. The validation attributes then run and their results land in ModelState. Check ModelState.IsValid. If it is false, return the same view with the same model, so the typed values and the messages come back with status 200. <span asp-validation-for="Email"></span> shows one field's message and <div asp-validation-summary="ModelOnly"></div> shows the errors that belong to no single field.

Antiforgery

The form tag helper also writes a hidden __RequestVerificationToken input, paired with a cookie. Without that pair, another site could post to yours using your visitor's signed-in cookies. That attack is cross-site request forgery. Razor Pages checks the token for you. An MVC controller does not: add [ValidateAntiForgeryToken] to the POST action, or [AutoValidateAntiforgeryToken] to the whole class, and a request without a valid token is rejected with status 400.

Example

Example · csharp
@* Views/Account/Register.cshtml *@
@model RegisterForm

<h1>Register</h1>

@* method="post" with no action attribute, so the tag helper adds the
   hidden __RequestVerificationToken input for us. *@
<form asp-controller="Account" asp-action="Register" method="post">

    @* Errors that belong to the form as a whole. *@
    <div asp-validation-summary="ModelOnly"></div>

    <label asp-for="Email"></label>
    <input asp-for="Email" />
    <span asp-validation-for="Email"></span>

    <label asp-for="Password"></label>
    <input asp-for="Password" />
    <span asp-validation-for="Password"></span>

    <button type="submit">Create account</button>
</form>

@*
The Email line renders roughly this, with the rules taken from the
attributes on the RegisterForm class:
<label for="Email">Email address</label>
<input type="email" data-val="true"
       data-val-required="The Email address field is required."
       data-val-email="The Email address field is not a valid e-mail address."
       id="Email" name="Email" value="" />
<span class="field-validation-valid" data-valmsg-for="Email"
      data-valmsg-replace="true"></span>
*@

When to use it

  • Build a sign-up or settings form whose fields come from one C# class.
  • Redisplay a rejected form with the visitor's text still in the boxes and a message beside the bad field.
  • Stop another site from posting to your form endpoints using a signed-in visitor's cookies.

More examples

The class the form is built from

One class drives the input types, the labels, the browser-side rules and the server-side check, so the two sides cannot drift apart. Keep a form class separate from a database entity: the form is only the fields this one page accepts.

Example · csharp
// Models/RegisterForm.cs
using System.ComponentModel.DataAnnotations;

namespace Shop.Models;

public class RegisterForm
{
    // [EmailAddress] gives the input type="email" and a client-side rule.
    // [Display] supplies the label text.
    [Required]
    [EmailAddress]
    [Display(Name = "Email address")]
    public string Email { get; set; } = "";

    // DataType.Password gives the input type="password", so the value is
    // never written back into the HTML.
    [Required]
    [StringLength(100, MinimumLength = 12)]
    [DataType(DataType.Password)]
    public string Password { get; set; } = "";
}

The controller on both verbs

This GET-renders, POST-validates, redirect-on-success shape is the whole pattern for a server-rendered form. Validating on the server is not optional: the data-val attributes only help browsers that run the client-side script, and anyone can post to the URL directly.

Example · csharp
using Microsoft.AspNetCore.Mvc;
using Shop.Models;

public class AccountController : Controller
{
    // GET /Account/Register renders the empty form.
    [HttpGet]
    public IActionResult Register() => View(new RegisterForm());

    // POST /Account/Register. Without this attribute the token is
    // written into the form but never checked.
    [HttpPost]
    [ValidateAntiForgeryToken]
    public IActionResult Register(RegisterForm form)
    {
        if (!ModelState.IsValid)
        {
            // Same view, same object: values and messages are redisplayed.
            return View(form);
        }

        // Save the account here, then redirect so a refresh does not
        // post the form a second time.
        return RedirectToAction(nameof(HomeController.Index), "Home");
    }
}

The same form written out by hand

Worth reading once, because it shows there is no magic: tag helpers produce exactly this. The hand-written version loses the generated data-val rules, and a typo in a name attribute stops that field binding without any error, which is why the asp-for form is preferred.

Example · cshtml
@model Shop.Models.RegisterForm

@* No tag helpers. Every name and URL is typed out, and the token is
   added explicitly. The same elements as the short version, minus the
   data-val-* attributes that asp-for generates from the annotations. *@
<form action="/Account/Register" method="post">
    @Html.AntiForgeryToken()

    <label for="Email">Email address</label>
    <input type="email" id="Email" name="Email" value="@Model.Email" />

    <label for="Password">Password</label>
    <input type="password" id="Password" name="Password" />

    <button type="submit">Create account</button>
</form>

Discussion

  • Be the first to comment on this lesson.