Server-Side Validation
Server-side validation is the line nobody can bypass. Do it well and users get clear messages and keep what they typed; do it badly and they lose a whole form because of one typo.
On this page · 6 min · Intermediate
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 29, 2026
Learning objectives#
- Check ModelState.IsValid in POST actions
- Return the form with errors and the values the user entered
- Add business-rule errors with ModelState.AddModelError
- Show field and model-level messages
Explanation#
After model binding, ModelState holds every value and every error. If ModelState.IsValid is false, return View(model): the helpers re-fill the inputs from ModelState and show the messages.
Some rules need data — an email that already exists, stock that ran out. Check them in the action or a service and call ModelState.AddModelError("Email", "…") for a field or ModelState.AddModelError("", "…") for the whole form.
Html.ValidationSummary(true) shows only model-level errors (key ""), so field errors are not repeated; Html.ValidationSummary() shows all of them.
Inputs with errors get the CSS class input-validation-error and messages get field-validation-error, so you can style them.
Only when everything is valid do you save and redirect (Post/Redirect/Get).
Syntax#
[HttpPost, ValidateAntiForgeryToken]
public ActionResult Create(CustomerViewModel model)
{
if (_customers.EmailExists(model.Email))
ModelState.AddModelError("Email", "This email is already registered");
if (!ModelState.IsValid) return View(model);
_customers.Add(model);
return RedirectToAction("Index");
}
Code example: A customer form that keeps values and shows every error#
// Models/CustomerViewModel.cs
using System.ComponentModel.DataAnnotations;
namespace ShopSite.Models
{
public class CustomerViewModel
{
[Required(ErrorMessage = "Name is required")]
public string Name { get; set; }
[Required(ErrorMessage = "Email is required"), EmailAddress(ErrorMessage = "Enter a valid email")]
public string Email { get; set; }
}
}
// Controllers/CustomersController.cs
using System;
using System.Collections.Generic;
using System.Web.Mvc;
using ShopSite.Models;
namespace ShopSite.Controllers
{
public class CustomersController : Controller
{
private static readonly HashSet<string> Emails = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "asha@example.com" };
public ActionResult Create() => View(new CustomerViewModel());
[HttpPost, ValidateAntiForgeryToken]
public ActionResult Create(CustomerViewModel model)
{
if (model.Email != null && Emails.Contains(model.Email))
ModelState.AddModelError("Email", "This email is already registered");
if (DateTime.Now.Hour < 0)
ModelState.AddModelError("", "Registrations are closed right now");
if (!ModelState.IsValid) return View(model);
Emails.Add(model.Email);
TempData["Message"] = "Customer added";
return RedirectToAction("Create");
}
}
}
@* Views/Customers/Create.cshtml *@
@model CustomerViewModel
<p>@TempData["Message"]</p>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
@Html.ValidationSummary(false)
@Html.TextBoxFor(m => m.Name)
@Html.ValidationMessageFor(m => m.Name)
@Html.TextBoxFor(m => m.Email)
@Html.ValidationMessageFor(m => m.Email)
<button>Save</button>
}
GET /Customers/Create 200 OK POST /Customers/Create Name=&Email=ASHA%40example.com 200 OK <p></p> <form action="/Customers/Create" method="post"><input name="__RequestVerificationToken" type="hidden" value="(random token)" /><div class="validation-summary-errors" data-valmsg-summary="true"><ul><li>Name is required</li> <li>This email is already registered</li> </ul></div><input class="input-validation-error" data-val="true" data-val-required="Name is required" id="Name" name="Name" type="text" value="" /><span class="field-validation-error" data-valmsg-for="Name" data-valmsg-replace="true">Name is required</span><input class="input-validation-error" data-val="true" data-val-email="Enter a valid email" data-val-required="Email is required" id="Email" name="Email" type="text" value="ASHA@example.com" /><span class="field-validation-error" data-valmsg-for="Email" data-valmsg-replace="true">This email is already registered</span> <button>Save</button> </form> POST /Customers/Create Name=Ravi&Email=ravi%40example.com 302 Found → /Customers/Create GET /Customers/Create 200 OK <p>Customer added</p>
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#
- The first POST has an empty name (attribute error) and an email that already exists (business-rule error added in the action).
- Because ModelState is invalid, the action returns View(model). The Email box keeps ASHA@example.com, and both inputs get the input-validation-error class.
- ValidationSummary(false) lists every error at the top; the spans next to each field show the same messages.
- The second POST is valid: the email is saved, a TempData message is set and the browser is redirected (302).
- The follow-up GET shows “Customer added” once.
Real-world use case#
A clinic’s appointment form checks on the server that the chosen slot is still free. When two receptionists book the same slot, the second sees “This slot was just taken” next to the time field, with the rest of the form still filled in.
Common mistakes#
RedirectToAction throws away ModelState, so the user sees an empty form with no messages. Return View(model) instead.
AddModelError("email", …) works (keys are case-insensitive) but AddModelError("EmailAddress", …) does not match the Email field, so the message only appears in the summary.
If the form has a dropdown built from ViewBag, the POST action must rebuild it before returning View(model), or the view throws.
Best practices#
- Check ModelState.IsValid in every POST
- Use field keys for field errors and "" for form-level errors
- Return View(model) on errors, redirect on success
- Keep validation messages short and actionable
Remember this#
Invalid? return View(model). Valid? save and redirect.
AddModelError("Field", msg) or ("", msg).
ValidationSummary(true) = model-level errors only.
Practice exercise#
Your task: Add a rule to a ProductViewModel POST: if Price is below 1, add a field error “Price must be at least 1”; if the name is “test”, add a model-level error “Test products are not allowed”. Return the errors as text.
Need a hint?
Show solution
// Controllers/ProductRulesController.cs
using System.Linq;
using System.Web.Mvc;
namespace ShopSite.Controllers
{
public class ProductRulesController : Controller
{
[HttpPost]
public ActionResult Check(string name, decimal price)
{
if (price < 1) ModelState.AddModelError("Price", "Price must be at least 1");
if (name == "test") ModelState.AddModelError("", "Test products are not allowed");
if (ModelState.IsValid) return Content("OK", "text/plain");
return Content(string.Join("\n", ModelState.Where(kv => kv.Value.Errors.Any())
.Select(kv => quot;[{kv.Key}] {kv.Value.Errors[0].ErrorMessage}")), "text/plain");
}
}
}
POST /ProductRules/Check name=test&price=0.5 200 OK [price] Price must be at least 1 [] Test products are not allowed POST /ProductRules/Check name=Ruler&price=15 200 OK OK
Mini quiz#
What should a POST action return when ModelState.IsValid is false?
Want more? Practise all ASP.NET MVC MCQs · ASP.NET MVC interview questions
My notes#
Saved in this browser.