This is an update to several posts I did years ago, mainly: https://evolpin.wordpress.com/2012/07/22/posting-complex-types-to-mvc. If you are interested in the source code then download it from here.
In this post are included several methods to post data to .NET core 2.2 MVC controller. The example uses the default MVC template that comes with bootstrap, jQuery validate and unobtrusive scripts.
If you rather just visit a possible custom model binding solution to send multiple objects in a POST JSON request, click here.
The test form is quite simple: a scaffolded Person form with several test buttons.
Note the Scripts section beneath the html. It provides the unobtrusive and validate jQuery code. You don’t have to use it and you may very well use whatever validation you prefer. I just preferred to use it as it comes out-of-the-box with the Visual Studio MVC template.
The ViewModel I used for this example is that of a Person:
public class Person
{
[Required]
public string FirstName { get; set; }
public string LastName { get; set; }
}
One more code change that I introduced into my Startup.cs, is to use the DefaultContractResolver. This will override the, well…, default behavior in MVC 6 that returns camel cased JSON (e.f. firstName instead of FirstName). I prefer the original casing.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddJsonOptions(f => f.SerializerSettings.ContractResolver = new DefaultContractResolver());
}
‘Regular post’ button
The first button is a regular post of the form data. This being a submit button, it’ll auto activate validation.
You can use either this controller code, which receives the IFormCollection (including any form items such as the AntiForgery token).
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(IFormCollection collection)
{
try
{
if (!ModelState.IsValid)
throw new Exception($"not valid");
foreach (var item in collection)
{
_logger.LogInformation($"{item.Key}={item.Value}");
}
return RedirectToAction(nameof(Index));
}
catch
{
return View();
}
}
Alternatively you can use this code which focuses on binding the posted form variables to the Person C# object:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Person person)
{
try
{
if (!ModelState.IsValid)
throw new Exception($"not valid");
_logger.LogInformation($"{nameof(person.FirstName)}={person.FirstName}");
_logger.LogInformation($"{nameof(person.LastName)}={person.LastName}");
return RedirectToAction(nameof(Index));
}
catch
{
return View();
}
}
Enough of this, where’s the Ajax?
The ‘Create ajax’ button uses the following jQuery to post. Note the ‘beforeSend’ which adds the Anti Forgery token. You must specify this or the request will fail due to the controller code [ValidateAntiForgeryToken] attribute that validates it.
// Ajax POST using regular content type: 'application/x-www-form-urlencoded'
$('#btnFormAjax').on('click', function () {
if (myForm.valid()) {
var first = $('#FirstName').val();
var last = $('#LastName').val();
var data = { FirstName: first, LastName: last };
$.ajax({
url: '@Url.Action("CreateAjaxForm")',
type: 'POST',
beforeSend: function (xhr) {
xhr.setRequestHeader("RequestVerificationToken",
$('input:hidden[name="__RequestVerificationToken"]').val());
},
data: data
}).done(function (result) {
alert(result.FullName);
});
}
});
The controller code is quite the same (I omitted the try-catch for abbreviation purposes). Note, that the returned result in this example is JSON but it doesn’t have to be.
// Ajax POST using regular content type: 'application/x-www-form-urlencoded' (non-JSON)
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CreateAjaxForm(Person person)
{
if (!ModelState.IsValid)
throw new Exception($"not valid");
_logger.LogInformation($"{nameof(person.FirstName)}={person.FirstName}");
_logger.LogInformation($"{nameof(person.LastName)}={person.LastName}");
return Json(new { FullName = $"{person.FirstName} {person.LastName}" });
}
This is the Ajax request:
OK, but I want to POST JSON and not application/x-www-form-urlencoded
I prefer to post and receive JSON. It is more consistent and will allow me more flexibility to use more complex objects as will be shown later on.
The default ‘contentType’ for $.ajax is ‘application/x-www-form-urlencoded; charset=UTF-8‘ so we did not have to specify it earlier. To send JSON we now specify a content type of ‘application/json; charset=utf-8‘.
The ‘data’ is now converted to JSON using JSON.stringify() common browser method.
The ‘dataType’ indicates expecting a JSON as the return object.
$('#btnFormAjaxJson').on('click', function () {
if (myForm.valid()) {
var first = $('#FirstName').val();
var last = $('#LastName').val();
var data = { FirstName: first, LastName: last };
$.ajax({
url: '@Url.Action("CreateAjaxFormJson")',
type: 'POST',
beforeSend: function (xhr) {
xhr.setRequestHeader("RequestVerificationToken",
$('input:hidden[name="__RequestVerificationToken"]').val());
},
dataType: 'json',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(data)
}).done(function (result) {
alert(result.FullName)
});
}
});
The controller code has one small but important change: The [FromBody] attribute of the Person argument. Without this, person will not be populated with the values from the request payload and we will waste a lot of time understanding why.
// Ajax POST using JSON (content type: 'application/json')
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CreateAjaxFormJson([FromBody] Person person)
{
if (!ModelState.IsValid)
throw new Exception($"not valid");
_logger.LogInformation($"{nameof(person.FirstName)}={person.FirstName}");
_logger.LogInformation($"{nameof(person.LastName)}={person.LastName}");
return Json(new { FullName = $"{person.FirstName} {person.LastName}" });
}
This time, the request looks like this:
But what if I want to POST more data?
This is where it gets tricky. Unlike the good old WebMethod/PageMethods which allowed you to post and receive multiple JSON and complex data transparently, unfortunately, in MVC controllers this can’t be done and you can have only a single [FromBody] parameter (why??) as you can read here: https://docs.microsoft.com/en-us/aspnet/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api#using-frombody.
So, if you want to send a more complex type, that includes for example arrays or multiple objects, you need to receive them as a single argument. The ‘Create ajax json complex type’ button demonstrates this. The View Model used here is:
public class Person
{
[Required]
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class People
{
public Person[] SeveralPeople { get; set; }
}
In the sending Javascript, it is very important to note the identically structured ‘people’ object as highlighted below:
// form ajax with json complex type
$('#btnFormAjaxJsonComplexType').on('click', function () {
var joe = { FirstName: 'Joe' };
var jane = { FirstName: 'Jane' };
var people = { SeveralPeople: [joe, jane] };
$.ajax({
url: '@Url.Action("CreateAjaxFormJsonComplexType")',
type: 'POST',
beforeSend: function (xhr) {
xhr.setRequestHeader("RequestVerificationToken",
$('input:hidden[name="__RequestVerificationToken"]').val());
},
dataType: 'json',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(people)
}).done(function (result) {
alert(result.Count)
});
});
The controller code receives a single object:
// Ajax POST of a more complex type using JSON (content type: 'application/json')
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CreateAjaxFormJsonComplexType([FromBody] People people)
{
if (!ModelState.IsValid)
throw new Exception($"not valid");
foreach (var person in people.SeveralPeople)
{
_logger.LogInformation($"{nameof(person.FirstName)}={person.FirstName}");
}
return Json(new { Count = people.SeveralPeople.Count() });
}
And the request:
A step back
While ‘people’ might make sense to send as one array object, what happens if you want to send multiple objects which are less related? Again, in past WebMethods this was trivial and transparent. Here, you are required to send them as a single object. For some reason, Microsoft decided to take a step back from a well working and decent WebMethods that exists for years.
Consider the Javascript below. It looks very much alike the previous example, but this time the ‘data’ is not related to the People class in our server side. Instead, it simply binds two objects together to be sent over as JSON.
Please note that I named the parameters ‘one’ and ‘two’. This will be important for the server side binding.
// form ajax with multiple json complex types
$('#btnFormAjaxJsonMultipleComplexTypes').on('click', function () {
var joe = { FirstName: 'Joe' };
var jane = { FirstName: 'Jane' };
var data = { one: joe, two: jane };
$.ajax({
url: '@Url.Action("CreateAjaxFormJsonMultipleComplexType")',
type: 'POST',
beforeSend: function (xhr) {
xhr.setRequestHeader("RequestVerificationToken",
$('input:hidden[name="__RequestVerificationToken"]').val());
},
dataType: 'json',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(data)
}).done(function (result) {
alert(result.Count)
});
});
The server side controller that we would like to have has two parameters (‘one and ‘two’ as send from the client Javascript), but explained earlier, the default model binder in MVC today does not support it and it would not work.
// this would FAIL and not work
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CreateAjaxFormJsonMultipleComplexType([FromBody] Person one, [FromBody] Person two)
{
if (!ModelState.IsValid)
throw new Exception($"not valid");
var people = new[] { one, two };
foreach (var person in people)
{
_logger.LogInformation($"{nameof(person.FirstName)}={person.FirstName}");
}
return Json(new { Count = people.Length });
}
I was looking for a way this can be done. Fortunately it seems that we can tailor our own model binder, meaning that we can implement a custom class to bind the request body to our parameters. Here are a couple of references that helped me out:
https://docs.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-2.2
https://www.c-sharpcorner.com/article/custom-model-binding-in-asp-net-core-mvc/
Writing a custom binder seems easy enough (although I reckon there is much to learn). You need to code a ‘binding provider’. This gets called once per parameter.
public class CustomModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (/* some condition to decide whether we invoke the custom binder or not */)
return new CustomModelBinder();
return null;
}
}
The CustomModelBinder needs to do some work parsing content and create an object. That object will be passed to your controller method. This method will also be called once per parameter.
public class CustomModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var obj =/* some code to populate the parameter that will be passed to your controller method */
bindingContext.Result = ModelBindingResult.Success(obj);
return Task.CompletedTask;
}
}
You need to modify the ConfigureServices in your Startup class to use this binder.
services.AddMvc(
config => config.ModelBinderProviders.Insert(0, new CustomModelBinderProvider()))
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddJsonOptions(f => f.SerializerSettings.ContractResolver = new DefaultContractResolver());
In my case I wanted to populate not a specific model type (as can be seen in the 2 links above), but something that will populate any parameter type. In other words, I wanted something like the [FromBody] that will work with multiple parameters. So I named it [FromBody2]… I even placed a Required option so if a parameter is missing from the request, it may trigger an exception if set.
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class FromBody2 : Attribute
{
public bool Required { get; set; } = false;
}
Note: originally I did not use [FromBody2], and the custom binder worked for ALL parameters. But then I thought it might be better to have some sort of attribute to gain better control, as input arguments in various requests might be different than what we expect.
My binder provider class is checking whether the parameter has the [FromBody2] parameter. If found, it will also pass it to the custom binder so it can be used internally.
public class CustomModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
var metaData = context.Metadata as Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DefaultModelMetadata;
var attr = metaData?.Attributes?.Attributes?.FirstOrDefault(a => a.GetType() == typeof(FromBody2));
if (attr != null)
return new CustomModelBinder((FromBody2)attr);
return null;
}
}
Now the binder itself. This was a bit tricky to write because I am consuming the request body stream, which can be done just once, whereas the custom binder is called once per parameter. Therefore I read the stream once and store it in the HttpContext.Items bag for other parameters. I reckon that there could be much more elegant solutions, but this will do for now. Explanation follows the example.
public class CustomModelBinder : IModelBinder
{
private FromBody2 _attr;
public CustomModelBinder(FromBody2 attr)
{
this._attr = attr;
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var httpContext = bindingContext.HttpContext;
var body = httpContext.Items["body"] as Dictionary<string, object>;
// read the request stream once and store it for other items
if (body == null)
{
string json;
using (StreamReader sr = new StreamReader(httpContext.Request.Body))
{
json = sr.ReadToEnd();
body = JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
httpContext.Items["body"] = body;
}
}
// attempt to find the parameter in the body stream
if (body.TryGetValue(bindingContext.FieldName, out object obj))
{
JObject jsonObj = obj as JObject;
if (jsonObj != null)
{
obj = jsonObj.ToObject(bindingContext.ModelType);
}
else
{
obj = Convert.ChangeType(obj, bindingContext.ModelType);
}
// set as result
bindingContext.Result = ModelBindingResult.Success(obj);
}
else
{
if (this._attr.Required)
{
// throw an informative exception notifying a missing field
throw new ArgumentNullException($"Missing field: '{bindingContext.FieldName}'");
}
}
return Task.CompletedTask;
}
}
Explanation:
- Line 6: Stores the [FromBody2] instance of the parameter. This will be used later to check on the Required property if the request is missing the expected parameter data.
- Line 14: Get the HttpContext.
- Line 15: Check whether we have already cached the body payload.
- Lines 18-28: A one time (per http request) read of the body payload. It will be deserialized to a Dictionary<string, object> and stored in the HttpContext for later parameters to use. Consider adding further validation code here, maybe checking on the ContentType etc.
- Line 31: This is the nice part. The ModelBinder is given the name of the parameter. As a reminder, our desired method signature had a couple of arguments: Person ‘one’ and Person ‘two’. So the FieldName would be ‘one’ and in a second invocation ‘two’. This is very useful because we can extract it from the Dictionary.
- Lines 33-35: We attempt to cast the object to JObject (JSON complex types). If we are successful, we further convert it to the actual parameter type.
- Lines 39-42: If the object is a primitive non-JObject, we simply cast it according to the expected type. You may consider further validations here or just let it fail if the casting fails.
- Line 45: We take the final object and pass it as a ‘successful result’. This would be handed over to our controller method.
- Lines 49-53: If the request payload did not contain the expected parameter name, we may decide to throw an exception if it is marked as Required.
Finally, the controller code looks almost identical to the [failed] example somewhere above. Only this time, the parameters are decorated with [FromBody2]. As a reminder, we did not have to use [FromBody2] at all. This was only in order to gain more control over the binding process and avoid future situations in which this solution might not be suitable.
// Ajax POST of multiple complex type using JSON (content type: 'application/json')
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CreateAjaxFormJsonMultipleComplexType([FromBody2] Person one, [FromBody2] Person two)
{
if (!ModelState.IsValid)
throw new Exception($"not valid");
var people = new[] { one, two };
foreach (var person in people)
{
_logger.LogInformation($"{nameof(person.FirstName)}={person.FirstName}");
}
return Json(new { Count = people.Length });
}
This looks like this:
We can even add more parameters e.g. a Required ‘three’. The current calling Javascript does not pass ‘three’ so an informative exception will be raised, specifying that ‘three’ is missing.
// Ajax POST of multiple complex type using JSON (content type: 'application/json')
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CreateAjaxFormJsonMultipleComplexType([FromBody2] Person one,
[FromBody2] Person two,
[FromBody2(Required = true)] Person three)
{
if (!ModelState.IsValid)
throw new Exception($"not valid");
var people = new[] { one, two };
foreach (var person in people)
{
_logger.LogInformation($"{nameof(person.FirstName)}={person.FirstName}");
}
return Json(new { Count = people.Length });
}
Summary: it is really nice to have complete control over everything, but in the end I would expect Microsoft to fix this and provide an out of the box implementation for receiving multiple JSON objects as multiple parameters.