ASP.NET MVC · Forms & Validation

Model Binding

Model binding is the quiet helper that turns text from the URL and form fields into typed C# objects. Knowing how it works explains half of the “why is my property null?” questions in MVC.

Intermediate 6 min Lesson 14 of 37Covers ASP.NET MVC 5.2 on .NET Framework 4.8.
Before you start:Strongly Typed Views
0/37 in ASP.NET MVC
On this page · 6 min · Intermediate

Learning objectives#

  • Bind simple and complex parameters
  • Bind lists using indexed field names
  • Read conversion errors from ModelState
  • Prevent over-posting with [Bind] or view models

Explanation#

For each action parameter, the default model binder looks for a value with the same name in the form fields, then route values, then the query string. Names are matched case-insensitively.

For a complex type (a class), the binder creates the object and fills each public property with a matching name: a field named Price fills product.Price. Nested properties use dots (Address.City) and lists use indexes (Lines[0].Qty).

Values that cannot be converted (Price=abc) do not throw. The property keeps its default value and ModelState gets an error: “The value 'abc' is not valid for Price.”

The binder fills every property it finds a value for — including ones you did not put in the form, such as IsAdmin. This is over-posting. Use [Bind(Include = "Name,Price")] or, better, a view model that only has the editable fields.

UpdateModel(entity) and TryUpdateModel(entity, new[] { "Name", "Price" }) bind into an existing object, which is handy for edit pages.

Syntax#

C# Illustrative
public ActionResult Search(string q, int page = 1)     // ?q=pen&page=2
public ActionResult Create(Product product)          // form fields → properties
public ActionResult Save(List<OrderLine> lines)      // lines[0].Qty, lines[1].Qty
public ActionResult Edit([Bind(Include = "Name,Price")] Product product)

Code example: Binding a product, its lines and a bad value#

C#BindingController.cs Illustrative
// Models/Product.cs
using System.Collections.Generic;


namespace ShopSite.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        public bool IsFeatured { get; set; }          // must NOT be set by customers
        public List<string> Tags { get; set; }
    }
}


// Controllers/BindingController.cs
using System.Linq;
using System.Web.Mvc;
using ShopSite.Models;


namespace ShopSite.Controllers
{
    public class BindingController : Controller
    {
        // POST /Binding/Create/7
        [HttpPost]
        public ActionResult Create(int id, [Bind(Include = "Name,Price,Tags")] Product product)
        {
            var errors = ModelState
                .Where(kv => kv.Value.Errors.Any())
                .Select(kv => 
quot;
{kv.Key}: {kv.Value.Errors[0].ErrorMessage}");
return Content(
quot;id (from route) =
{id}\n" +
quot;Name =
{product.Name}, Price = {product.Price}, IsFeatured = {product.IsFeatured}\n" +
quot;Tags =
{string.Join(" | ", product.Tags ?? new System.Collections.Generic.List<string>())}\n" +
quot;Valid =
{ModelState.IsValid}; " + string.Join("; ", errors),
"text/plain"); } } }
Expected output
POST /Binding/Create/7 name=Gel+Pen&price=20.50&isFeatured=true&tags[0]=blue&tags[1]=smooth
200 OK
id (from route) = 7
Name = Gel Pen, Price = 20.50, IsFeatured = False
Tags = blue | smooth
Valid = True;

POST /Binding/Create/7 Name=Ruler&Price=abc
200 OK
id (from route) = 7
Name = Ruler, Price = 0, IsFeatured = False
Tags =
Valid = False; Price: The value 'abc' is not valid for Price.

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. The id comes from the route segment /7; the other values come from the form body.
  2. Field names are matched case-insensitively, so name and price fill Name and Price, and tags[0], tags[1] become a two-item list.
  3. isFeatured=true was posted, but [Bind(Include = …)] does not list IsFeatured, so it stays false — the over-posting attempt fails.
  4. Price=abc cannot be converted to decimal: Price keeps 0 and ModelState records the error, so IsValid is false.

Real-world use case#

An online shop’s product form was abused by a seller who added IsFeatured=true to the POST with browser tools, putting his products on the home page for free. Switching the action to a ProductEditViewModel without IsFeatured closed the hole.

Common mistakes#

Field names that do not match property names

<input name="productName"> never fills Name. Use Html.TextBoxFor(m => m.Name) so names always match.

Ignoring ModelState after binding

A bad number silently becomes 0. Always check ModelState.IsValid before saving.

Binding straight into EF entities

Every public property of the entity can be set by the client. Bind into a view model and copy the allowed fields.

Best practices#

  • Bind into view models that contain only editable fields
  • Generate inputs with strongly typed helpers so names match
  • Check ModelState.IsValid in every POST action
  • Use TryUpdateModel with an explicit property list for edits

Remember this#

Form → route → query string, matched by name.

Bad conversion = default value + ModelState error.

Over-posting: bind to a view model or use [Bind(Include)].

Practice exercise#

Your task: Write an action that binds a list of OrderLine { Sku, Qty } posted as lines[0].Sku, lines[0].Qty, lines[1].Sku, lines[1].Qty and returns the total quantity.

Need a hint?
Use a parameter List<OrderLine> lines and lines.Sum(l => l.Qty).
Show solution
C# Illustrative
// Controllers/CartController.cs
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;


namespace ShopSite.Controllers
{
    public class OrderLine
    {
        public string Sku { get; set; }
        public int Qty { get; set; }
    }


    public class CartController : Controller
    {
        [HttpPost]
        public ActionResult Total(List<OrderLine> lines)
        {
            var count = lines?.Count ?? 0;
            var total = lines?.Sum(l => l.Qty) ?? 0;
            return Content(
quot;
{count} lines, {total} items", "text/plain");
} } }
Expected output
POST /Cart/Total lines[0].Sku=PEN&lines[0].Qty=3&lines[1].Sku=BOOK&lines[1].Qty=2
200 OK
2 lines, 5 items

Mini quiz#

Model Binding quizMultiple choice Question 1 of 3

A form posts Price=abc to a decimal property. What happens?

Score so far: 0/0

Want more? Practise all ASP.NET MVC MCQs · ASP.NET MVC interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.