Learn how validation attributes such as Required, StringLength, Range, EmailAddress and Compare protect your forms, how ModelState collects the results, and how to show error messages in the view.

Bishal Biswas
Self Learner
Never trust the form
Every value that arrives from a browser could be wrong. A price could be negative. An email could be missing its @. A name could be three thousand characters long. Validation is the habit of checking every incoming value before you use it.
In MVC, the easiest way to validate is with data annotations: attributes you place on the properties of your model. The framework reads them, checks the posted values, and tells you what went wrong.
Your first validation attributes
using System.ComponentModel.DataAnnotations;
public class RegisterViewModel
{
[Required]
[StringLength(50, MinimumLength = 2)]
public string Name { get; set; } = "";
[Required]
[EmailAddress]
public string Email { get; set; } = "";
[Range(18, 120)]
public int Age { get; set; }
}
[Required]means the value must not be empty.[StringLength(50, MinimumLength = 2)]limits the length.[EmailAddress]checks the shape of an email address.[Range(18, 120)]limits a number to a range.
Nothing else is needed. When this model is bound from a form, these rules run automatically.
Where the results go
After binding, the framework stores the outcome in ModelState. Your action checks one property:
[HttpPost]
public IActionResult Register(RegisterViewModel vm)
{
if (!ModelState.IsValid)
{
return View(vm);
}
// Safe to use vm here
return RedirectToAction("Welcome");
}
ModelState.IsValid is false if any rule failed. You return the same view with the same model so the user sees their input and the messages.
The action still runs when validation fails. It is up to you to check IsValid. Forgetting this check is one of the most common bugs in MVC apps.
Showing the messages
The tag helpers from the forms topic print the messages:
<div asp-validation-summary="All" class="text-danger"></div>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
asp-validation-for shows the message for one property. asp-validation-summary shows either all messages (All) or only those not tied to a property (ModelOnly). Most forms use per field spans plus a ModelOnly summary at the top.
Custom messages
The default messages are functional but plain, such as “The Email field is required.” Set ErrorMessage to say it better:
[Required(ErrorMessage = "Please enter your email address.")]
[EmailAddress(ErrorMessage = "That does not look like an email address.")]
public string Email { get; set; } = "";
Use {0} inside the message to insert the display name, and {1}, {2} for the attribute’s arguments:
[StringLength(50, MinimumLength = 2, ErrorMessage = "{0} must be between {2} and {1} characters.")]
[Display(Name = "Full name")]
public string Name { get; set; } = "";
This produces “Full name must be between 2 and 50 characters.”
The common attributes
| Attribute | Checks |
|---|---|
[Required] |
Not null or empty |
[StringLength(max, MinimumLength = min)] |
Text length |
[MinLength(n)], [MaxLength(n)] |
Length of text or collections |
[Range(min, max)] |
Number or date within bounds |
[EmailAddress] |
Email shape |
[Phone] |
Phone number shape |
[Url] |
URL shape |
[RegularExpression(pattern)] |
Matches a regex |
[Compare(nameof(Other))] |
Equals another property |
[CreditCard] |
Passes the Luhn check |
[DataType] and [Display] are not validation attributes. They only affect how fields are drawn.
Comparing two fields
The registration form classic:
[Required]
[DataType(DataType.Password)]
public string Password { get; set; } = "";
[DataType(DataType.Password)]
[Compare(nameof(Password), ErrorMessage = "The passwords do not match.")]
public string ConfirmPassword { get; set; } = "";
Regular expressions
For anything with a specific shape, such as a postcode or a username:
[RegularExpression(@"^[a-z0-9_]{3,20}$", ErrorMessage = "Use 3 to 20 lowercase letters, numbers or underscores.")]
public string Username { get; set; } = "";
Required and value types
[Required] on an int does nothing useful. An int cannot be null, so if the user leaves the field empty the binder puts 0 there and the required check passes. To make a number truly required, make it nullable:
[Required]
public int? Quantity { get; set; }
Now an empty field leaves Quantity as null and [Required] catches it. The same applies to decimal, DateTime and bool.
Nullable reference types and Required
When nullable reference types are enabled, which is the default in new projects, a non nullable string property is treated as required even without the attribute. The rule of thumb:
string Nameis required by the compiler’s nullability.string? Nameis optional.
Adding [Required] anyway is fine and lets you set a custom message.
Adding errors from code
Some checks need data the attributes cannot see, such as “is this email already registered?” Add those errors yourself before checking IsValid:
[HttpPost]
public IActionResult Register(RegisterViewModel vm)
{
if (_users.EmailExists(vm.Email))
{
ModelState.AddModelError(nameof(vm.Email), "This email is already registered.");
}
if (!ModelState.IsValid)
{
return View(vm);
}
// ...
}
The first argument is the property name, so the message appears next to the email field. Pass an empty string to add a model level error that shows in the summary.
Validation on nested objects
Validation walks into child objects and collections. If an Order has a ShippingAddress with its own attributes, those rules run too, and the error keys look like ShippingAddress.PostalCode.
Quick recap
- Data annotations are attributes on model properties that describe what values are allowed.
- Validation runs automatically during model binding and stores results in
ModelState. - Always check
ModelState.IsValidin POST actions and return the view with the model when it is false. - Use
ErrorMessagewith{0}placeholders for friendly messages. - Use nullable types for numbers and dates that must be filled in, or
[Required]will never fail. ModelState.AddModelErroradds checks that need data from outside the model.
// ViewModels/RegisterViewModel.cs
using System.ComponentModel.DataAnnotations;
public class RegisterViewModel
{
[Required(ErrorMessage = “Please enter your name.”)]
[StringLength(50, MinimumLength = 2, ErrorMessage = “{0} must be between {2} and {1} characters.”)]
[Display(Name = “Full name”)]
public string Name { get; set; } = “”;
[Required(ErrorMessage = "Please enter your email address.")]
[EmailAddress(ErrorMessage = "That does not look like an email address.")]
public string Email { get; set; } = "";
[Required]
[RegularExpression(@"^[a-z0-9_]{3,20}$",
ErrorMessage = "Use 3 to 20 lowercase letters, numbers or underscores.")]
public string Username { get; set; } = "";
[Required]
[Range(18, 120, ErrorMessage = "You must be between {1} and {2} years old.")]
public int? Age { get; set; }
[Required]
[StringLength(100, MinimumLength = 8)]
[DataType(DataType.Password)]
public string Password { get; set; } = "";
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare(nameof(Password), ErrorMessage = "The passwords do not match.")]
public string ConfirmPassword { get; set; } = "";
[Phone]
public string? PhoneNumber { get; set; }
[Url]
public string? Website { get; set; }
}
// Controllers/AccountController.cs
public class AccountController : Controller
{
private readonly IUserRepository _users;
public AccountController(IUserRepository users) => _users = users;
[HttpGet]
public IActionResult Register() => View(new RegisterViewModel());
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Register(RegisterViewModel vm)
{
if (_users.EmailExists(vm.Email))
{
ModelState.AddModelError(nameof(vm.Email), "This email is already registered.");
}
if (_users.UsernameExists(vm.Username))
{
ModelState.AddModelError(nameof(vm.Username), "This username is taken.");
}
if (!ModelState.IsValid)
{
return View(vm);
}
_users.Create(vm.Name, vm.Email, vm.Username, vm.Password);
TempData["Success"] = "Welcome aboard!";
return RedirectToAction("Index", "Home");
}
}
// Views/Account/Register.cshtml
//
// @model RegisterViewModel
// <form asp-action=“Register” method=“post”>
// <div asp-validation-summary=“ModelOnly” class=“text-danger”></div>
// <label asp-for=“Name”></label>
// <input asp-for=“Name” class=“form-control” />
// <span asp-validation-for=“Name” class=“text-danger”></span>
// <label asp-for=“Email”></label>
// <input asp-for=“Email” class=“form-control” />
// <span asp-validation-for=“Email” class=“text-danger”></span>
// <label asp-for=“Age”></label>
// <input asp-for=“Age” class=“form-control” />
// <span asp-validation-for=“Age” class=“text-danger”></span>
// <label asp-for=“Password”></label>
// <input asp-for=“Password” class=“form-control” />
// <span asp-validation-for=“Password” class=“text-danger”></span>
// <label asp-for=“ConfirmPassword”></label>
// <input asp-for=“ConfirmPassword” class=“form-control” />
// <span asp-validation-for=“ConfirmPassword” class=“text-danger”></span>
// <button type=“submit” class=“btn btn-primary”>Register</button>
// </form>
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready