Section 3 of 7
Model Binding
🎯 What You'll Learn
- What model binding is
- Binding sources ([FromBody], [FromRoute], etc.)
- Automatic binding inference
- Complex type binding
- Custom model binding
- Best practices
What is Model Binding?
Model binding automatically maps HTTP request data to action method parameters.
Model Binding Flow
Text
HTTP Request
↓
Model Binding
↓
Action Method Parameters
Binding Sources
| Attribute | Source | Example |
|---|---|---|
[FromRoute] |
URL path | /api/products/123 |
[FromQuery] |
Query string | ?page=1&pageSize=20 |
[FromBody] |
Request body | JSON/XML payload |
[FromHeader] |
HTTP headers | X-API-Key: abc123 |
[FromForm] |
Form data | HTML form submission |
[FromServices] |
DI container | Injected services |
[FromRoute] - URL Parameters
Route Parameters
C#
// GET /api/products/123
[HttpGet("{id}")]
public ActionResult<Product> GetProduct([FromRoute] int id)
{
// id = 123
}
// Multiple route parameters
// GET /api/customers/456/orders/789
[HttpGet("{customerId}/orders/{orderId}")]
public ActionResult GetOrder(
[FromRoute] int customerId,
[FromRoute] int orderId)
{
// customerId = 456, orderId = 789
}
[FromQuery] - Query String
Query Parameters
C#
// GET /api/products?page=1&pageSize=20&category=electronics
[HttpGet]
public ActionResult GetProducts(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
[FromQuery] string? category = null)
{
// page = 1, pageSize = 20, category = "electronics"
}
// Complex query object
public class ProductFilter
{
public string? Category { get; set; }
public decimal? MinPrice { get; set; }
public decimal? MaxPrice { get; set; }
public bool? InStock { get; set; }
}
// GET /api/products?category=electronics&minPrice=100&maxPrice=500
[HttpGet]
public ActionResult GetProducts([FromQuery] ProductFilter filter)
{
// filter.Category = "electronics"
// filter.MinPrice = 100
// filter.MaxPrice = 500
}
[FromBody] - Request Body
JSON Body
C#
// POST /api/products
// Body: { "name": "Laptop", "price": 999.99 }
[HttpPost]
public ActionResult CreateProduct([FromBody] CreateProductDto dto)
{
// dto.Name = "Laptop"
// dto.Price = 999.99
}
public class CreateProductDto
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public string? Description { get; set; }
}
⚠️ One [FromBody] Per Action
You can only have one [FromBody] parameter per action.
The entire request body is deserialized into that parameter.
[FromHeader] - HTTP Headers
Header Parameters
C#
// GET /api/products
// Headers: X-API-Key: abc123
[HttpGet]
public ActionResult GetProducts(
[FromHeader(Name = "X-API-Key")] string apiKey)
{
// apiKey = "abc123"
}
// Multiple headers
[HttpGet]
public ActionResult GetProducts(
[FromHeader(Name = "X-API-Key")] string apiKey,
[FromHeader(Name = "X-Correlation-Id")] string? correlationId = null)
{
}
Automatic Binding Inference
With [ApiController], ASP.NET Core automatically infers binding sources:
| Parameter Type | Inferred Source |
|---|---|
| Route parameter | [FromRoute] |
| Simple type (int, string, etc.) | [FromQuery] |
| Complex type | [FromBody] |
| IFormFile, IFormFileCollection | [FromForm] |
Automatic Inference
C#
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
// GET /api/products/123?includeDetails=true
[HttpGet("{id}")]
public ActionResult GetProduct(
int id, // [FromRoute] inferred
bool includeDetails = false) // [FromQuery] inferred
{
}
// POST /api/products
[HttpPost]
public ActionResult CreateProduct(
CreateProductDto dto) // [FromBody] inferred (complex type)
{
}
}
Combining Multiple Sources
Multiple Binding Sources
C#
// PUT /api/products/123?notify=true
// Body: { "name": "Updated Laptop", "price": 1099.99 }
[HttpPut("{id}")]
public ActionResult UpdateProduct(
[FromRoute] int id, // From URL
[FromBody] UpdateProductDto dto, // From body
[FromQuery] bool notify = false) // From query
{
// id = 123, dto = {...}, notify = true
}
Complete InvenTrack Example
Controllers/ProductsController.cs
C#
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
// GET /api/products?page=1&pageSize=20&category=electronics
[HttpGet]
public ActionResult GetProducts([FromQuery] ProductFilter filter)
{
return Ok(new { filter });
}
// GET /api/products/123
[HttpGet("{id}")]
public ActionResult GetProduct(int id) // [FromRoute] inferred
{
return Ok(new { id });
}
// POST /api/products
// Body: { "name": "Laptop", "price": 999.99 }
[HttpPost]
public ActionResult CreateProduct(
CreateProductDto dto) // [FromBody] inferred
{
return CreatedAtAction(nameof(GetProduct), new { id = 123 }, dto);
}
// PUT /api/products/123?notify=true
// Body: { "name": "Updated", "price": 1099.99 }
[HttpPut("{id}")]
public ActionResult UpdateProduct(
int id, // [FromRoute] inferred
UpdateProductDto dto, // [FromBody] inferred
bool notify = false) // [FromQuery] inferred
{
return NoContent();
}
// DELETE /api/products/123
// Headers: X-API-Key: abc123
[HttpDelete("{id}")]
public ActionResult DeleteProduct(
int id,
[FromHeader(Name = "X-API-Key")] string apiKey)
{
return NoContent();
}
}
public class ProductFilter
{
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
public string? Category { get; set; }
public decimal? MinPrice { get; set; }
public decimal? MaxPrice { get; set; }
}
public class CreateProductDto
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public string? Description { get; set; }
}
public class UpdateProductDto
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
Best Practices
- Be explicit: Use binding attributes for clarity
- One [FromBody]: Only one per action
- Use DTOs: Don't bind directly to domain models
- Optional parameters: Use nullable types or default values
- Complex queries: Use query objects instead of many parameters
- Validation: Always validate bound data
- Naming: Match property names to query/form field names
Key Takeaways
- Model binding: Maps request data to parameters
- [FromRoute]: URL path parameters
- [FromQuery]: Query string parameters
- [FromBody]: Request body (JSON/XML)
- [FromHeader]: HTTP headers
- [FromForm]: Form data
- Automatic inference: [ApiController] infers sources
- Complex types: Automatically bound from body
- Simple types: Automatically bound from query
🎯 Next Steps
You now understand model binding! In the next section, we'll explore Validation—how to validate bound data using Data Annotations, FluentValidation, and custom validators.