Posting JSON Data to ASP.NET MVC
C#Jqueryasp.net MvcAjaxJsonC# Problem Overview
Im trying to get a list of line items to a webpage using JSON, which will then be manipulated and sent back to the server by ajax request using the same JSON structure that arrived (except having had a field values changed).
Receiving data from the server is easy, manipulation even easier! but sending that JSON data back to the server for saving... suicide time! PLEASE can someone help!
Javascript
var lineitems;
// get data from server
$.ajax({
url: '/Controller/GetData/',
success: function(data){
lineitems = data;
}
});
// post data to server
$.ajax({
url: '/Controller/SaveData/',
data: { incoming: lineitems }
});
C# - Objects
public class LineItem{
public string reference;
public int quantity;
public decimal amount;
}
C# - Controller
public JsonResult GetData()
{
IEnumerable<LineItem> lineItems = ... ; // a whole bunch of line items
return Json(lineItems);
}
public JsonResult SaveData(IEnumerable<LineItem> incoming){
foreach(LineItem item in incoming){
// save some stuff
}
return Json(new { success = true, message = "Some message" });
}
The data arrives at the server as serialized post data. The automated model binder tries to bind IEnumerable<LineItem> incoming
and surprisingly gets the resulting IEnumerable
has the correct number of LineItems
- it just doesnt populate them with data.
SOLUTION
Using answers from a number of sources, primarily djch
on another stackoverflow post and BeRecursive
below, I solved my problem using two main methods.
Server Side
The deserialiser below requires reference to System.Runtime.Serialization
and using System.Runtime.Serialization.Json
private T Deserialise<T>(string json)
{
using (var ms = new MemoryStream(Encoding.Unicode.GetBytes(json)))
{
var serialiser = new DataContractJsonSerializer(typeof(T));
return (T)serialiser.ReadObject(ms);
}
}
public void Action(int id, string items){
IEnumerable<LineItem> lineitems = Deserialise<IEnumerable<LineItem>>(items);
// do whatever needs to be done - create, update, delete etc.
}
Client Side
It uses json.org's stringify method, available in this dependecy https://github.com/douglascrockford/JSON-js/blob/master/json2.js (which is 2.5kb when minified)
$.ajax({
type: 'POST',
url: '/Controller/Action',
data: { 'items': JSON.stringify(lineItems), 'id': documentId }
});
C# Solutions
Solution 1 - C#
Take a look at Phil Haack's post on model binding JSON data. The problem is that the default model binder doesn't serialize JSON properly. You need some sort of ValueProvider OR you could write a custom model binder:
using System.IO;
using System.Web.Script.Serialization;
public class JsonModelBinder : DefaultModelBinder {
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
if(!IsJSONRequest(controllerContext)) {
return base.BindModel(controllerContext, bindingContext);
}
// Get the JSON data that's been posted
var request = controllerContext.HttpContext.Request;
//in some setups there is something that already reads the input stream if content type = 'application/json', so seek to the begining
request.InputStream.Seek(0, SeekOrigin.Begin);
var jsonStringData = new StreamReader(request.InputStream).ReadToEnd();
// Use the built-in serializer to do the work for us
return new JavaScriptSerializer()
.Deserialize(jsonStringData, bindingContext.ModelMetadata.ModelType);
// -- REQUIRES .NET4
// If you want to use the .NET4 version of this, change the target framework and uncomment the line below
// and comment out the above return statement
//return new JavaScriptSerializer().Deserialize(jsonStringData, bindingContext.ModelMetadata.ModelType);
}
private static bool IsJSONRequest(ControllerContext controllerContext) {
var contentType = controllerContext.HttpContext.Request.ContentType;
return contentType.Contains("application/json");
}
}
public static class JavaScriptSerializerExt {
public static object Deserialize(this JavaScriptSerializer serializer, string input, Type objType) {
var deserializerMethod = serializer.GetType().GetMethod("Deserialize", BindingFlags.NonPublic | BindingFlags.Static);
// internal static method to do the work for us
//Deserialize(this, input, null, this.RecursionLimit);
return deserializerMethod.Invoke(serializer,
new object[] { serializer, input, objType, serializer.RecursionLimit });
}
}
And tell MVC to use it in your Global.asax file:
ModelBinders.Binders.DefaultBinder = new JsonModelBinder();
Also, this code makes use of the content type = 'application/json' so make sure you set that in jquery like so:
$.ajax({
dataType: "json",
contentType: "application/json",
type: 'POST',
url: '/Controller/Action',
data: { 'items': JSON.stringify(lineItems), 'id': documentId }
});
Solution 2 - C#
The simplest way of doing this
I urge you to read this blog post that directly addresses your problem.
Using custom model binders isn't really wise as Phil Haack pointed out (his blog post is linked in the upper blog post as well).
Basically you have three options:
-
Write a
JsonValueProviderFactory
and use a client side library likejson2.js
to communicate wit JSON directly. -
Write a
JQueryValueProviderFactory
that understands the jQuery JSON object transformation that happens in$.ajax
or -
Use the very simple and quick jQuery plugin outlined in the blog post, that prepares any JSON object (even arrays that will be bound to
IList<T>
and dates that will correctly parse on the server side asDateTime
instances) that will be understood by Asp.net MVC default model binder.
Of all three, the last one is the simplest and doesn't interfere with Asp.net MVC inner workings thus lowering possible bug surface. Using this technique outlined in the blog post will correctly data bind your strong type action parameters and validate them as well. So it is basically a win win situation.
Solution 3 - C#
In MVC3 they've added this.
But whats even more nice is that since MVC source code is open you can grab the ValueProvider and use it yourself in your own code (if youre not on MVC3 yet).
You will end up with something like this
ValueProviderFactories.Factories.Add(new JsonValueProviderFactory())
Solution 4 - C#
I solved this problem following vestigal's tips here:
When I needed to post a large json to an action in a controller, I would get the famous "Error during deserialization using the JSON JavaScriptSerializer. The length of the string exceeds the value set on the maxJsonLength property.\r\nParameter name: input value provider".
What I did is create a new ValueProviderFactory, LargeJsonValueProviderFactory, and set the MaxJsonLength = Int32.MaxValue in the GetDeserializedObject method
public sealed class LargeJsonValueProviderFactory : ValueProviderFactory
{
private static void AddToBackingStore(LargeJsonValueProviderFactory.EntryLimitedDictionary backingStore, string prefix, object value)
{
IDictionary<string, object> dictionary = value as IDictionary<string, object>;
if (dictionary != null)
{
foreach (KeyValuePair<string, object> keyValuePair in (IEnumerable<KeyValuePair<string, object>>) dictionary)
LargeJsonValueProviderFactory.AddToBackingStore(backingStore, LargeJsonValueProviderFactory.MakePropertyKey(prefix, keyValuePair.Key), keyValuePair.Value);
}
else
{
IList list = value as IList;
if (list != null)
{
for (int index = 0; index < list.Count; ++index)
LargeJsonValueProviderFactory.AddToBackingStore(backingStore, LargeJsonValueProviderFactory.MakeArrayKey(prefix, index), list[index]);
}
else
backingStore.Add(prefix, value);
}
}
private static object GetDeserializedObject(ControllerContext controllerContext)
{
if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
return (object) null;
string end = new StreamReader(controllerContext.HttpContext.Request.InputStream).ReadToEnd();
if (string.IsNullOrEmpty(end))
return (object) null;
var serializer = new JavaScriptSerializer {MaxJsonLength = Int32.MaxValue};
return serializer.DeserializeObject(end);
}
/// <summary>Returns a JSON value-provider object for the specified controller context.</summary>
/// <returns>A JSON value-provider object for the specified controller context.</returns>
/// <param name="controllerContext">The controller context.</param>
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
if (controllerContext == null)
throw new ArgumentNullException("controllerContext");
object deserializedObject = LargeJsonValueProviderFactory.GetDeserializedObject(controllerContext);
if (deserializedObject == null)
return (IValueProvider) null;
Dictionary<string, object> dictionary = new Dictionary<string, object>((IEqualityComparer<string>) StringComparer.OrdinalIgnoreCase);
LargeJsonValueProviderFactory.AddToBackingStore(new LargeJsonValueProviderFactory.EntryLimitedDictionary((IDictionary<string, object>) dictionary), string.Empty, deserializedObject);
return (IValueProvider) new DictionaryValueProvider<object>((IDictionary<string, object>) dictionary, CultureInfo.CurrentCulture);
}
private static string MakeArrayKey(string prefix, int index)
{
return prefix + "[" + index.ToString((IFormatProvider) CultureInfo.InvariantCulture) + "]";
}
private static string MakePropertyKey(string prefix, string propertyName)
{
if (!string.IsNullOrEmpty(prefix))
return prefix + "." + propertyName;
return propertyName;
}
private class EntryLimitedDictionary
{
private static int _maximumDepth = LargeJsonValueProviderFactory.EntryLimitedDictionary.GetMaximumDepth();
private readonly IDictionary<string, object> _innerDictionary;
private int _itemCount;
public EntryLimitedDictionary(IDictionary<string, object> innerDictionary)
{
this._innerDictionary = innerDictionary;
}
public void Add(string key, object value)
{
if (++this._itemCount > LargeJsonValueProviderFactory.EntryLimitedDictionary._maximumDepth)
throw new InvalidOperationException("JsonValueProviderFactory_RequestTooLarge");
this._innerDictionary.Add(key, value);
}
private static int GetMaximumDepth()
{
NameValueCollection appSettings = ConfigurationManager.AppSettings;
if (appSettings != null)
{
string[] values = appSettings.GetValues("aspnet:MaxJsonDeserializerMembers");
int result;
if (values != null && values.Length > 0 && int.TryParse(values[0], out result))
return result;
}
return 1000;
}
}
}
Then, in the Application_Start method from Global.asax.cs, replace the ValueProviderFactory with the new one:
protected void Application_Start()
{
...
//Add LargeJsonValueProviderFactory
ValueProviderFactory jsonFactory = null;
foreach (var factory in ValueProviderFactories.Factories)
{
if (factory.GetType().FullName == "System.Web.Mvc.JsonValueProviderFactory")
{
jsonFactory = factory;
break;
}
}
if (jsonFactory != null)
{
ValueProviderFactories.Factories.Remove(jsonFactory);
}
var largeJsonValueProviderFactory = new LargeJsonValueProviderFactory();
ValueProviderFactories.Factories.Add(largeJsonValueProviderFactory);
}
Solution 5 - C#
You can try these.
- stringify your JSON Object before calling the server action via ajax
- deserialize the string in the action then use the data as a dictionary.
Javascript sample below (sending the JSON Object
$.ajax(
{
type: 'POST',
url: 'TheAction',
data: { 'data': JSON.stringify(theJSONObject)
}
})
Action (C#) sample below
[HttpPost]
public JsonResult TheAction(string data) {
string _jsonObject = data.Replace(@"\", string.Empty);
var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
Dictionary<string, string> jsonObject = serializer.Deserialize<Dictionary<string, string>>(_jsonObject);
return Json(new object{status = true});
}
Solution 6 - C#
I solved using a "manual" deserialization. I'll explain in code
public ActionResult MyMethod([System.Web.Http.FromBody] MyModel model)
{
if (module.Fields == null && !string.IsNullOrEmpty(Request.Form["fields"]))
{
model.Fields = JsonConvert.DeserializeObject<MyFieldModel[]>(Request.Form["fields"]);
}
//... more code
}
Solution 7 - C#
If you've got ther JSON data coming in as a string (e.g. '[{"id":1,"name":"Charles"},{"id":8,"name":"John"},{"id":13,"name":"Sally"}]')
Then I'd use JSON.net and use Linq to JSON to get the values out...
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (Request["items"] != null)
{
var items = Request["items"].ToString(); // Get the JSON string
JArray o = JArray.Parse(items); // It is an array so parse into a JArray
var a = o.SelectToken("[0].name").ToString(); // Get the name value of the 1st object in the array
// a == "Charles"
}
}
}
Solution 8 - C#
BeRecursive's answer is the one I used, so that we could standardize on Json.Net (we have MVC5 and WebApi 5 -- WebApi 5 already uses Json.Net), but I found an issue. When you have parameters in your route to which you're POSTing, MVC tries to call the model binder for the URI values, and this code will attempt to bind the posted JSON to those values.
Example:
[HttpPost]
[Route("Customer/{customerId:int}/Vehicle/{vehicleId:int}/Policy/Create"]
public async Task<JsonNetResult> Create(int customerId, int vehicleId, PolicyRequest policyRequest)
The BindModel
function gets called three times, bombing on the first, as it tries to bind the JSON to customerId
with the error: Error reading integer. Unexpected token: StartObject. Path '', line 1, position 1.
I added this block of code to the top of BindModel
:
if (bindingContext.ValueProvider.GetValue(bindingContext.ModelName) != null) {
return base.BindModel(controllerContext, bindingContext);
}
The ValueProvider, fortunately, has route values figured out by the time it gets to this method.
Solution 9 - C#
I see everyone here "took the long route!". As long as you are using MVC
, I strongly recommend you to use the easiest method over all which is Newtonsoft.JSON
... Also If you dont wanna use libraries check the answer links below. I took a good research time for this to solve the issue for my self and these are the solutions I found;
First implement the Newtonsoft.Json:
using Newtonsoft.Json;
Prepare your Ajax request:
$.ajax({
dataType: "json",
contentType: "application/json",
type: 'POST',
url: '/Controller/Action',
data: { 'items': JSON.stringify(lineItems), 'id': documentId }
});
Then go for your result class:
public ActionResult SaveData(string incoming, int documentId){
// DeSerialize into your Model as your Model Array
LineItem[] jsr = JsonConvert.DeserializeObject<LineItem[]>(Temp);
foreach(LineItem item in jsr){
// save some stuff
}
return Json(new { success = true, message = "Some message" });
}
See the trick up there? Instead of using JsonResult
I used regular ActionResult
with a string which includes json string. Then deserialized into my Model
so I can use this in any action method I have.
Plus sides of this method is :
- Easier to pass between actions,
- Lesser and more clear usage of code,
- No need to change your model,
- Simple implementation with
JSON.stringify(Model)
- Passing only string
Down sides of this method is :
- Passing only string
- Deserialization process
Also check these questions and answers which are really helpfull:
https://stackoverflow.com/a/45682516/861019
another method :
https://stackoverflow.com/a/31656160/861019
and another method :