ASP.NET Core MVC · Models

Model Binding

When ShopSmart’s search form is submitted, nobody wants to read Request.Query["q"] by hand. Model binding does it for you: it reads the request and fills your action’s parameters — simple values, arrays and whole objects.

Intermediate 7 min Lesson 13 of 42Examples target .NET 9.
Before you start:Controllers
0/42 in ASP.NET Core MVC
On this page · 7 min · Intermediate

In simple words#

Model binding is an assistant who reads the submitted form, query string and route values and fills a C# object for you automatically.

Model binding
HTTP data → action parameters/objects.
Sources
Form, route, query, body, headers.
[FromQuery] etc.
Choose the source explicitly.

Learning objectives#

  • Bind simple values from route, query and form
  • Bind arrays and complex objects
  • Choose sources explicitly with [FromQuery], [FromRoute], [FromForm], [FromHeader] and [FromBody]
  • Prevent over-posting

Explanation#

For each parameter, MVC looks for a value with the same name: form fields first, then route values, then the query string. Names are matched case-insensitively. Types are converted for you — "1000" becomes decimal 1000.

Missing values become the default (null for string or decimal?, 0 for int). Values that cannot be converted are recorded as errors in ModelState rather than throwing.

Repeated keys (?tags=wireless&tags=usb) bind to arrays or lists. Complex types (a class with properties) are built from fields with matching names: Email, Weekly, Topics.

Attributes pick a source explicitly: [FromRoute], [FromQuery], [FromForm], [FromHeader(Name = "Accept-Language")] and [FromBody] for JSON request bodies (one per action).

Over-posting: if the bound class has a property like IsAdmin, an attacker can add IsAdmin=true to the form. Prevent it with input models that only contain allowed fields (best), or [Bind("Name,City")] on the parameter.

Syntax#

C# Illustrative
public IActionResult Products(string q, decimal? maxPrice, string[] tags) => …;      // ?q=…&maxPrice=…&tags=…
public IActionResult Review([FromBody] ReviewInput review) => …;                       // JSON body
public IActionResult Profile([Bind("Name,City")] ProfileInput input) => …;              // only Name and City

Code example: Values from every part of the request#

C#Controllers/SearchController.cs Illustrative
// Program.cs
using System.Net.Http.Json;


var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();                                   // keep the demo output clean
builder.Services.AddControllersWithViews();
var app = builder.Build();
app.MapDefaultControllerRoute();
app.Urls.Add("http://127.0.0.1:5111");
await app.StartAsync();


using var http = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:5111") };
http.DefaultRequestHeaders.Add("Accept-Language", "hi-IN");
async Task Show(string label, HttpResponseMessage response) =>
    Console.WriteLine(
quot;
{label,-10} {await response.Content.ReadAsStringAsync()}");
await Show("query", await http.GetAsync("/Search/Products?q=mouse&maxPrice=1000&tags=wireless&tags=usb")); await Show("route", await http.GetAsync("/Search/Category/5/page/2")); await Show("form", await http.PostAsync("/Search/Subscribe", new FormUrlEncodedContent( [new("Email", "asha@example.com"), new("Weekly", "true"), new("Topics", "deals"), new("Topics", "new")]))); await Show("json", await http.PostAsJsonAsync("/Search/Review", new { productId = 7, stars = 5, text = "Great!" })); await Show("[Bind]", await http.PostAsync("/Search/Profile", new FormUrlEncodedContent( [new("Name", "Asha"), new("City", "Pune"), new("IsAdmin", "true")]))); await app.StopAsync(); // Controllers/SearchController.cs using Microsoft.AspNetCore.Mvc; public class Subscription { public string Email { get; set; } = ""; public bool Weekly { get; set; } public List<string> Topics { get; set; } = []; } public record ReviewInput(int ProductId, int Stars, string Text); public class ProfileInput { public string Name { get; set; } = ""; public string? City { get; set; } public bool IsAdmin { get; set; } // must never be set from a form } public class SearchController : Controller { // GET /Search/Products?q=mouse&maxPrice=1000&tags=wireless&tags=usb public IActionResult Products(string q, decimal? maxPrice, string[] tags, [FromHeader(Name = "Accept-Language")] string? language) => Json(new { q, maxPrice, tags, language }); // GET /Search/Category/5/page/2 [HttpGet("/Search/Category/{id:int}/page/{page:int}")] public IActionResult Category([FromRoute] int id, [FromRoute] int page) => Json(new { id, page }); [HttpPost] // form fields → complex object public IActionResult Subscribe(Subscription form) => Json(form); [HttpPost] // JSON body → record public IActionResult Review([FromBody] ReviewInput review) => Json(review); [HttpPost] // only Name and City may be bound public IActionResult Profile([Bind("Name,City")] ProfileInput input) => Json(input); }
Expected output
query      {"q":"mouse","maxPrice":1000,"tags":["wireless","usb"],"language":"hi-IN"}
route      {"id":5,"page":2}
form       {"email":"asha@example.com","weekly":true,"topics":["deals","new"]}
json       {"productId":7,"stars":5,"text":"Great!"}
[Bind]     {"name":"Asha","city":"Pune","isAdmin":false}

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. Products binds q, maxPrice and the repeated tags from the query string, and language from a header.
  2. Category reads id and page from the attribute route’s segments.
  3. Subscribe builds a Subscription object from form fields, including a List<string> from repeated Topics.
  4. Review reads a JSON body because of [FromBody].
  5. [Bind("Name,City")] ignores the IsAdmin=true sent by the client, so it stays false.

Real-world use case#

ShopSmart’s filters (?category=…&maxPrice=…&brand=…&brand=…), checkout form (address object), and review widget (JSON from JavaScript) all rely on model binding.

Common mistakes#

Binding entities directly

Using the EF entity as the action parameter lets users set any column (price, IsAdmin). Use input models with only editable fields.

Expecting JSON without [FromBody]

In MVC controllers, complex types bind from form/query by default. Add [FromBody] for JSON.

Ignoring conversion errors

?maxPrice=abc does not throw; it leaves maxPrice null and adds a ModelState error. Check ModelState.IsValid.

Best practices#

  • Dedicated input models per form
  • Explicit [From…] attributes when the source matters
  • Nullable types for optional values
  • Always check ModelState.IsValid on POST

Remember this#

Name match: form → route → query (case-insensitive).

Repeated keys → arrays. Matching field names → objects.

Over-posting: use input models or [Bind].

Practice exercise#

Your task: Write an action Filter(ProductFilter filter) for GET /Search/Filter?Category=audio&MinPrice=500&Brands=sony&Brands=jbl where ProductFilter has Category, MinPrice (decimal?) and Brands (List<string>). Return the filter as JSON.

Need a hint?
For GET requests, complex types bind from the query string; mark it [FromQuery] to be explicit.
Show solution
C# Illustrative
// Controllers/SearchController.cs
using Microsoft.AspNetCore.Mvc;


public class ProductFilter
{
    public string? Category { get; set; }
    public decimal? MinPrice { get; set; }
    public List<string> Brands { get; set; } = [];
}


public class SearchController : Controller
{
    public IActionResult Filter([FromQuery] ProductFilter filter) => Json(filter);
}

Mini quiz#

Model Binding quizMultiple choice Question 1 of 3

How do you bind a JSON request body in an MVC controller?

Score so far: 0/0

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

My notes#

Saved in this browser.

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