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.
On this page · 7 min · Intermediate
On this page
- In simple words
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 7 min
- Difficulty
- Intermediate
- Updated
- Oct 29, 2026
Related tutorials
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#
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#
// 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);
}
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#
- Products binds q, maxPrice and the repeated tags from the query string, and language from a header.
- Category reads id and page from the attribute route’s segments.
- Subscribe builds a Subscription object from form fields, including a List<string> from repeated Topics.
- Review reads a JSON body because of [FromBody].
- [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#
Using the EF entity as the action parameter lets users set any column (price, IsAdmin). Use input models with only editable fields.
In MVC controllers, complex types bind from form/query by default. Add [FromBody] for JSON.
?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?
Show solution
// 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#
How do you bind a JSON request body in an MVC controller?
Want more? Practise all ASP.NET Core MVC MCQs · ASP.NET Core MVC interview questions
My notes#
Saved in this browser.