In our MVC 2 app we have a JSON model binder implemented like so:
public virtual object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
string input;
using (var reader = new StreamReader(controllerContext.HttpContext.Request.InputStream))
{
input = reader.ReadToEnd();
}
return JsonConvert.DeserializeObject(
input,
bindingContext.ModelType);
}
After updating to MVC 4 I noticed that we were getting null incoming models for incoming JSON posts. When digging in it became apparent that something upstream was advancing the stream. This was easy enough to fix, like so
public virtual object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
string input;
//something upstream after MVC 4 upgrade is advancing the stream to end before we can read it
controllerContext.HttpContext.Request.InputStream.Position = 0;
using (var reader = new StreamReader(controllerContext.HttpContext.Request.InputStream))
{
input = reader.ReadToEnd();
}
return JsonConvert.DeserializeObject(
input,
bindingContext.ModelType);
}
But I am wondering what happened that made the change necessary? Was the prior implementation only working by coincidence?
No, The prior implementation was not working by coincidence.
ASP.NET MVC 3
introduced built-in JSON binding support that enables action methods to receive JSON-encoded data and model-bind it to action-method parameters.
JsonValueProviderFactory
is registered by default in ASP.NET MVC 3
and onwards. The JSON
value provider runs before model binding and serializes request data into a dictionary. The dictionary data is then passed to the model binder
.
Let's see how JsonValueProviderFactory
works.Here is link for the source code of JsonValueProviderFactory
provided in ASP.NET MVC Open Source code JsonValueProviderFactory.cs
GetDeserializedObject
method, defined in JsonValueProviderFactory.cs
, reads the stream
if the Content-Type
is set to application/json
and hence it leaves the Request.InputStream
at the end of the stream. So here GetDeserializedObject
is called first and then BindModel
is called. Since
GetDeserializedObject
has already read the stream once and advanced the Request.InputStream
to the end of the stream, we need to reset the Request.InputStream
again in BindModel
private static object GetDeserializedObject(ControllerContext controllerContext)
{
if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
{
// not JSON request
return null;
}
StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
string bodyText = reader.ReadToEnd();
if (String.IsNullOrEmpty(bodyText))
{
// no JSON data
return null;
}
JavaScriptSerializer serializer = new JavaScriptSerializer();
object jsonData = serializer.DeserializeObject(bodyText);
return jsonData;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With