How Form Data Moves Through ASP.NET Core MVC

This article follows one trip through the Survey Says Demographics form: loading the form, submitting it, validating it on the server, and either saving it or sending it back with errors.

Three-row flow diagram: a GET request loads the Edit form, the browser checks the form and sends a POST, and the server binds and validates the data, then either saves and redirects to Index or returns the form with errors.
How form data moves through Survey Says, from the initial GET through validation to either a redirect or a redisplayed form. Adapted from the CS 3550 Assignment 2 diagram.

Open the full-size form processing diagram

GET vs. POST

A GET request asks the server for something, like a page. Any data it sends goes in the URL, so it should never change anything on the server. Clicking a link or typing an address sends a GET.

A POST request sends data to the server to be processed, and that data goes in the request body instead of the URL. Submitting a form with method="post" sends a POST. That's why displaying the Edit form is a GET, but saving it is a POST.

How the Browser Sends Form Data

When the user clicks Save, the browser goes through every form control that has a name attribute and builds a list of name/value pairs. It joins them with & and puts them in the body of the POST request:

POST /Demographics/Edit HTTP/1.1
Content-Type: application/x-www-form-urlencoded

BirthYear=2006&Country=United+States&EducationLevel=5&HasHadFullTimeJob=true

A control without a name is never sent. Enum dropdowns send the enum's number, not its display text.

Connecting Form Controls to ViewModel Properties

The name attribute is the link between the HTML and the C# code. An input written as <input name="Country"> sends a pair named Country, and ASP.NET Core puts that value into the ViewModel's Country property. If the name is misspelled, the property is simply left empty. There's no error, just missing data.

In the Edit form, Birth year and Country are written in plain HTML, so we typed the name, id, and value attributes ourselves. Every other control uses a Tag Helper like asp-for="Country", which generates those attributes on the server. The browser never sees asp-for, only the plain HTML it produces.

Model Binding

Before the POST Edit action runs, ASP.NET Core creates a new DemographicViewModel and fills it in. For each property, it looks for a submitted pair with the same name and converts the text into the property's type, such as turning "2006" into an int? or "true" into a bool?. This process is called model binding, and the finished object is passed into the action as its model parameter.

Optional values use nullable types so that "left blank" becomes null instead of a misleading default like 0 or false.

Two Kinds of Validation

Attribute-Based Validation

Rules like [Required], [Range], and [StringLength] are placed on ViewModel properties. ASP.NET Core checks them automatically during model binding and records any failures. The same rules are also sent to the browser as data-val-* attributes, so JavaScript can catch mistakes before the form is even submitted.

Manual Controller Validation

Some rules are checked by hand in the controller. Birth year is validated manually because its upper limit is the current year, which changes. The full-time job rule compares two fields: years worked is required only if the user has had a full-time job. When a manual check fails, the controller calls ModelState.AddModelError to record the problem for that field.

Browser checks are a convenience that can be skipped or bypassed. The server's validation is the one that actually protects the data.

What ModelState.IsValid Means

ModelState holds the submitted values and every error found for them, from both the attributes and the manual checks. ModelState.IsValid is true only when there are no errors at all. That's why the manual checks run first: their errors need to be recorded before the controller asks whether the data is valid.

Invalid Data: Return the Edit View

If the data is invalid, the action runs return View(model);. This sends back the same Edit form, filled in with what the user typed, plus an error message next to each problem field. Nothing is saved. Redirecting instead would start a brand-new request, and the user's input and error messages would be lost.

Valid Data: Redirect to Index

If the data is valid, the action saves it and runs return RedirectToAction(nameof(Index));. This tells the browser to make a new GET request for the Index page. This pattern is called Post-Redirect-Get:

  1. GET /Demographics/Edit loads the form.
  2. POST /Demographics/Edit submits and saves it.
  3. GET /Demographics shows the saved result.

Because the last request is a GET, refreshing the page just reloads Index. Without the redirect, refreshing would ask the browser to submit the form a second time.

Why Saved Data Disappears on Restart

For now, the saved data lives in a static field on the controller. A static field belongs to the class, not to any single request, so it lasts between requests. But it only exists in the running program's memory. When the app stops, that memory is released and the data is gone.

A static field is also shared by every user and holds only one record, so each save overwrites the last. A database, which we'll add later, fixes both problems by storing data permanently and separately for each user.