Core API Model Binding
When a client sends an HTTP request to the BookStore API, the data can come from different parts of the request — the URL, the query string, the request body, or request headers. Model binding is the automatic process ASP.NET Core uses to extract that data and map it to the parameters of an action method.
What Is Model Binding?
Without model binding, a developer would need to manually read the URL, parse the query string, and deserialize the JSON body on every request. Model binding does all of this automatically.
HTTP Request
URL: GET /api/books/5?category=Technology
Body: (empty for GET)
|
v
[ Model Binding ]
- Reads "5" from the URL → maps to int id
- Reads "Technology" from query string → maps to string category
|
v
GetById(int id, string category) is called with id=5, category="Technology"
Binding Sources
ASP.NET Core can extract data from four sources in a request. Each source has a corresponding attribute to tell the framework exactly where to look.
| Attribute | Source | Example |
|---|---|---|
[FromRoute] | URL path | /api/books/5 → id = 5 |
[FromQuery] | Query string | ?category=Tech → category = "Tech" |
[FromBody] | Request body (JSON) | JSON object → Book object |
[FromHeader] | Request header | X-Api-Key: abc123 |
FromRoute – Binding from the URL
[FromRoute] extracts a value from the URL path. This is the most common way to pass an identifier for a specific resource.
// GET /api/books/3
[HttpGet("{id}")]
public IActionResult GetById([FromRoute] int id)
{
var book = _books.FirstOrDefault(b => b.Id == id);
if (book == null) return NotFound();
return Ok(book);
}
Note: When parameter names match the route template, [FromRoute] can be omitted. ASP.NET Core infers the binding source automatically.
FromQuery – Binding from the Query String
[FromQuery] extracts values from the query string — the part of the URL after ?. This is ideal for optional filters, sorting options, and pagination.
// GET /api/books?category=Technology&isAvailable=true&page=1&pageSize=10
[HttpGet]
public IActionResult GetAll(
[FromQuery] string? category,
[FromQuery] bool? isAvailable,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10)
{
var books = _books.AsQueryable();
if (!string.IsNullOrEmpty(category))
books = books.Where(b => b.Category == category);
if (isAvailable.HasValue)
books = books.Where(b => b.IsAvailable == isAvailable.Value);
var result = books
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
return Ok(result);
}
Test URLs:
GET /api/books → All books, page 1
GET /api/books?category=Technology → Only Technology books
GET /api/books?isAvailable=true&page=2 → Available books, page 2
GET /api/books?pageSize=5 → First 5 books only
FromBody – Binding from the Request Body
[FromBody] reads the JSON body of a POST or PUT request and deserializes it into a C# object. This is how data is sent to the API when creating or updating a resource.
// POST /api/books
[HttpPost]
public IActionResult Create([FromBody] Book book)
{
book.Id = _books.Any() ? _books.Max(b => b.Id) + 1 : 1;
book.CreatedDate = DateTime.Now;
_books.Add(book);
return CreatedAtAction(nameof(GetById), new { id = book.Id }, book);
}
The client sends:
POST /api/books
Content-Type: application/json
{
"title": "Clean Architecture",
"author": "Robert C. Martin",
"price": 39.99,
"category": "Technology",
"isAvailable": true
}
ASP.NET Core reads the JSON body and automatically maps it to a Book object. The Id and CreatedDate are set inside the action method.
Important: Only One [FromBody] Per Action
An action method can only have one [FromBody] parameter. This is because the request body can only be read once. Combining [FromBody] with [FromRoute] or [FromQuery] in the same action is perfectly fine.
// Valid: one [FromBody] and one [FromRoute]
[HttpPut("{id}")]
public IActionResult Update([FromRoute] int id, [FromBody] Book updatedBook) { ... }
FromHeader – Binding from Request Headers
[FromHeader] reads values from HTTP request headers. This is commonly used for reading API keys or custom tracking headers.
[HttpGet]
public IActionResult GetAll([FromHeader(Name = "X-Api-Version")] string? apiVersion)
{
// apiVersion contains the value from the X-Api-Version header, if present
return Ok(_books);
}
Request with custom header:
GET /api/books
X-Api-Version: 2.0
Mixing Binding Sources
A single action method can bind data from multiple sources at the same time:
// PUT /api/books/5?notify=true
// Body: { "title": "New Title", "price": 25.99, ... }
[HttpPut("{id}")]
public IActionResult Update(
[FromRoute] int id,
[FromBody] Book updatedBook,
[FromQuery] bool notify = false)
{
var book = _books.FirstOrDefault(b => b.Id == id);
if (book == null) return NotFound();
book.Title = updatedBook.Title;
book.Price = updatedBook.Price;
if (notify)
{
// Trigger some notification logic
}
return NoContent();
}
Route source: id = 5 (from /api/books/5)
Body source: updatedBook (from JSON request body)
Query source: notify = true (from ?notify=true)
Automatic Binding Inference with [ApiController]
When a controller has the [ApiController] attribute, ASP.NET Core applies these default binding rules automatically:
| Parameter Type | Default Binding Source |
|---|---|
| Simple types (int, string, bool) | Route → then Query String |
| Complex types (Book, DTO objects) | Request Body (JSON) |
| IFormFile | Form data |
| CancellationToken | Special ASP.NET Core service |
Because of this, explicit binding attributes can often be omitted, but adding them makes the code intention clear and prevents accidental binding from the wrong source.
Key Points
- Model binding is the automatic process that extracts HTTP request data and maps it to action method parameters.
[FromRoute]reads from the URL path,[FromQuery]from the query string,[FromBody]from the JSON body, and[FromHeader]from request headers.- Only one
[FromBody]parameter is allowed per action method. - The
[ApiController]attribute applies smart default binding rules that often eliminate the need to write binding attributes explicitly. - Multiple binding sources can be combined in a single action method.
