Author : HASSAN MD TAREQ | Updated : 2020/06/23

Model binding

Overview

Image courtesy: https://www.c-sharpcorner.com/article/introduction-to-asp-net-mvc-model-binding/

Model binding overview

Binding types:

  • Binding primitive data types: ModelBinder maps premitive types (i.e. int) to controller action (method) parameter(s)
  • Binding complex data types: ModelBinder uses reflections to get the public properties and then binds to each of parameters in turn

Binding Source

By default, model binding gets data in the form of key-value pairs from the following sources in an HTTP request (sources are scanned in the order indicated in the list):

Binding targets

Model binding tries to find values for the following kinds of targets:

  • Parameters of the controller action method that a request is routed to.
  • Parameters of the Razor Pages handler method that a request is routed to.
  • Public properties of a controller or PageModel class, if specified by attributes.

By default, a model state error isn’t created if no value is found for a model property. The property is set to null or a default value:

  • Nullable simple types are set to null.
  • Non-nullable value types are set to default(T). For example, a parameter int id is set to 0.
  • For complex Types, model binding creates an instance by using the default constructor, without setting properties.
  • Arrays are set to Array.Empty(), except that byte[] arrays are set to null.

[BindRequired] attribute

  • Use this attribute/annotation if model state should be invalidated when nothing is found in form fields for a model property
  • [BindRequired] behavior applies to model binding from posted form data, not to JSON or XML data in a request body
  • Request body data is handled by input formatters

Type conversion errors
If a source is found but can’t be converted into the target type, model state is flagged as invalid and:

  • Web App controller: property is set to null or a default value
  • API controller that has the [ApiController] attribute: invalid model state results in an automatic HTTP 400 response
  • if you don’t want type conversion errors to result in model state errors: make the model property a string and perform the data conversion manually

Common binding annotations

  • If target is ‘all properties of model’ then just using Action(Foo fooModel) will work fine
  • [Bind]:
  • [BindRequired]:
    • Can only be applied to model properties, not to method parameters
    • If binding cannot happen, this attribute adds a ModelState error
  • [BindNever]: Tells the model binder to ignore this parameter
  • [FromHeader]: Forces binding from the HTTP request header
  • [FromQuery]: Forces binding from the URL’s query string
  • [FromRoute]: Forces binding from values provided by Routing
  • [FromForm]: Forces binding from values in the FORM
  • [FromBody]: Forces binding from values in the body of the HTTP request
  • [FromServices]: Binds the parameter from services provided by dependency injection

Content negotiation

Disclaimer: the following explanation to make it easy to understand model binding (might not be accurate w.r.t.)

  • Model binder will try to bind source data (incoming data) to target based on incoming Content-Type: ...
  • If content type is not found (or model binder could not figure out content type) => fall backs to default based on controller:
    • WebApp: form
    • API: json
  • IActionResult Foo(Hover hover)
    • all properties of Hover will be populated
    • if binding is missing in source => depending on the data annotation applied to properties:
      • either model state will be invalid
      • or properties will be null
  • IActionResult Foo([FromForm]Hover hover):
    • data source: form (enfocing source has to be form)
    • if source is not form posted data => error (http status code 415, unsupported)
  • IActionResult Foo([FromBody]Hover hover):
    • data source: json (enfocing source has to be json => API controller)
    • if source is not json => error (http status code 415, unsupported)
  • IActionResult Foo([Bind(“X,Y,Z”)]Hover hover): Hover has ‘n’ properties, but binder will bind only X, Y, Z

Data annotations

Model Binding example

Following example is for AspNetCore MVC (WebApp)

HoverInputModel.cs

public class HoverInputModel
{
  public int FirstName { get; set; }
  public int LastName { get; set; }
  public string FullName { get; set; }
  public string Email { get; set; }
  // will be null (because there will be no input field in the form for this prop)
  public string Bar { get; set; }
}

HoverController.cs

public class HoverController : Controller
{

    // ... ... ...

	// GET: Hover/Create
	public IActionResult Create()
	{
		return View();
	}
	

	// POST: Hover/Create
	[HttpPost]
	public async Task<IActionResult> Create([FromFrom]HoverInputModel hoverInputModel)
	{
		// check model state
		
		// process data
	}
	
	// ... ... ...
}

Create.cshtml

@model ModelBindingAndFormValidation.Models.HoverInputModel

@{
    ViewData["Title"] = "Create";
}

