For the next part of my ASP.NET Core Demystified series, we're sticking with MVC and explaining how the model binding system works. As with all my Demystified series posts, there's a sample project over on GitHub which contains the sample code used here. Check it out!
What is Model Binding?
Model Binding is the process by which ASP.NET Core MVC takes an HTTP request and "binds" pieces of that request, as well as other data sources, to inputs (e.g. parameters) on a controller action. For example, let's see a simple URL:
We know from simple inspection that this URL is intending to retrieve the blog post with ID 14 from the /blog section of mysite.com. But ASP.NET has no way of knowing what our intentions are, and so it uses model binding to parse the URL. By default, ASP.NET Core MVC's uses the following route to map incoming URLs:
{controller}/{action}/{id?}
And so, the above url would be mapped to the BlogController's Posts() action, passing a parameter with name id and value 14.
Data Sources
ASP.NET Core MVC uses three primary data sources to map HTTP requests to actions, in the following order:
- Form values: Values in the FORM in HTTP POST requests.
- Route values: Values provided by the Routing system.
- Query string: Values found in the URL's query string (e.g. after the ? character).
In the earlier example, the default route was looking for a parameter id, which was mapped from the Routing system (because the default route was defined as {controller}/{action}/{id?}). Had there been a FORM posted, and had that form had a parameter called id, the value would have been mapped from there.
Binding Complex Types
So far, our examples have used simple (or "primitive") types, e.g. int
, string
, etc. But what about more complex types?
ASP.NET Core MVC will use both reflection and recursion to bind complex types. For example, say we have the following trivial class:
public class User
{
public string FirstName { get; set; }
}
And a corresponding URL:
The model binding system will use reflection to discover that the User
class has a FirstName
property, and bind to it the firstName parameter's value "Hariya" found in the query string. In this way default model binding is both powerful and flexible enough to suit most needs. If you need something more, though, you can always use attributes.
Custom Model Binding with Attributes
In addition to the default binding methodology, ASP.NET Core MVC offers a more customized way to accomplish model binding by the use of attributes. Here's a few of the attributes offered by ASP.NET Core MVC:
[BindRequired]
: 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.[FromServices]
: Binds the parameter from services provided by dependency injection.[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.
You can also perform completely custom binding using your own binder. The official ASP.NET Core docs have some good examples of how to do this.
Default Model Binding Examples
Now that we've seen how we can do model binding in ASP.NET Core MVC, we're going to build a very simple user creation application to demonstrate some of these ideas. Let's start with a very simple User
class:
public class User
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DateOfBirth { get; set; }
public string Controller { get; set; }
}
Let's also create a controller:
public class DefaultController : Controller
{
[HttpGet]
public IActionResult Index(string message) //Bound from Query String
{
ViewData["message"] = message;
return View();
}
[HttpGet]
public IActionResult New(string controller) //Bound from Routing
{
User user = new User();
user.Controller = controller;
return View(user);
}
[HttpPost]
public IActionResult New(User user) //Bound from FORM
{
var message = user.FirstName + " " + user.LastName + ", Date of Birth: " + user.DateOfBirth.ToString("yyyy-MM-dd") + ", ID: " + user.ID + ", Controller: " + user.Controller;
return RedirectToAction("index", new { message = message });
}
}
The three actions demonstrate three different ways of binding data to our model, of which the first two examples are relatively straightforward. In the Index()
action, the message parameter is bound from the URL's query string. In the New(string controller)
action, the controller parameter is bound from the Routing system unless it exists in the POST form, in which case the POST form value of controller is used.
The third example is a bit more complex (pun very much intended). Since we're using default binding, the complex type User
will have its properties from the HTTP request's FORM being posted. However, remember that User
contains a property Controller
. That property will be bound from the Routing system if and only if the POSTed form doesn't also contain a property called controller.
Custom Model Binding Examples
Now let's see how we might utilize more custom binding with attributes. Here's a slightly modified version of the User
class, now called BoundUser
.
public class BoundUser
{
[BindRequired]
public int ID { get; set; }
[BindRequired]
public string FirstName { get; set; }
[BindNever]
public string LastName { get; set; }
[BindRequired]
public DateTime DateOfBirth { get; set; }
[BindRequired]
[FromRoute]
public string Controller { get; set; }
}
Let's also modify the controller to match; it will now use attributes for binding:
public class BindingsController : Controller
{
[HttpGet]
public IActionResult Index([FromQuery] string message)
{
ViewData["message"] = message;
return View();
}
[HttpGet]
public IActionResult New([FromRoute] string controller)
{
BoundUser user = new BoundUser();
user.Controller = controller;
return View(user);
}
[HttpPost]
public IActionResult New([FromForm] BoundUser user)
{
var message = user.FirstName + " " + user.LastName + ", Date of Birth: " + user.DateOfBirth.ToString("yyyy-MM-dd") + ", ID: " + user.ID + ", Controller: " + user.Controller;
return RedirectToAction("index", new { message = message });
}
}
This may look the same as the default model binding examples, but there are some subtle differences. To start, the Index()
action will now only bind from the query string, meaning that if a parameter message does not exist, ASP.NET Core MVC will use the default value for the type (since the type is string
, the default value is null
).
Further, in the Default examples the value controller in the New()
GET action could theoretically be mapped from a POSTed form value (if, say, we didn't use the [HttpGet]
attribute). Whereas, in this example, the controller
parameter can only ever be mapped from the Routing system, and if it doesn't exist Core MVC will use the type's default value.
Also, one last trick:
[BindNever]
public string LastName { get; set; }
[BindNever]
will, unsurprisingly, cause the LastName property to never be bound. This is almost certainly not what we want. Code reviewers, better keep your eyes open!
Summary
Model Binding in ASP.NET Core MVC is accomplished through two methods: default binding and custom binding. Both can handle simple and complex types, and custom binding allows you to specify which properties get bound, and from what data sources.
Take a look at the sample project on GitHub to see working versions of these demos. Also, if you see anything I missed, or have some cool stories to share, sound off in the comments!
Happy Coding!