Forms, model binding & validation
Turn browser input into trustworthy data with input models, validation, and antiforgery protection.
Here's what you'll be able to do.
- Separate create input from a database entity
- Handle invalid submissions without losing user input
- Protect form posts and redirect after successful writes
Complete the Razor lesson. Run TaskBoard and sign in to a learning account.
Understand the idea
A form is a boundary between a user-controlled browser and your application. Model binding converts submitted values into a C# object. Validation checks whether that object meets your rules. Authorization decides whether the user can perform the operation. None of these steps replaces the others.
TaskBoard uses a dedicated input model to make the accepted fields explicit. The action redisplays invalid input, writes only after validation succeeds, and redirects after success. That small sequence is the foundation of most reliable MVC forms.
Choose your environment
Open TaskBoard.csproj from the example download. Install the ASP.NET and web development workload and a Visual Studio release supporting .NET 10. Select the TaskBoard launch profile and press F5. Use Solution Explorer to find the files named below.
Install the .NET 10 SDK and C# Dev Kit. Open the TaskBoard folder in VS Code. Run dotnet restore, then dotnet run in the integrated terminal. Browse http://localhost:5300. Open files with Ctrl+P (Cmd+P on macOS).
Define the input contract
Required rejects an absent or whitespace-only title. StringLength limits the range of accepted lengths. Notes is optional but bounded. A small input model keeps database-only properties out of the binding surface.
Keep messages useful to a person filling in the form. “Give your task a title” tells them what to do. A database exception about a constraint does not. Attributes provide common checks; use ModelState.AddModelError for business rules that need additional context.
using System.ComponentModel.DataAnnotations;
namespace TaskBoard.Models;
public class TaskInput
{
[Required(ErrorMessage = "Give your task a title.")]
[StringLength(100, MinimumLength = 3)]
public string Title { get => _title; set => _title = value?.Trim() ?? ""; }
private string _title = "";
[StringLength(2000)]
public string? Notes { get; set; }
}
public sealed record TaskListPage(IReadOnlyList<BoardTask> Tasks, string? Query, int Page, int TotalPages, int TotalCount);Build an accessible form
The helpers create field names matching TaskInput and show validation errors beside the appropriate controls. The sample also displays an error summary. Antiforgery uses a token in the form and a companion cookie to establish that a submission belongs to the expected browser context.
Client-side validation can improve feedback speed, but it must never be the only check. This sample deliberately works with server-side validation alone, so disabling JavaScript does not bypass its rules.
@model TaskInput
@{ ViewData["Title"] = "Create task"; }
<a asp-action="Index">← My tasks</a><h1>Create task</h1>
<form method="post" class="editor">
@Html.AntiForgeryToken()
<div asp-validation-summary="All" role="alert"></div>
<label asp-for="Title"></label><input asp-for="Title" />
<span asp-validation-for="Title"></span>
<label asp-for="Notes"></label><textarea asp-for="Notes" rows="5"></textarea>
<span asp-validation-for="Notes"></span>
<button class="button" type="submit">Save task</button>
</form>Accept or redisplay the submission
MVC populates ModelState before the action runs. If it is invalid, returning the same view preserves the entered data and the validation errors. Only the valid path constructs a database entity and saves it. The owner comes from the authenticated principal, never from a hidden field.
After saving, the redirect changes the browser’s current page into a GET. Refreshing that result repeats the read, not the original create operation. TempData carries a one-time success message across the redirect.
[HttpPost, ValidateAntiForgeryToken]
public async Task<IActionResult> Create(TaskInput input, CancellationToken ct)
{
if (!ModelState.IsValid) return View(input);
db.Tasks.Add(new BoardTask
{
Title = input.Title.Trim(),
Notes = input.Notes,
OwnerId = OwnerId
});
await db.SaveChangesAsync(ct);
TempData["Message"] = "Task created.";
return RedirectToAction(nameof(Index));
}Test the boundary, not just the happy path
Submit an empty title, a two-character title, and a valid title. Confirm that invalid submissions do not add records. Then use the practice lab to send invalid values to a real server-side validation endpoint and inspect the returned errors.
A forged field that is absent from TaskInput should not affect the stored entity. A request with a missing antiforgery token should be rejected before the action saves anything. These are distinct tests: input shape, business rules, and request authenticity.
Empty title → validation error, no new record
Two-character title → validation error, no new record
Valid title → save, redirect, success message
Missing CSRF token → HTTP 400 before the writeMake it your own.
Add a rule preventing the title “Untitled task”. Return a field-specific error and preserve the entered notes. Test the rule with both lowercase and uppercase input.
See the solution & reasoning
After binding, compare input.Title with the reserved title using StringComparison.OrdinalIgnoreCase. If it matches, call ModelState.AddModelError(nameof(input.Title), "Choose a more descriptive title."). Then keep the existing !ModelState.IsValid return View(input) branch. For whitespace-insensitive comparison, trim before comparing.
When something doesn't work
Returning RedirectToAction for invalid input discards ModelState. Return View(input) instead.
An antiforgery 400 can result from missing tokens, stale cookies, or posting to a different origin. Inspect the request rather than removing the protection.
If you normalize input in ways that change validity, validate the normalized form too; for example, three characters including leading spaces may become a one-character title after trimming.
What should an MVC action do when ModelState is invalid?
That's another skill in your toolkit.
Your progress is saved in this browser. Sign in to save completion to your account.