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.