ASP.NET CORE MVC - Data Validation

ASP.NET CORE MVC TUTORIAL SERIES · PART 7

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.

Objective

By the end of this tutorial, the Product Management System will reject invalid Product data before it is saved to SQLite.

Starting point

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.

In this tutorial
  1. Verify the Part 6 application
  2. Understand why validation is required
  3. Add data annotations to Product
  4. Use [Required]
  5. Use [StringLength]
  6. Use [Range]
  7. Understand ModelState
  8. Add validation messages to Create
  9. Add validation messages to Edit
  10. Enable client-side validation
  11. Test invalid input
  12. Understand server-side validation
  13. 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:

FieldInvalid example
NameEmpty
DescriptionExtremely long text
Price-50.00
Quantity-10

The application should reject values that violate its business rules before they are stored.

User input ↓ Model Binding ↓ Validation ↓ Valid? ┌──┴──┐ Yes No ↓ ↓ Save Show errors

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.

Business rules

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.

POST form ↓ Model Binding ↓ Product object ↓ Data Annotation validation ↓ ModelState.IsValid

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.

Browser validation ↓ Convenient early feedback POST reaches server ↓ Model Binding ↓ Data Annotation validation ↓ ModelState.IsValid
Never rely only on browser validation

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.

Product model ↓ Validation attributes ↓ Create POST ↓ Edit POST

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
Teaching principle

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

Razor form ↓ Tag Helpers ↓ Browser/client validation ↓ HTTP POST ↓ Model Binding ↓ Data Annotations ↓ ModelState ↓ Controller ┌────────────┴────────────┐ Valid Invalid ↓ ↓ Save Redisplay view

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:

TestExpected result
Empty NameRejected
Price = -5Rejected
Price = 0Rejected
Price = 25.50Accepted
Quantity = -1Rejected
Quantity = 0Accepted
Description omittedAccepted

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

  1. Why should Product data be validated?
  2. What namespace contains the common data annotation attributes?
  3. What does [Required] do?
  4. What does [StringLength] do?
  5. What does [Range] do?
  6. What is ModelState?
  7. What happens when ModelState.IsValid is false?
  8. What does asp-validation-for do?
  9. What is the purpose of asp-validation-summary?
  10. Why is server-side validation required even when client-side validation is enabled?
  11. Why is Quantity allowed to be zero in this tutorial?
  12. Should every model edit automatically create a new migration?
Show suggested answers
  1. To prevent invalid or unreasonable values from being processed or saved.
  2. System.ComponentModel.DataAnnotations.
  3. It requires a value.
  4. It limits string length and can optionally define a minimum length.
  5. It constrains a numeric or comparable value to a specified range.
  6. It stores model-binding and validation state for the current request.
  7. The controller redisplays the view instead of saving the invalid model.
  8. It displays validation errors for a specific property.
  9. It displays model-level validation errors.
  10. Because client-side validation can be bypassed.
  11. Zero can represent an out-of-stock Product.
  12. 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.IsValid with 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.
Input ↓ Validation ↓ ModelState ↓ Valid data ↓ EF Core ↓ SQLite
Next: Part 8 — Categories and Database Relationships

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.