ASP.NET Core MVC · Models

Model Validation

Users mistype emails, leave fields empty and enter impossible ages. ShopSmart’s registration form must catch that, show friendly messages next to the right fields, and keep what the user already typed. MVC’s validation system does most of the work.

Intermediate 7 min Lesson 14 of 42Examples target .NET 9.
Before you start:Model Binding
0/42 in ASP.NET Core MVC
On this page · 7 min · Intermediate

In simple words#

Validation is the exam invigilator checking the form: required fields filled, email looks right, age in range — before anything is saved.

ModelState.IsValid
True if all rules passed.
Client + server
Browser checks for UX, server checks for safety.
asp-validation-for
Shows the error next to the field.

Learning objectives#

  • Check ModelState.IsValid after binding
  • Redisplay a form with errors and the user’s input
  • Show messages with asp-validation-for and asp-validation-summary
  • Add business-rule errors with ModelState.AddModelError

Explanation#

Validation runs automatically during model binding: data annotation attributes on the input model ([Required], [EmailAddress], [Range]) are checked, and conversion failures are recorded too. The result is in ModelState.

In the POST action, check if (!ModelState.IsValid) return View(input); — the same view is rendered again with the user’s values and the error messages.

Tag helpers display errors: <span asp-validation-for="Email"></span> next to a field, and <div asp-validation-summary="ModelOnly"></div> for errors that are not tied to a field.

Some rules need code: “this email is already registered”. Add them with ModelState.AddModelError(nameof(input.Email), "…") before checking IsValid. Use an empty key for form-level errors.

On success, redirect (Post/Redirect/Get) so a browser refresh does not submit the form twice. With the jQuery validation scripts (_ValidationScriptsPartial), the same rules also run in the browser — but the server check is the one that counts.

Syntax#

C# Illustrative
[HttpPost, ValidateAntiForgeryToken]
public IActionResult Register(RegisterInput input)
{
    if (!ModelState.IsValid) return View(input);   // show the form again with messages
    // … save …
    return RedirectToAction(nameof(Welcome));      // Post/Redirect/Get
}

Code example: A registration form with server-side validation#

C#Controllers/AccountController.cs Illustrative
// Program.cs
using System.Net;
using System.Text.RegularExpressions;


var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();                                  // keep the demo output clean
builder.Services.AddControllersWithViews();
var app = builder.Build();
app.MapDefaultControllerRoute();
app.Urls.Add("http://127.0.0.1:5112");
await app.StartAsync();


// The client keeps cookies, like a browser, so the antiforgery token check passes
var handler = new HttpClientHandler { AllowAutoRedirect = false };
using var http = new HttpClient(handler) { BaseAddress = new Uri("http://127.0.0.1:5112") };
var form = await http.GetStringAsync("/Account/Register");
var token = Regex.Match(form, "name=\"__RequestVerificationToken\" type=\"hidden\" value=\"([^\"]+)\"").Groups[1].Value;


