Model Binding a Form

ASP.NET Core fills an action's parameters, even a whole Product, from the request by matching names. asp-for writes inputs whose names match the model, and ModelState says when a value didn't fit.

Journeyman · ASP.NET Core · Razor · C# · Updated

After this lesson you can

  • Explain how model binding fills an action's parameters and models from the request by name
  • Write a form with asp-for so its fields match the model's properties
  • Check ModelState.IsValid and show the form again when a value doesn't fit

Key takeaways

  • Model binding fills an action's parameters by name, from the route, the query string or the form, and fills a class such as Product property by property.
  • asp-for writes a field whose name matches the model's property, along with its id, type and value.
  • When a value doesn't fit, such as abc for the price or an empty ProductName, ModelState.IsValid is false.
  • Check ModelState.IsValid before saving, and if it's false, return the view with the model so the user can fix what they typed.

The POST action in the last lesson took a whole Product:

public IActionResult Create(Product product)

Nobody wrote code to read the form and build that object. ASP.NET Core did it, and that's called model binding.

Binding matches names

Model binding fills each parameter by name, from whichever part of the request has that name:

Comes from Example Fills
The route /Products/Details/7 int id
The query string /Products/Search?name=sauce string? name
Form fields, in a POST ProductName=Maple Syrup&UnitPrice=7.50 Product product's properties

For a class such as Product, binding creates the object, then fills each property from the field with the property's name: the ProductName field into ProductName, the UnitPrice field into UnitPrice, converting the text to decimal on the way. A property with no matching field keeps its default.

So a form's job is to send fields with exactly the model's property names.

asp-for writes the fields for you

You could type every name yourself, but the asp-for tag helper does it from the model, and more. Here's NorthwindWeb's Views/Products/Create.cshtml:

@model Product
@{
    ViewData["Title"] = "Add a Product";
    List<Category> categories = (List<Category>)ViewData["Categories"]!;
}

<h1>Add a Product</h1>

<form method="post" asp-action="Create">
    <div class="mb-3">
        <label asp-for="ProductName" class="form-label">Name</label>
        <input asp-for="ProductName" class="form-control">
        <span asp-validation-for="ProductName" class="text-danger"></span>
    </div>
    <div class="mb-3">
        <label asp-for="CategoryId" class="form-label">Category</label>
        <select asp-for="CategoryId" class="form-select">
            @foreach (Category category in categories)
            {
                <option value="@category.CategoryId">@category.CategoryName</option>
            }
        </select>
    </div>
    <div class="mb-3">
        <label asp-for="UnitPrice" class="form-label">Price</label>
        <input asp-for="UnitPrice" class="form-control">
    </div>
    <div class="mb-3">
        <label asp-for="UnitsInStock" class="form-label">In stock</label>
        <input asp-for="UnitsInStock" class="form-control">
    </div>
    <button type="submit" class="btn btn-primary">Add the product</button>
</form>

asp-for="ProductName" names a property of the view's model. Here's some of the HTML it becomes:

<label class="form-label" for="ProductName">Name</label>
<input class="form-control" type="text" data-val="true" data-val-required="The ProductName field is required." id="ProductName" name="ProductName" value="">
...
<select class="form-select" id="CategoryId" name="CategoryId">
    <option value="1">Beverages</option>
    <option value="2">Condiments</option>
    ...
</select>
...
<input class="form-control" type="number" id="UnitsInStock" name="UnitsInStock" value="">

From one attribute, each field got:

  • a name that matches the property, so binding can fill it;
  • an id, and a label whose for points at it, so clicking the label puts the cursor in the field;
  • a type from the property's type: number for short?, text for string;
  • a value from the model, so a form shown again keeps what the user typed.

The <select> sends the chosen option's value, a category's id, as CategoryId.

When a value doesn't fit: ModelState

What if the user types abc for the price, or leaves the name empty? Binding can't put abc into a decimal?, so it leaves UnitPrice empty, and it records the problem in ModelState, the action's list of binding results. ModelState.IsValid is false if anything went wrong.

The name counts too. ProductName is a plain string, not string?, so ASP.NET Core treats it as required: an empty name is an error. (That's the data-val-required in the HTML above.)

Check ModelState.IsValid before saving, and if it's false, show the form again with what the user typed:

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(Product product)
{
    if (!ModelState.IsValid)
    {
        ViewData["Categories"] = productDao.GetCategories();
        return View(product);
    }

    int newId = productDao.Add(product);
    return RedirectToAction("Details", new { id = newId });
}

Posting an empty name and abc for the price now shows the form again, and the asp-validation-for span shows the name's message:

<input class="form-control input-validation-error" type="text" ... id="ProductName" name="ProductName" value="">
<span class="text-danger field-validation-error" data-valmsg-for="ProductName" data-valmsg-replace="true">The ProductName field is required.</span>
...
<input class="form-control input-validation-error" type="text" ... id="UnitPrice" name="UnitPrice" value="abc">

The price box still shows abc, marked with the input-validation-error class, ready to be fixed. (The categories are loaded again because ViewData lasts only one request.)

You'll add your own rules, such as a price that must be more than zero, with friendlier messages, in ASP.NET Core MVC in Depth.

Common problems

What you see What to do
A property is always empty after the POST The field's name doesn't match the property. Use asp-for instead of typing names.
NullReferenceException on categories when the form is shown again The POST action returned the view without setting ViewData["Categories"] again.
A bad value is saved as empty instead of being reported The action doesn't check ModelState.IsValid before saving.

Check yourself

3 questions to try on the lesson page.

So what

Names connect everything: the property's name becomes the field's name, which becomes the request's name, which binding matches back to the property. Write forms with asp-for, check ModelState, and the round trip from a form to a C# object takes no code at all.

Key terms

  • Model Binding
  • asp-for
  • ModelState

Cheat sheets