Route, query, header, body — four places a value could come from. Model binding is what finds the right one.
Every lesson in this Part has quietly leaned on one thing without fully explaining it: a handler parameter just... gets its value. int id shows up populated from the URL. CreateOrderRequest request shows up populated from a JSON body. This isn't magic, and it isn't guesswork — it's a specific, named mechanism called model binding, and understanding it precisely is what separates "my endpoint usually works" from "I know exactly why my endpoint's parameters get the values they get."
This also ties directly back to something you already know: back in lessons 047 and 131, you learned how System.Text.Json deserializes a JSON string into a C# object, from the client's side — calling GetFromJsonAsync against someone else's API. Model binding is largely the same deserialization machinery, running on the server side of that same conversation, turning an incoming JSON request body into your action or handler's parameter.
In this lesson, you'll learn the explicit binding-source attributes — [FromRoute], [FromQuery], [FromBody], [FromHeader], [FromServices] — the general idea of binding-source inference, and an important, honest distinction between how Controllers and Minimal APIs each approach that inference.
Model binding is the process ASP.NET Core uses to take pieces of an incoming HTTP request — parts of the URL, the query string, headers, the request body — and populate them into the parameters of your action or handler method, or into the properties of a class those parameters describe.
Model binding maps data from one or more defined sources in an HTTP request into strongly-typed .NET parameters or objects, using a set of binder components, each responsible for one kind of source. Which source a given parameter binds from is either stated explicitly, using a [From...] attribute, or determined by the framework's own binding-source inference rules when no attribute is present. For request bodies specifically, the binder relies on System.Text.Json — the same serializer from lessons 047 and 131 — to deserialize the incoming JSON into your target type.
An HTTP request scatters its data across genuinely different places: an id might sit in the URL path, a filter or page size in the query string, an API version in a header, and the actual payload in a JSON body. Without model binding, every handler would need to manually reach into HttpContext.Request, pull the right raw string out of the right place, parse it, and handle the case where it's missing or malformed — for every single parameter, on every single endpoint.
Model binding turns that repetitive extraction work into a declaration. You describe the shape you want — a parameter list, or a class — and the framework figures out, source by source, how to fill it in from the raw request. You write int id; you don't write the string-parsing code that turns a URL segment into an int.
GET /api/orders/42?includeItems=true
Header: X-Correlation-Id: 9f2a...
Body: { "note": "Rush this one" }
ROUTE ("42") ─────┐
QUERY ("true") ────┤
HEADER (guid) ─────┼──▶ MODEL BINDING ──▶ your parameters, populated
BODY (JSON) ───────┘
Every one of those four values lands in a different physical part of the HTTP request, yet they can all end up as ordinary, strongly-typed C# values on your method's parameter list — that convergence is exactly what model binding does.
[FromRoute] — FROM A URL SEGMENT[HttpGet("{id:int}")]
public IActionResult GetById([FromRoute] int id) => ...
[FromQuery] — FROM THE QUERY STRING[HttpGet]
public IActionResult GetAll([FromQuery] bool? includeItems) => ...
// GET /api/orders?includeItems=true
[FromBody] — FROM THE REQUEST BODY, DESERIALIZED AS JSON[HttpPost]
public IActionResult Create([FromBody] CreateOrderRequest request) => ...
[FromBody] parameters on the same method isn't supported.[FromHeader] — FROM AN HTTP HEADERpublic IActionResult Get([FromHeader(Name = "X-Correlation-Id")] string correlationId) => ...
[FromServices] — NOT FROM THE REQUEST AT ALLpublic IActionResult Get([FromServices] IOrderService orderService, int id) => ...
[From...] attribute, ASP.NET Core tries to infer the source for you. The broadly reliable, well-established rules of thumb are: a parameter whose name matches a route template segment is bound from the route; a simple type (string, int, bool, Guid, and similar) not matched in the route is typically bound from the query string; a complex, class-typed parameter is typically bound from the request body as JSON; and a parameter typed as a registered DI service is resolved from the container. These general shapes are stable and well documented for both Controllers and Minimal APIs — but the two styles have their own, separate inference implementations, and the precise edge cases (collections in query strings, multiple complex parameters, less common type shapes) can differ between them in ways worth verifying against the current official documentation rather than assuming. When a signature is anything but obviously simple, reach for an explicit [From...] attribute instead of relying on inference — it costs one attribute and removes any doubt.
One action, using all four request-data sources explicitly:
[ApiController]
[Route("api/[controller]")]
public class OrdersController(IOrderService orderService) : ControllerBase
{
[HttpPatch("{id:int}")]
public IActionResult UpdateNote(
[FromRoute] int id,
[FromQuery] bool notify,
[FromHeader(Name = "X-Correlation-Id")] string correlationId,
[FromBody] UpdateNoteRequest request)
{
orderService.UpdateNote(id, request.Note);
if (notify) orderService.NotifyCustomer(id, correlationId);
return NoContent();
}
}
public record UpdateNoteRequest(string Note);
What each attribute tells the framework: id comes out of the {id:int} URL segment. notify comes from ?notify=true in the query string. correlationId comes from the named request header. request comes from deserializing the raw JSON body into an UpdateNoteRequest, using System.Text.Json under the hood — the exact same serializer, and the same case-insensitive property matching, you saw from the consuming side back in lesson 131.
The same idea in a Minimal API handler — same sources, different syntax, and this is exactly where the "separate inference rules" caveat matters most:
app.MapPatch("/api/orders/{id:int}", (
int id, // route
bool notify, // query string
[FromHeader(Name = "X-Correlation-Id")] string correlationId,
UpdateNoteRequest request, // body (complex type)
IOrderService orderService) => // DI
{
orderService.UpdateNote(id, request.Note);
if (notify) orderService.NotifyCustomer(id, correlationId);
return Results.NoContent();
});
Notice that [FromRoute] and [FromQuery] weren't needed here — the general inference rules from the callout above cover this straightforward case identically well for both Controllers and Minimal APIs. But the moment a signature gets less obvious — say, a second complex-typed parameter, or a simple type you actually want bound from the body instead of the query string — that's exactly the point where the two styles' inference rules can diverge, and where reaching for an explicit [From...] attribute (as this lesson's callout recommends) stops being optional caution and starts being the only reliable way to know what will happen.
System.Text.Json for deserialization into the target type — the same deserializer, and largely the same default behaviors (like case-insensitive property matching), covered from the client side in lessons 047 and 131.[Required] or [Range] is a separate step (validation), which for Controllers is what [ApiController]'s automatic 400 (lesson 257) is reacting to — dedicated validation coverage comes later in this Part.Binding answers "what value does this parameter have?" Validation answers "is that value acceptable?" They're separate, sequential steps. A parameter can bind perfectly successfully (the JSON deserialized fine) and still fail validation (a required field was present but empty, or a number was out of range). Don't conflate a binding failure (malformed JSON, wrong type) with a validation failure (well-formed data that just doesn't satisfy your business rules) — they're different problems with different causes.
For simple, obvious signatures, they behave the same way in practice. But they are implemented as two separate systems with their own inference logic, built at different points in ASP.NET Core's history for different handler shapes. Treat the general rules in this lesson as a reliable starting mental model, not as a guarantee that every edge case behaves identically across both styles — verify against current documentation, or just be explicit, when it matters.
[FromBody] parameters on the same action Adding a second complex parameter and marking it [FromBody] too, expecting the framework to split the JSON body across both.
The body is a single stream, consumed once — only one parameter can bind from it. If you need multiple pieces of data, combine them into one request DTO, or move the extras to the route/query/headers.
Adding a class-typed filter/search-options parameter to a GET endpoint and assuming it'll bind neatly from multiple query string keys, without verifying the actual inference behavior for your specific handler style.
For anything beyond the simplest case, be explicit, test the actual request against your endpoint, or structure the parameter as individual simple-typed query parameters instead of one complex object.
Spending time adding [Required] or other validation attributes to fix a 400 that's actually caused by malformed JSON the deserializer couldn't even parse in the first place.
Check the actual error response body — a binding failure and a validation failure look different in the details, and fixing the wrong one wastes time chasing a symptom instead of the cause.
This isn't optional — any action or handler with parameters is using model binding, whether you reach for explicit attributes or not. The real decision is how much you rely on inference versus stating sources explicitly:
[From...] attributes — they're self-documenting and remove any dependency on inference behavior you'd otherwise have to double-check.[FromRoute] / [FromQuery] / [FromBody] / [FromHeader] / [FromServices] — five explicit sources, one of them (services) not really "request data" at all.[FromBody] parameter per action/handler — the body stream is read once.[FromRoute], [FromQuery], [FromBody], [FromHeader], and [FromServices] state the source explicitly; only one parameter per method can bind from the body.System.Text.Json deserialization machinery from lessons 047 and 131 — just running server-side instead of client-side.You've now seen exactly how request data becomes handler parameters. Let's check the details are clear.
1. What is the correct distinction between model binding and model validation?
Correct: B
Why B is correct: Binding is purely about extracting and converting values into your parameter types. Validation is a distinct, later step that checks whether those already-bound values meet your declared rules — a value can bind fine and still fail validation.
Why A is incorrect: They answer different questions ("what's the value?" vs. "is it acceptable?") and run at different points in the pipeline.
Why C is incorrect: Validation cannot run before binding — there's nothing to validate until a value has actually been bound.
Why D is incorrect: Both binding and validation apply to Controllers and, in different forms, to Minimal APIs as well.
Reinforcement: Keeping binding and validation conceptually separate makes debugging a failed request much faster — you know which step to look at first.
2. An action has two parameters, both class-typed, both marked [FromBody]. What happens?
Correct: B
Why B is correct: The request body is one stream that gets consumed once during deserialization — there's no mechanism to split or duplicate it across two separately-bound parameters, so only a single [FromBody] parameter per action/handler is supported.
Why A, C are incorrect: Neither splitting nor duplicating the body is how the binder behaves — the body maps to exactly one target.
Why D is incorrect: There's no silent fallback to [FromQuery] — this is a genuine configuration problem to avoid, not something the framework quietly papers over.
Reinforcement: If you need multiple pieces of body data, combine them into a single request DTO instead.
3. Why does this lesson recommend using explicit [From...] attributes for any signature that isn't obviously simple, rather than relying on inference?
Correct: B
Why B is correct: This is exactly the honest, conservative guidance the lesson gives — the general inference rules are reliable for simple cases, but Controllers and Minimal APIs are separate implementations, so anything beyond the obvious case is safer stated explicitly rather than assumed.
Why A is incorrect: Inference is a current, actively used, documented feature — nothing suggests it's being removed.
Why C is incorrect: The recommendation is about correctness and clarity, not performance.
Why D is incorrect: Inference applies across HTTP verbs generally — this isn't a GET-only mechanism.
Reinforcement: When in doubt about which rule applies, an explicit attribute costs almost nothing and removes the doubt entirely.
4. A handler parameter is typed IOrderService, a type registered in the DI container. Which binding source does this represent?
Correct: B
Why B is correct: A parameter typed as a registered service is resolved from the DI container — the same mechanism from lesson 124 — rather than extracted from any part of the HTTP request. [FromServices] is how you'd state this explicitly, though it's often inferred automatically for recognized service types.
Why A, C are incorrect: Those sources apply to request data — an IOrderService instance isn't request data; it's application infrastructure supplied by the container.
Why D is incorrect: This is a completely standard, working pattern — it's exactly what powers DI directly into Minimal API and Controller parameters throughout this Part.
Reinforcement: Not every parameter comes from the request — service-typed parameters are a distinct, DI-driven category of "binding."
5. A request body deserializes successfully into a CreateOrderRequest, but its CustomerEmail property — marked [Required] — was sent as an empty string. What kind of failure is this?
Correct: B
Why B is correct: The JSON was well-formed and successfully deserialized — binding did its job correctly. The empty string simply fails to satisfy the [Required] rule, which is checked in the separate validation step that runs after binding.
Why A is incorrect: A binding failure would mean the value couldn't be extracted or converted at all (e.g., malformed JSON) — here, an empty string bound just fine as a value.
Why C, D are incorrect: Routing already succeeded (the correct action was reached), and no DI-resolved service is involved in this scenario at all.
Reinforcement: This is exactly the binding-vs-validation distinction from the lesson — don't debug a validation problem as if it were a binding problem.
You've now completed the full arc of this Part — from Kestrel accepting a connection, all the way down to exactly how a single parameter gets its value. That's the whole ASP.NET Core request pipeline, in your own words.
dotnetmadeeasy.com — Learn C# and .NET, the right way.