<h1>Create Hover Info</h1>

<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Id" class="control-label"></label>
                <input asp-for="Id" class="form-control" />
                <span asp-validation-for="Id" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="FirstName" class="control-label"></label>
                <input asp-for="FirstName" class="form-control" />
                <span asp-validation-for="FirstName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="LastName" class="control-label"></label>
                <input asp-for="LastName" class="form-control" />
                <span asp-validation-for="LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="FullName" class="control-label"></label>
                <input asp-for="FullName" class="form-control" />
                <span asp-validation-for="FullName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Email" class="control-label"></label>
                <input asp-for="Email" class="form-control" />
                <span asp-validation-for="Email" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

When form is posted to server, form data will be bound to HoverInputModel.Props

Notes:

  • asp-for is special tag helper that does not require use of Model i.e. we can use Name property directly instead of Model.Name
  • for other tag helpers, you have to use Model.Xxx

Model Validation

HoverInputModel.cs

public class HoverInputModel
{

	[Required]
	[Display(Name = "First Name")]
	[Range(5, 50)]
	public int FirstName { get; set; }


	[Required]
	[Display(Name = "Last Name")]
	[StringLength(50, MinimumLength = 5, ErrorMessage = "Last name must have at least 5 characters")]
	public int LastName { get; set; }


	[Required(ErrorMessage = "{0} is required")]
	[StringLength(100, MinimumLength = 10, ErrorMessage = "Name must have at least 10 characters")]
	[RegularExpression(@"^[a-zA-Z\s]+$", ErrorMessage = "Please, use letters in the name. Digits are not allowed.")]
	[Display(Name = "Full Name")]
	public string FullName { get; set; }


	[Required(ErrorMessage = "{0} is required")]
	[StringLength(100, MinimumLength = 1, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.")]
	public string Email { get; set; }


	// will be null
	public string Bar { get; set; }
}

HoverController.cs

public class HoverController : Controller
{

    // ... ... ...

	// GET: Hover/Create
	public IActionResult Create()
	{
		return View();
	}
	

	// POST: Hover/Create
	// To protect from overposting attacks, enable the specific properties you want to bind to, for 
	// more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
	[HttpPost]
	[ValidateAntiForgeryToken]
	public async Task<IActionResult> Create([FromFrom]HoverInputModel hoverInputModel)
	{
		if (ModelState.IsValid)
		{
			// perform EF core task here i.e. _context.Add(hoverInputModel);await _context.SaveChangesAsync();
			
			return RedirectToAction(nameof(Index));
		}
		
		return View(hoverInputModel);
	}
	
	// ... ... ...
}

Excluding model property

MVC

[Bind(nameof(FirstName), nameof(LastName), nameof(FullName))]
public class HoverInputModel
{
	// data annotation here
	public string FirstName { get; set; }


	// data annotation here
	public string LastName { get; set; }


	// data annotation here
	public string FullName { get; set; }


	// ignored (value will be null)
	public string Bar { get; set; }
}

// OR
public void SubmitData([Bind(include= ...)]HoverInputModel hoverModel)
{
   //... ... ...
}

API

public class MyModel
{
    [JsonIgnore]
    public string Name { get; set; }
	
    // ... ... ...
}

// Action:
[HttpPost]
public IActionResult Student([FromBody]MyModel model)

Prefixed form element

Annotation: [Bind(Prefix= "Foo")]DataModel model

public void SubmitData([Bind(Prefix= "Address")]AddressModel addressOnly)
{
   //even if id property was provided, model binder will ignore it
}

Custom validation attribute

See: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation#custom-attributes

Model Binding and Validation for Web API

  • Model Binding Source: request body (json payload)
  • When ModelState.IsValid evaluates to false in web API controllers using the [ApiController] attribute, an automatic HTTP 400 response containing issue details is returned. For more information, see Automatic HTTP 400 responses

Hover.cs

public class Hover
{
	// properties with data annotations
}

HoverController.cs

[ApiController]
public class HoverController : ControllerBase
{

    [HttpPost]
    public IActionResult AddFoo([FromBody]Foo foo) // JSON -> POCO
    {
		// following code is unnecessary in web api because Model validation errors automatically trigger an HTTP 400 response
		//if(!ModelState.Isvalid){
		//	return View(foo);
		//}
	
		// EF Core
		_fooDbContext.Add(foo);
       
		return Created(foo);
    }
}