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

## 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 an `ErrorMessage` that says what to do.
- A nullable property such as `int?` is optional unless it's marked `[Required]`.
- When `ModelState.IsValid` is `false`, the POST action shows the form again with what the user typed, and saves nothing.
- `ModelState.AddModelError` adds your own error for rules an attribute can't check, such as a product name that's already taken.
- `asp-validation-for` shows 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:

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

```csharp
[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.IsValid`** is `false` if 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:

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

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

## Cheat sheets

- [Data Annotations and ModelState](https://www.ask-codey.com/cheat-sheets/model-binding-and-validation-data-annotations-and-modelstate-cheat-sheet)

Source: https://www.ask-codey.com/lessons/model-binding-and-validation-data-annotations-and-modelstate
