I was catching up on the latest ASP.NET Community Standup the other day when a question popped up about Model Binding that I hadn't previously picked up on (you can see the question around 46:30). It pointed out that in ASP.NET Core (the new name for ASP.NET 5), you can no longer simply post JSON data to an MVC controller and have it bound automatically, which you could previously do in ASP.NET 4/MVC 5.
In this post, I am going to show what to do if you are converting a project to ASP.NET Core and you discover your JSON POST
s aren't working. I'll demonstrate the differences between MVC 5 model binding and MVC Core model binding, highlighting the differences between the two, and how to setup your controllers for your project, depending on the data you expect.
TL;DR: Add the
[FromBody]
attribute to the parameter in your ASP.NET Core controller action Note, if you're using ASP.NET Core 2.1, you can also use the[ApiController]
attribute to automatically infer the[FromBody]
binding source for your complex action method parameters. See the documentation for details.
Where did my data go?
Imagine you have created a shiny new ASP.NET core project which you are using to rewrite an existing ASP.NET 4 app (only for sensible reasons of course!) You copy and paste your old WebApi controller in to your .NET Core Controller, clean up the namespaces, test out the GET
action and all seems to be working well.
Note: In ASP.NET 4, although the MVC and WebApi pipelines behave very similarly, they are completely separate. Therefore you have separate
ApiController
andController
classes for WebApi and Mvc respectively (and all the associated namespace confusion). In ASP.NET Core, the pipelines have all been merged and there is only the singleController
class.
As your GET
request is working, you know the majority of your pipeline, for example routing, is probably configured correctly. You even submit a test form, which sends a POST
to the controller and receives the JSON values it sent back. All looking good.
As the final piece of the puzzle, you test sending an AJAX POST
with the data as JSON, and it all falls apart - you receive a 200 OK
, but all the properties on your object are empty. But why?
What is Model Binding?
Before we can go into details of what is happening here, we need to have a basic understanding of model binding. Model binding is the process whereby the MVC or WebApi pipeline takes the raw HTTP request and converts that into the arguments for an action method invocation on a controller.
So for example, consider the following WebApi controller and Person
class:
public class PersonController : ApiController
{
[HttpPost]
public Person Index(Person person)
{
return person;
}
}
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
We can see that there is a single action method on the controller, a POST
action, which takes a single parameter - an instance of the Person
class. The controller then just echoes that object out, back to the response.
So where does the Person
parameter come from? Model binding to the rescue! There are a number of different places the model binders can look for data in order to hydrate the person object. The model binders are highly extensible, and allow custom implementations, but common bindings include:
- Route values - navigating to a route such as
{controller}/{action}/{id}
will allow binding to anid
parameter - Querystrings - If you have passed variables as querystring parameters such as
?FirstName=Andrew
, then theFirstName
parameter can be bound. - Body - If you send data in the body of the post, this can be bound to the Person object
- Header - You can also bind to HTTP header values, though this is less common.
So you can see there are a number of ways to send data to the server and have the model binder automatically create the correct method parameter for you. Some require explcit configuration, while others you get for free. For example Route values and querystring parameters are always bound, and for complex types (i.e. not primitives like string
or int
) the body is also bound.
It is important to note that if the model binders fail to bind the parameters for some reason, they will not throw an error, instead you will receive a default object, with none of the properties set, which is the behaviour we showed earlier.
How it works in ASP.NET 4
To play with what's going on here I created two projects, one using ASP.NET 4 and the other using the latest ASP.NET Core at the time of writing (so very nearly RC2). You can find them on GitHub here and here.
In the ASP.NET WebApi project, there is a simple controller which takes a Person
object and simply returns the object back as I showed in the previous section.
On a simple web page, we then make POST
s (using jQuery for convenience), sending requests either x-www-form-urlencoded
(as you would get from a normal form POST
) or as JSON.
//form encoded data
var dataType = 'application/x-www-form-urlencoded; charset=utf-8';
var data = $('form').serialize();
//JSON data
var dataType = 'application/json; charset=utf-8';
var data = {
FirstName: 'Andrew',
LastName: 'Lock',
Age: 31
}
console.log('Submitting form...');
$.ajax({
type: 'POST',
url: '/Person/Index',
dataType: 'json',
contentType: dataType,
data: data,
success: function(result) {
console.log('Data received: ');
console.log(result);
}
});
This will create an HTTP request for the form encoded POST
similar to (elided for brevity):
POST /api/Person/UnProtected HTTP/1.1
Host: localhost:5000
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
FirstName=Andrew&LastName=Lock&Age=31
and for the JSON post:
POST /api/Person/UnProtected HTTP/1.1
Host: localhost:5000
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/json; charset=UTF-8
{"FirstName":"Andrew","LastName":"Lock","Age":"31"}
Sending these two POST
s elicits the following console response:
In both cases the controller has bound to the body of the HTTP request, and the parameters we sent were returned back to us, without us having to do anything declarative. The model binders do all the magic for us. Note that although I've been working with a WebApi controller, the MVC controller model binders behave the same in this example, and would bind both POST
s.
The new way in ASP.NET Core
So, moving on to ASP.NET Core, we create a similar controller, using the same Person
class as a parameter as before:
public class PersonController : Controller
{
[HttpPost]
public IActionResult Index(Person person)
{
return Json(person);
}
}
Using the same HTTP requests as previously, we see the following console output, where the x-www-url-formencoded
POST
is bound correctly, but the JSON POST
is not.
In order to bind the JSON correctly in ASP.NET Core, you must modify your action to include the attribute [FromBody]
on the parameter. This tells the framework to use the content-type
header of the request to decide which of the configured IInputFormatter
s to use for model binding.
By default, when you call AddMvc()
in Startup.cs
, a JSON formatter, JsonInputFormatter
, is automatically configured, but you can add additional formatters if you need to, for example to bind XML to an object.
With that in mind, our new controller looks as follows:
public class PersonController : Controller
{
[HttpPost]
public IActionResult Index([FromBody] Person person)
{
return Json(person);
}
}
And our JSON POST
now works like magic again!
So just always include [FromBody]?
So if you were thinking you can just always use [FromBody]
in your methods, hold your horses. Lets see what happens when you hit your new endpoint with a x-www-url-formencoded
request:
Oh dear. In this case, we have specifically told the ModelBinder to bind the body of the post, which is FirstName=Andrew&LastName=Lock&Age=31
, using an IInputFormatter
. Unfortunately, the JSON formatter is the only formatter we have and that doesn't match our content type, so we get a 415
error response.
In order to specifically bind to the form parameters we can either remove the FromBody
attribute or add the alternative FromForm
attribute, both of which will allow our form data to be bound but again will prevent the JSON binding correctly.
But what if I need to bind both data types?
In some cases you may need to be able to bind both types of data to an action. In that case, you're a little bit stuck, as it won't be possible to have the same end point receive two different sets of data.
Instead you will need to create two different action methods which can specifically bind the data you need to send, and then delegate the processing call to a common method:
public class PersonController : Controller
{
//This action at /Person/Index can bind form data
[HttpPost]
public IActionResult Index(Person person){
return DoSomething(person);
}
//This action at /Person/IndexFromBody can bind JSON
[HttpPost]
public IActionResult IndexFromBody([FromBody] Person person){
return DoSomething(person);
}
private IActionResult DoSomething(Person person){
// do something with the person here
// ...
return Json(person);
}
}
You may find it inconvenient to have to use two different routes for essentially the same action. Unfortunately, routes are obviously mapped to actions before model binding has occurred, so the model binder cannot be used as a discriminator. If you try to map the two above actions to the same route you will get an error saying Request matched multiple actions resulting in ambiguity
. It may be possible to create a custom route to call the appropriate action based on header values, but in all likelihood that will just be more effort than it's worth!
Why the change?
So why has this all changed? Wasn't it simpler and easier the old way? Well, maybe, though there are a number of gotchas to watch out for, particularly when POST
ing primitive types.
The main reason, according to Damian Edwards at the community standup, is for security reasons, in particular cross-site request forgery (CSRF) prevention. I will do a later post on anti-CSRF in ASP.NET Core, but in essence, when model binding can occur from multiple different sources, as it did in ASP.NET 4, the resulting stack is not secure by default. I confess I haven't got my head around exactly why that is yet or how it could be exploited, but I presume it is related to identifying your anti-CSRF FormToken
when you are getting your data from multiple sources.
Summary
In short, if your model binding isn't working properly, make sure it's trying to bind from the right part of your request and you have registered the appropriate formatters. If it's JSON binding you're doing, adding [FromBody]
to your parameters should do the trick!
References
- https://docs.asp.net/en/latest/mvc/models/model-binding.html
- https://lbadri.wordpress.com/2014/11/23/web-api-model-binding-in-asp-net-mvc-6-asp-net-5/