async Task Submit(string name, string email, string age)
{
    var response = await http.PostAsync("/Account/Register", new FormUrlEncodedContent(
        [new("Name", name), new("Email", email), new("Age", age), new("__RequestVerificationToken", token)]));
    Console.WriteLine(
quot;POST Name='
{name}' Email='{email}' Age='{age}' -> {(int)response.StatusCode} {response.Headers.Location}".TrimEnd());
var html = await response.Content.ReadAsStringAsync(); foreach (Match m in Regex.Matches(html, "data-valmsg-for=\"(\\w+)\"[^>]*>([^<]+)<")) Console.WriteLine(
quot;
{m.Groups[1].Value}: {WebUtility.HtmlDecode(m.Groups[2].Value)}");
} await Submit("", "not-an-email", "15"); await Submit("Asha", "asha@example.com", "abc"); await Submit("Asha", "asha@shopsmart.example", "28"); await app.StopAsync(); // Models/RegisterInput.cs using System.ComponentModel.DataAnnotations; public class RegisterInput { [Required, StringLength(40, MinimumLength = 2)] public string Name { get; set; } = ""; [Required, EmailAddress] public string Email { get; set; } = ""; [Range(18, 120, ErrorMessage = "You must be at least 18.")] public int Age { get; set; } } // Controllers/AccountController.cs using Microsoft.AspNetCore.Mvc; public class AccountController : Controller { [HttpGet] public IActionResult Register() => View(new RegisterInput()); [HttpPost, ValidateAntiForgeryToken] public IActionResult Register(RegisterInput input) { if (input.Email.EndsWith("@example.com", StringComparison.OrdinalIgnoreCase)) // a rule that needs code ModelState.AddModelError(nameof(input.Email), "Please use your real email address."); if (!ModelState.IsValid) return View(input); // same view, with messages and typed values return RedirectToAction(nameof(Welcome), new { name = input.Name }); } public IActionResult Welcome(string name) => Content(
quot;Welcome,
{name}!");
} @* Views/Account/Register.cshtml *@ @model RegisterInput <form asp-action="Register" method="post"> <label asp-for="Name"></label> <input asp-for="Name" /> <span asp-validation-for="Name"></span> <label asp-for="Email"></label> <input asp-for="Email" /> <span asp-validation-for="Email"></span> <label asp-for="Age"></label> <input asp-for="Age" /> <span asp-validation-for="Age"></span> <button>Create account</button> </form>
Expected output
POST Name='' Email='not-an-email' Age='15' -> 200
  Name: The Name field is required.
  Email: The Email field is not a valid e-mail address.
  Age: You must be at least 18.
POST Name='Asha' Email='asha@example.com' Age='abc' -> 200
  Email: Please use your real email address.
  Age: The value 'abc' is not valid for Age.
POST Name='Asha' Email='asha@shopsmart.example' Age='28' -> 302 /Account/Welcome?name=Asha

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. The GET request renders the form; the form tag helper adds a hidden antiforgery token, which the client sends back.
  2. The first POST breaks three rules, so ModelState is invalid and the form is shown again with three messages.
  3. In the second POST, “abc” cannot be converted to int, and the example.com address fails the custom rule.
  4. The third POST is valid, so the action redirects (302) to Welcome — Post/Redirect/Get.
  5. Each <span asp-validation-for> shows the message for its own field.

Real-world use case#

ShopSmart validates registration, address, checkout and product-editing forms this way, adding code rules such as “email already registered” or “coupon expired” with AddModelError.

Common mistakes#

Forgetting to check ModelState.IsValid

Binding never throws for bad input. Without the check, invalid data is saved.

Redirecting on errors

Redirecting after a failed POST loses the user’s input and the messages. Return View(input) instead.

Client-side validation only

Browser checks can be bypassed in seconds. The server must validate every request.

Best practices#

  • Validate on the server; add client scripts for UX
  • Return View(input) on errors, redirect on success
  • Use AddModelError for business rules
  • [ValidateAntiForgeryToken] on every form POST

Remember this#

Bind → validate → ModelState.IsValid.

Invalid → return View(input). Valid → redirect.

asp-validation-for shows the field’s message.

Practice exercise#

Your task: Add a ConfirmEmail field that must match Email, and a form-level error “Registrations are closed on Sundays” when DateTime.Today is a Sunday (shown with asp-validation-summary="ModelOnly").

Need a hint?
[Compare(nameof(Email))] for the field; ModelState.AddModelError(string.Empty, "…") for the form-level error.
Show solution
C# Illustrative
// Models/RegisterInput.cs
using System.ComponentModel.DataAnnotations;


public class RegisterInput
{
    [Required, EmailAddress] public string Email { get; set; } = "";
    [Compare(nameof(Email), ErrorMessage = "The emails do not match.")] public string ConfirmEmail { get; set; } = "";
}


// Controllers/AccountController.cs
using Microsoft.AspNetCore.Mvc;


public class AccountController : Controller
{
    [HttpPost, ValidateAntiForgeryToken]
    public IActionResult Register(RegisterInput input)
    {
        if (DateTime.Today.DayOfWeek == DayOfWeek.Sunday)
            ModelState.AddModelError(string.Empty, "Registrations are closed on Sundays.");
        if (!ModelState.IsValid) return View(input);
        return RedirectToAction("Index", "Home");
    }
}


@* Views/Account/Register.cshtml *@
@model RegisterInput
<form asp-action="Register" method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <input asp-for="Email" /> <span asp-validation-for="Email"></span>
    <input asp-for="ConfirmEmail" /> <span asp-validation-for="ConfirmEmail"></span>
    <button>Create account</button>
</form>

Mini quiz#

Model Validation quizMultiple choice Question 1 of 3

What should a POST action do when ModelState is invalid?

Score so far: 0/0

Want more? Practise all ASP.NET Core MVC MCQs · ASP.NET Core MVC interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.