Data Annotations and ModelState
Put validation rules on the model with data annotations such as [Required], [StringLength] and [Range]; check ModelState.IsValid in the action; add your own errors with AddModelError; and show the messages with validation tag helpers.
Journeyman · ASP.NET Core · C# · Updated
After this lesson you can
- Add validation rules to a model with data annotations, with their own error messages
- Check ModelState.IsValid in a POST action, add custom errors with AddModelError, and show the form again
- Show validation messages with asp-validation-for and asp-validation-summary
Key takeaways
- Data annotations such as
[Required],[StringLength(40)]and[Range(0.01, 1000)]put rules on a model's properties, each with anErrorMessagethat says what to do. - A nullable property such as
int?is optional unless it's marked[Required]. - When
ModelState.IsValidisfalse, the POST action shows the form again with what the user typed, and saves nothing. ModelState.AddModelErroradds your own error for rules an attribute can't check, such as a product name that's already taken.asp-validation-forshows one field's message beside it;asp-validation-summary="ModelOnly"shows the errors that belong to no single field.
Rules on the model
Data annotations are attributes from System.ComponentModel.DataAnnotations that state a rule for a property:
using System.ComponentModel.DataAnnotations;
namespace NorthwindWeb.Models;
public class Product
{
public int ProductId { get; set; }
[Required(ErrorMessage = "Enter the product's name.")]
[StringLength(40, ErrorMessage = "A name can be at most 40 characters.")]
[Display(Name = "Name")]
public string ProductName { get; set; } = "";
[Required(ErrorMessage = "Choose a category.")]
[Display(Name = "Category")]
public int? CategoryId { get; set; }
[Range(0.01, 1000, ErrorMessage = "Enter a price from $0.01 to $1,000.")]
[Display(Name = "Price")]
public decimal? UnitPrice { get; set; }
[Range(0, 10000, ErrorMessage = "Enter a number of units from 0 to 10,000.")]
[Display(Name = "In stock")]
public short? UnitsInStock { get; set; }
}
| Annotation | Rule |
|---|---|
[Required] |
Must have a value. (A nullable property, such as int?, is optional unless marked [Required].) |
[StringLength(40)] |
At most 40 characters; add MinimumLength = 2 for a minimum |
[Range(0.01, 1000)] |
A number between the two, inclusive |
[EmailAddress] |
Looks like an email address |
[RegularExpression("...")] |
Matches a pattern |
[Compare("Password")] |
Equals another property, such as a password and its confirmation |
[Display(Name = "Price")] |
Not a rule: the label asp-for writes, and the name used in default messages |
Every rule takes an ErrorMessage. Write it for the person filling in the form: what to do, not what went wrong.
Checking: ModelState.IsValid
Model binding checks the rules while it fills the model, and records each problem in ModelState. The action decides what to do:
[HttpPost("new")]
[ValidateAntiForgeryToken]
public IActionResult Create(Product product)
{
if (productDao.NameExists(product.ProductName))
{
ModelState.AddModelError(nameof(Product.ProductName), "There's already a product with that name.");
}
if (!ModelState.IsValid)
{
ViewData["Categories"] = productDao.GetCategories();
return View(product);
}
int newId = productDao.Add(product);
return RedirectToAction(nameof(Details), new { id = newId });
}
ModelState.IsValidisfalseif any rule failed. Then the action shows the form again, with what the user typed, and saves nothing.ModelState.AddModelError(field, message)adds an error of your own, for rules an attribute can't express: here, a name that's already taken needs the database. Pass the property's name to attach it to that field, or""for an error about the whole form.nameof(Product.ProductName)gives the text"ProductName", but the compiler checks it, so a renamed property can't leave a stale name behind.
Showing the messages
Two tag helpers show the errors on the form:
<form method="post" asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="mb-3">
<label asp-for="ProductName" class="form-label"></label>
<input asp-for="ProductName" class="form-control">
<span asp-validation-for="ProductName" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="UnitPrice" class="form-label"></label>
<input asp-for="UnitPrice" class="form-control">
<span asp-validation-for="UnitPrice" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Add the product</button>
</form>
| Tag helper | Shows |
|---|---|
<span asp-validation-for="ProductName"> |
That field's message, beside the field |
<div asp-validation-summary="ModelOnly"> |
Errors that belong to no single field |
<div asp-validation-summary="All"> |
Every error, as a list |
The labels are empty in the view: asp-for fills them from [Display(Name = ...)].
Post the form with an empty name, no category and a price of 0, and it comes back with three messages:
Enter the product's name.
Choose a category.
Enter a price from $0.01 to $1,000.
Post a name that's already taken, such as Chai, and the custom error appears instead: There's already a product with that name.
Check yourself
3 questions to try on the lesson page.
So what
Rules on the model, ModelState.IsValid in the action, messages in the view: bad input comes back to the person who typed it, with a clear note, and never reaches the database.
Key terms
- Data Annotations
- ModelState
- Model Validation
- Validation Tag Helpers