ASP.NET CORE MVC - Data Validation
Validating Product Data with Data Annotations
Add validation rules to the Product model, prevent invalid values from being saved, display validation messages in Create and Edit forms, and understand how client-side and server-side validation work together.
By the end of this tutorial, the Product Management System will reject invalid Product data before it is saved to SQLite.
Part 6 completed the CRUD workflow. Users can create, read, edit and delete Products, but the application still allows values that may not make sense, such as an empty name, a negative price or a negative quantity.
- Verify the Part 6 application
- Understand why validation is required
- Add data annotations to
Product - Use
[Required] - Use
[StringLength] - Use
[Range] - Understand
ModelState - Add validation messages to Create
- Add validation messages to Edit
- Enable client-side validation
- Test invalid input
- Understand server-side validation
- Troubleshoot common validation problems
1. Open and Verify the Existing Project
cd ~/aspnet-mvc-tutorial/ProductManagement
pwd
The expected path is:
/home/xubuntu/aspnet-mvc-tutorial/ProductManagement
Confirm the important files:
ls Models/Product.cs
ls Controllers/ProductsController.cs
ls Views/Products/Create.cshtml
ls Views/Products/Edit.cshtml
Open the project:
code .
Build before continuing:
dotnet build
2. Why Validation Is Necessary
Without validation, a user might submit values such as:
| Field | Invalid example |
|---|---|
| Name | Empty |
| Description | Extremely long text |
| Price | -50.00 |
| Quantity | -10 |
The application should reject values that violate its business rules before they are stored.
3. Open the Product Model
Open:
Models/Product.cs
The Part 6 model currently resembles:
namespace ProductManagement.Models;
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
}
4. Add DataAnnotations Namespace
Add this namespace at the top:
using System.ComponentModel.DataAnnotations;
Data annotations are attributes that describe validation and display rules directly on model properties.
5. Add Validation Rules
Replace the Product class with:
using System.ComponentModel.DataAnnotations;
namespace ProductManagement.Models;
public class Product
{
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; } = string.Empty;
[StringLength(500)]
public string? Description { get; set; }
[Range(0.01, 1000000)]
public decimal Price { get; set; }
[Range(0, 1000000)]
public int Quantity { get; set; }
}
6. Understand [Required]
This attribute:
[Required]
indicates that the property must contain a value.
For the Product model:
[Required]
public string Name { get; set; } = string.Empty;
means that a Product name is required.
7. Understand [StringLength]
This rule:
[StringLength(100)]
limits the maximum length of Product Name to 100 characters.
The Description rule:
[StringLength(500)]
allows the field to remain optional but limits its maximum length when a value is supplied.
8. Understand [Range]
The Price rule:
[Range(0.01, 1000000)]
requires the price to fall between 0.01 and 1,000,000.
The Quantity rule:
[Range(0, 1000000)]
allows zero but rejects negative quantities.
The exact limits in a real application depend on business requirements. These values are tutorial examples that provide meaningful constraints for learning validation.
9. Add Friendly Error Messages
You can make the messages clearer:
[Required(ErrorMessage = "Product name is required.")]
[StringLength(
100,
ErrorMessage = "Product name cannot exceed 100 characters.")]
public string Name { get; set; } = string.Empty;
[StringLength(
500,
ErrorMessage = "Description cannot exceed 500 characters.")]
public string? Description { get; set; }
[Range(
0.01,
1000000,
ErrorMessage = "Price must be greater than zero.")]
public decimal Price { get; set; }
[Range(
0,
1000000,
ErrorMessage = "Quantity cannot be negative.")]
public int Quantity { get; set; }
10. Completed Product Model
using System.ComponentModel.DataAnnotations;
namespace ProductManagement.Models;
public class Product
{
public int Id { get; set; }
[Required(ErrorMessage = "Product name is required.")]
[StringLength(
100,
ErrorMessage = "Product name cannot exceed 100 characters.")]
public string Name { get; set; } = string.Empty;
[StringLength(
500,
ErrorMessage = "Description cannot exceed 500 characters.")]
public string? Description { get; set; }
[Range(
0.01,
1000000,
ErrorMessage = "Price must be greater than zero.")]
public decimal Price { get; set; }
[Range(
0,
1000000,
ErrorMessage = "Quantity cannot be negative.")]
public int Quantity { get; set; }
}
11. What Happens During a POST?
The Create and Edit POST actions already contain:
if (ModelState.IsValid)
Now that the model has validation attributes, MVC evaluates them during model binding.
12. Review the Create POST Action
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(Product product)
{
if (ModelState.IsValid)
{
_context.Add(product);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(product);
}
If validation fails, ModelState.IsValid is false. The view is redisplayed with the submitted Product and its validation errors.
13. Add Validation Summary to Create
Open:
Views/Products/Create.cshtml
Immediately inside the form, add:
<div asp-validation-summary="ModelOnly"
class="text-danger"></div>
The form begins:
<form asp-action="Create" method="post">
<div asp-validation-summary="ModelOnly"
class="text-danger"></div>
...
14. Add Field Validation Messages
For Name:
<div class="mb-3">
<label asp-for="Name" class="form-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name"
class="text-danger"></span>
</div>
Repeat the pattern for Description, Price and Quantity.
15. Completed Create View
@model Product
@{
ViewData["Title"] = "Create Product";
}
<h1>Create Product</h1>
<form asp-action="Create" method="post">
<div asp-validation-summary="ModelOnly"
class="text-danger"></div>
<div class="mb-3">
<label asp-for="Name" class="form-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name"
class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Description" class="form-label"></label>
<textarea asp-for="Description"
class="form-control"></textarea>
<span asp-validation-for="Description"
class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Price" class="form-label"></label>
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price"
class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Quantity" class="form-label"></label>
<input asp-for="Quantity" class="form-control" />
<span asp-validation-for="Quantity"
class="text-danger"></span>
</div>
<button type="submit"
class="btn btn-primary">Create</button>
<a asp-action="Index"
class="btn btn-secondary">Cancel</a>
</form>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
}
16. What asp-validation-for Does
For example:
<span asp-validation-for="Price"
class="text-danger"></span>
provides a location for validation messages associated with Price.
If the user enters a negative price, the message can appear beside that field.
17. Add Validation to Edit
Open:
Views/Products/Edit.cshtml
Add the same validation summary and field messages used in Create.
18. Completed Edit View
@model Product
@{
ViewData["Title"] = "Edit Product";
}
<h1>Edit Product</h1>
<form asp-action="Edit" method="post">
<input type="hidden" asp-for="Id" />
<div asp-validation-summary="ModelOnly"
class="text-danger"></div>
<div class="mb-3">
<label asp-for="Name" class="form-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name"
class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Description" class="form-label"></label>
<textarea asp-for="Description"
class="form-control"></textarea>
<span asp-validation-for="Description"
class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Price" class="form-label"></label>
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price"
class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Quantity" class="form-label"></label>
<input asp-for="Quantity" class="form-control" />
<span asp-validation-for="Quantity"
class="text-danger"></span>
</div>
<button type="submit"
class="btn btn-primary">Save Changes</button>
<a asp-action="Index"
class="btn btn-secondary">Cancel</a>
</form>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
}
19. Client-Side Validation
The default MVC template includes validation scripts through:
_ValidationScriptsPartial.cshtml
The Create and Edit views render that partial inside:
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
}
When the required client-side libraries are available, many validation errors can be shown before the form is sent to the server.
20. Server-Side Validation
Client-side validation improves usability, but server-side validation remains essential.
A user can disable JavaScript or send an HTTP request without using your form. The server must still validate submitted data.
21. Test an Empty Product Name
Run:
dotnet run
Open:
/Products/Create
Leave Name empty and attempt to submit the form.
You should see:
Product name is required.
The Product should not be saved.
22. Test a Negative Price
Enter:
Price: -10
The validation message should indicate:
Price must be greater than zero.
23. Test a Negative Quantity
Enter:
Quantity: -1
You should see:
Quantity cannot be negative.
24. Test Maximum String Length
Try entering a Name longer than 100 characters or a Description longer than 500 characters.
The corresponding [StringLength] rule should reject the value.
25. Verify Invalid Data Was Not Saved
Inspect SQLite:
sqlite3 ProductManagement.db
Run:
SELECT Id, Name, Price, Quantity FROM Products;
Invalid submissions should not appear as new records.
Exit:
.quit
26. Test Validation During Edit
Open /Products, edit an existing Product, and enter an invalid value such as:
Price: 0
The Edit POST action should redisplay the form instead of saving the invalid value.
27. Why Create and Edit Can Reuse the Same Rules
The validation rules belong to the Product model rather than being duplicated separately inside each controller action.
This keeps basic Product validation consistent across multiple forms.
28. Does Validation Require a New Migration?
For the particular validation attributes added in this tutorial, the immediate purpose is MVC input validation.
However, some model metadata changes can also affect the database model depending on the provider and configuration. Before creating a migration, always check whether EF Core detects a schema change.
Run:
dotnet ef migrations has-pending-model-changes
Do not automatically create a migration after every C# edit. Create a migration when the EF Core database model has actually changed.
29. Improve Display Names
You can also use [Display]:
[Display(Name = "Product Name")]
public string Name { get; set; } = string.Empty;
The Label Tag Helper can then use that metadata when generating a label.
For this tutorial, the existing property names are already readable, so [Display] is optional.
30. Validation Architecture
31. Troubleshooting
Validation messages do not appear
Confirm that the view contains asp-validation-for elements and that the view renders _ValidationScriptsPartial.
Client-side validation does not work
Check that the generated MVC project still contains the validation libraries and Views/Shared/_ValidationScriptsPartial.cshtml. Server-side validation should still work even when JavaScript validation does not.
The form does not save even with valid data
Inspect the validation summary and individual field messages. Also confirm that the POST action returns View(product) when ModelState.IsValid is false.
Price 0 is rejected
This is intentional because the tutorial uses [Range(0.01, 1000000)]. Adjust the business rule only if zero-priced Products should be allowed.
Quantity 0 is accepted
This is intentional. The rule [Range(0, 1000000)] permits zero because a Product may be temporarily out of stock.
32. Hands-On Exercise
Test the following cases:
| Test | Expected result |
|---|---|
| Empty Name | Rejected |
| Price = -5 | Rejected |
| Price = 0 | Rejected |
| Price = 25.50 | Accepted |
| Quantity = -1 | Rejected |
| Quantity = 0 | Accepted |
| Description omitted | Accepted |
33. Challenge Exercise
Add a minimum length requirement to Product Name:
[StringLength(
100,
MinimumLength = 3,
ErrorMessage = "Product name must contain between 3 and 100 characters.")]
Then verify that a two-character Product name is rejected.
34. Knowledge Check
- Why should Product data be validated?
- What namespace contains the common data annotation attributes?
- What does
[Required]do? - What does
[StringLength]do? - What does
[Range]do? - What is
ModelState? - What happens when
ModelState.IsValidis false? - What does
asp-validation-fordo? - What is the purpose of
asp-validation-summary? - Why is server-side validation required even when client-side validation is enabled?
- Why is Quantity allowed to be zero in this tutorial?
- Should every model edit automatically create a new migration?
Show suggested answers
- To prevent invalid or unreasonable values from being processed or saved.
System.ComponentModel.DataAnnotations.- It requires a value.
- It limits string length and can optionally define a minimum length.
- It constrains a numeric or comparable value to a specified range.
- It stores model-binding and validation state for the current request.
- The controller redisplays the view instead of saving the invalid model.
- It displays validation errors for a specific property.
- It displays model-level validation errors.
- Because client-side validation can be bypassed.
- Zero can represent an out-of-stock Product.
- No. Create a migration only when the EF Core database model has actually changed.
35. Part 7 Summary
In this tutorial, you:
- added data annotations to the Product model;
- required Product Name;
- limited Name and Description lengths;
- restricted Price and Quantity ranges;
- added custom validation messages;
- used
ModelState.IsValidwith real validation rules; - added validation summaries and field messages;
- enabled client-side validation in Create and Edit;
- tested invalid submissions; and
- confirmed that server-side validation prevents invalid data from being saved.
In Part 8, we will create a Category model, introduce primary and foreign keys, establish a one-to-many Category–Product relationship, create a new migration, load related data with Include(), and add a category dropdown to the Product forms.