Model Binding in ASP.NET Core MVC and Razor Pages

Model binding is the process of taking incoming request data (like form posts, route data, query strings, etc.) and creating a strongly-typed .NET object from it. This object is typically passed as a parameter to an action method in an MVC controller or a Razor Page handler method.

How Model Binding Works

When a request arrives, ASP.NET Core's model binder attempts to match incoming data from various sources to the properties of the target model object. The default model binder supports binding from:

The model binder looks for properties on the target model that have names matching the names of the incoming data. For example, if you have a model with a property named UserName, the model binder will look for a form field, query string parameter, or route parameter named UserName.

Binding Sources

You can explicitly specify which sources the model binder should use for a particular parameter using the [Bind{Source}] attributes.

Tip: When using [FromBody] for complex types, it's common to bind a single parameter representing the entire request payload, often a JSON object.

Example: MVC Controller Action

Consider an MVC controller:

Controller.cs

using Microsoft.AspNetCore.Mvc;

public class ProductsController : Controller
{
    public IActionResult Index([FromQuery] int categoryId)
    {
        // categoryId will be bound from the query string, e.g., /Products?categoryId=123
        return View();
    }

    [HttpPost]
    public IActionResult Create([FromBody] Product product)
    {
        // 'product' will be bound from the JSON request body
        // Example JSON body: { "Name": "Gadget", "Price": 19.99 }
        if (ModelState.IsValid)
        {
            // Save product to database...
            return Ok(product);
        }
        return BadRequest(ModelState);
    }

    public IActionResult Edit(int id, [FromForm] ProductDetails details)
    {
        // 'id' is from the route, 'details' is from form values
        // Example URL: /Products/Edit/5
        // Example Form Data: Name=Super Gadget&Description=A really cool item
        return View();
    }
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

public class ProductDetails
{
    public string Name { get; set; }
    public string Description { get; set; }
}
            

Example: Razor Page Handler

In Razor Pages, model binding works similarly for handler methods:

MyPage.cshtml.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

public class MyPageModel : PageModel
{
    public string Message { get; set; }

    public void OnGet(string searchString)
    {
        // searchString will be bound from the query string, e.g., /MyPage?searchString=books
        Message = $"You searched for: {searchString}";
    }

    public IActionResult OnPost([FromBody] Order order)
    {
        // 'order' will be bound from the JSON request body
        if (ModelState.IsValid)
        {
            // Process order...
            return new JsonResult(new { success = true, orderDetails = order });
        }
        return BadRequest(ModelState);
    }
}

public class Order
{
    public int OrderId { get; set; }
    public List<string> Items { get; set; }
}
            

Complex Types and Collections

Model binding supports complex types (objects with properties) and collections (arrays, lists).

Complex Types

When binding a complex type, the model binder recursively binds its properties. For instance, if you have a Person model with Address property (which is another complex type), the binder will look for data like Address.Street and Address.City.

Collections

For collections, data can be provided using indexed names. For example, for a list of strings named Tags, you might see form data like Tags[0]=asp.net, Tags[1]=core.

Important: Ensure that your complex type properties and collection elements have appropriate public setters for the model binder to work correctly.

Model Validation

Model binding is often followed by model validation. You can decorate your model classes with data annotations (e.g., [Required], [StringLength], [Range]) to define validation rules.

ASP.NET Core automatically integrates with these attributes. If validation fails, the ModelState.IsValid property will be false, and validation errors are accessible via ModelState.

Example: Validating a Model

Product.cs (with validation)

using System.ComponentModel.DataAnnotations;

public class Product
{
    public int Id { get; set; }

    [Required(ErrorMessage = "Product name is required.")]
    [StringLength(100, ErrorMessage = "Product name cannot exceed 100 characters.")]
    public string Name { get; set; }

    [Range(0.01, 1000.00, ErrorMessage = "Price must be between 0.01 and 1000.00.")]
    public decimal Price { get; set; }
}
            

Customizing Model Binding

For advanced scenarios, you can create custom model binders or model binder providers.

These are typically registered in the ConfigureServices method of your Startup.cs file.

Further Reading